diff --git a/docs/changelog.rst b/docs/changelog.rst index f36bd531..8cfca2ed 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ Bugfixes * Fixed type hints of assert methods to match actual signature (`PR #1271 `__) * Handled Django 6.2's ``ImproperlyConfigured`` (in addition to ``ImportError``) when the configured ``DJANGO_SETTINGS_MODULE`` cannot be imported, so pytest-django still shows its guidance message. * Fixed ``django_db(transaction=True)`` tests being set up twice, which repeated the ``serialized_rollback`` restore and the ``fixtures`` load, and sent ``setting_changed`` and ``post_migrate`` twice when ``available_apps`` is set. +* Fixed ``--help``/``--version`` failing with ``AppRegistryNotReady`` (surfaced as a ``could not load initial conftests`` warning) when a ``conftest.py`` imports Django models at the top level. Django is now set up even when ``--help``/``--version`` are passed, before the initial conftests are loaded for these options (`#1152 `__). v4.12.0 (2026-02-14) -------------------- diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index e92e573c..3bc695df 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -6,6 +6,7 @@ from __future__ import annotations +import argparse import contextlib import inspect import os @@ -312,9 +313,36 @@ def pytest_load_initial_conftests( options = parser.parse_known_args(args) - if options.version or options.help: - return + # pytest still imports the initial conftests for `--help`/`--version`, so + # Django is set up for them as well. Otherwise a conftest with top-level + # model imports fails with AppRegistryNotReady (issue #1152). + is_help_or_version = bool(options.version or options.help) + + # Stashed up front, to be available even if the setup below fails. + report_header: list[str] = [] + early_config.stash[report_header_key] = report_header + early_config.stash[blocking_manager_key] = DjangoDbBlocker(_ispytest=True) + + try: + _initialize_django(early_config, options, args, report_header) + except Exception: + # `--help`/`--version` never run tests, so they must keep working on a + # broken configuration (issue #235); a real run still fails loudly. + if not is_help_or_version: + raise + + +def _initialize_django( + early_config: pytest.Config, + options: argparse.Namespace, + args: list[str], + report_header: list[str], +) -> None: + """Configure and set up Django from the pytest options/ini/environment. + Everything which can fail on a broken configuration lives here, so that + `pytest_load_initial_conftests` can tolerate it for `--help`/`--version`. + """ django_find_project = _get_boolean_value( early_config.getini("django_find_project"), "django_find_project" ) @@ -347,9 +375,6 @@ def _get_option_with_source( ds, ds_source = _get_option_with_source(options.ds, SETTINGS_MODULE_ENV) dc, dc_source = _get_option_with_source(options.dc, CONFIGURATION_ENV) - report_header: list[str] = [] - early_config.stash[report_header_key] = report_header - if ds: report_header.append(f"settings: {ds} (from {ds_source})") os.environ[SETTINGS_MODULE_ENV] = ds @@ -370,8 +395,7 @@ def _get_option_with_source( with _handle_import_error(_django_project_scan_outcome): dj_settings.DATABASES # noqa: B018 - early_config.stash[blocking_manager_key] = DjangoDbBlocker(_ispytest=True) - + # Populates the app registry, which fails on a broken INSTALLED_APPS. _setup_django(early_config) diff --git a/tests/test_initialization.py b/tests/test_initialization.py index 631a41ed..0ac49e96 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -1,5 +1,7 @@ from textwrap import dedent +import pytest + from .helpers import DjangoPytester @@ -60,3 +62,60 @@ def test_ds(): ] ) assert result.ret == 0 + + +@pytest.mark.parametrize("option", ["--help", "--version"]) +def test_django_setup_with_help_and_version( + django_pytester: DjangoPytester, + option: str, +) -> None: + """Django must be set up before conftest files are imported, even for + ``--help``/``--version``, so a top-level Django model import in a + ``conftest.py`` does not fail with ``AppRegistryNotReady``. + + Regression test for https://github.com/pytest-dev/pytest-django/issues/1152 + """ + django_pytester.makeconftest( + """ + from tpkg.app.models import Item # noqa: F401 + + # Only reached if the model import above succeeds (i.e. Django is set + # up). With --version, pytest records but never prints the conftest + # load failure, so we assert on this positive marker instead. + print("conftest-imported-models") + """ + ) + + # A single `--version`/`-V` is short-circuited before pytest loads any + # conftests (pytest #13574), so it wouldn't exercise this path; passing + # it twice forces full startup, which imports the initial conftests. + args = [option, option] if option == "--version" else [option] + result = django_pytester.runpytest_subprocess(*args) + + # `--help` writes its own output around the conftest's, so the marker does + # not necessarily end up on a line of its own. + result.stdout.fnmatch_lines(["*conftest-imported-models*"]) + result.stdout.no_fnmatch_line("*AppRegistryNotReady*") + result.stdout.no_fnmatch_line("*could not load initial conftests*") + assert result.ret == 0 + + +def test_help_with_unusable_configuration(django_pytester: DjangoPytester) -> None: + """Setting Django up for ``--help``/``--version`` must not make them depend + on a usable configuration, no matter where the initialization fails. + + ``test_manage_py_scan.py`` covers an invalid ``DJANGO_SETTINGS_MODULE`` + (issue #235); this covers a failure which is neither an ``ImportError`` nor + related to the settings module. + """ + django_pytester.makeini( + """ + [pytest] + django_find_project = not-a-bool + """ + ) + + result = django_pytester.runpytest_subprocess("--help") + + result.stdout.fnmatch_lines(["*usage:*"]) + assert result.ret == 0