Skip to content

Commit 1ba1eda

Browse files
mpasternakclaude
andcommitted
Release 0.2.1: quiet preload when Django not yet configured
Downgrade the rootdir-conftest preload error to DEBUG when the exception chain contains ImproperlyConfigured — typical when a conftest transitively imports model_bakery>=1.20 (which calls apps.is_installed at module import). Tests still pass, only the noisy traceback at the end of the run is gone. Unrelated conftest errors keep the WARNING + traceback behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f48f036 commit 1ba1eda

4 files changed

Lines changed: 108 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.1] - 2026-05-09
11+
12+
### Changed
13+
14+
- The rootdir-conftest preload (the mechanism that lets `register()` calls
15+
run before pytest-django's hook) now logs at DEBUG level — instead of
16+
emitting a full WARNING-level traceback — when the import fails because
17+
Django settings aren't configured yet. This is the common case when a
18+
conftest transitively imports `model_bakery >= 1.20`, which calls
19+
`apps.is_installed(...)` at module import time. The conftest is still
20+
re-imported by pytest's normal trylast loader after our env injection
21+
and pytest-django run, so tests work as before — only the noisy
22+
traceback at the end of the run is gone. Unrelated conftest errors
23+
(anything not chaining to `ImproperlyConfigured`) keep the original
24+
WARNING + traceback behaviour.
25+
1026
## [0.2.0] - 2026-05-08
1127

1228
### Added

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "pytest-testcontainers-django"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "Bridge between pytest-testcontainers and pytest-django: starts the DB container before Django imports settings."
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/pytest_testcontainers_django/plugin.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ def _resolve_baseline_path(config: DjangoContainerConfig) -> DjangoContainerConf
9494
return replace(config, postgres=new_pg)
9595

9696

97+
def _is_django_not_configured(exc: BaseException) -> bool:
98+
"""Walk ``__cause__`` / ``__context__`` for Django's ``ImproperlyConfigured``.
99+
100+
Matched by type name to avoid importing Django from this plugin (preload
101+
runs before Django is configured, and the plugin must work in setups
102+
where Django is loaded lazily).
103+
"""
104+
seen: set[int] = set()
105+
cur: BaseException | None = exc
106+
while cur is not None and id(cur) not in seen:
107+
seen.add(id(cur))
108+
if type(cur).__name__ == "ImproperlyConfigured":
109+
return True
110+
cur = cur.__cause__ or cur.__context__
111+
return False
112+
113+
97114
def _preload_rootdir_conftest(early_config: pytest.Config) -> None:
98115
"""Force-import the rootdir ``conftest.py`` so its ``register()`` calls run.
99116
@@ -117,9 +134,28 @@ def _preload_rootdir_conftest(early_config: pytest.Config) -> None:
117134
rootpath,
118135
consider_namespace_packages=consider_namespace_packages,
119136
)
120-
except Exception:
121-
# Conftest import failure here would re-raise from pytest's own
122-
# trylast loader anyway — we just preload, never swallow.
137+
except Exception as exc:
138+
if _is_django_not_configured(exc):
139+
# Expected at preload time: the conftest transitively imports code
140+
# that touches Django settings (e.g. model_bakery>=1.20 calls
141+
# apps.is_installed() at module import). Our hook runs before
142+
# pytest-django has configured DJANGO_SETTINGS_MODULE, so the
143+
# import fails here. Pytest's own trylast loader will re-import
144+
# the conftest later (after Django is configured *and* after our
145+
# env injection has populated DB host/port), where it will
146+
# succeed. Caveat: any register() calls *before* the failing
147+
# import won't run early — users hitting this should configure
148+
# via pyproject.toml's [tool.pytest-testcontainers-django]
149+
# instead, or move the offending import.
150+
logger.debug(
151+
"preloading rootdir conftest.py deferred: Django settings "
152+
"not configured yet (will be re-imported by pytest's normal "
153+
"loader)"
154+
)
155+
return
156+
# Real conftest error — pytest's trylast loader would re-raise it
157+
# with a full traceback anyway; we surface it here too so users see
158+
# it once even if something masks the later re-raise.
123159
logger.exception("preloading rootdir conftest.py raised; continuing")
124160

125161

tests/test_plugin.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,58 @@ def test_invalid_init_script_path_fails_loudly(pytester: pytest.Pytester) -> Non
426426
assert result.ret != 0
427427

428428

429+
def test_preload_quiet_when_conftest_hits_django_not_configured(
430+
pytester: pytest.Pytester,
431+
) -> None:
432+
"""Regression: when the user's conftest transitively imports code that
433+
touches Django settings before they're configured (e.g. ``model_bakery``
434+
>= 1.20 calling ``apps.is_installed`` at module import), our preload
435+
fails. That failure is benign — pytest's normal trylast loader will
436+
re-import the conftest later, after our env injection and pytest-django
437+
have run — so we should NOT print a full traceback for it.
438+
439+
The conftest below raises a locally-defined ``ImproperlyConfigured`` on
440+
its first import and lets the second (pytest core's) import succeed,
441+
matching the real-world flow.
442+
"""
443+
sentinel = """
444+
import sys
445+
if not getattr(sys, "_ttd_preload_seen", False):
446+
sys._ttd_preload_seen = True
447+
class ImproperlyConfigured(Exception):
448+
pass
449+
raise ImproperlyConfigured(
450+
"Requested setting INSTALLED_APPS, but settings are not configured."
451+
)
452+
"""
453+
_bootstrap(pytester, conftest_extra=sentinel)
454+
pytester.makepyfile("def test_dummy(): pass")
455+
result = pytester.runpytest_subprocess("-p", "no:cacheprovider")
456+
result.assert_outcomes(passed=1)
457+
combined = "\n".join([*result.outlines, *result.errlines])
458+
assert "Traceback" not in combined, combined
459+
assert "preloading rootdir conftest.py raised" not in combined, combined
460+
461+
462+
def test_preload_still_logs_traceback_for_unrelated_conftest_errors(
463+
pytester: pytest.Pytester,
464+
) -> None:
465+
"""The quiet-on-ImproperlyConfigured shortcut must NOT swallow other
466+
conftest import errors. A plain ``ValueError`` should still surface
467+
with a traceback (either from our own logger.exception during preload,
468+
or from pytest's trylast loader when it re-imports and fails again).
469+
"""
470+
sentinel = """
471+
raise ValueError("definitely not a django problem")
472+
"""
473+
_bootstrap(pytester, conftest_extra=sentinel)
474+
pytester.makepyfile("def test_dummy(): pass")
475+
result = pytester.runpytest_subprocess("-p", "no:cacheprovider")
476+
combined = "\n".join([*result.outlines, *result.errlines])
477+
assert "definitely not a django problem" in combined, combined
478+
assert result.ret != 0
479+
480+
429481
def test_hook_runs_before_pytest_django_default_priority(
430482
pytester: pytest.Pytester,
431483
) -> None:

0 commit comments

Comments
 (0)