Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Bugfixes
* Fixed type hints of assert methods to match actual signature (`PR #1271 <https://github.com/pytest-dev/pytest-django/pull/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 <https://github.com/pytest-dev/pytest-django/issues/1152>`__).

v4.12.0 (2026-02-14)
--------------------
Expand Down
38 changes: 31 additions & 7 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

import argparse
import contextlib
import inspect
import os
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand All @@ -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)


Expand Down
59 changes: 59 additions & 0 deletions tests/test_initialization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from textwrap import dedent

import pytest

from .helpers import DjangoPytester


Expand Down Expand Up @@ -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