From a24acbeabfd001a95c3629825b131f0ccbe58867 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Fri, 11 Sep 2026 12:24:49 +0100 Subject: [PATCH] Fix transactional_db when requested via getfixturevalue _django_db_helper decided TestCase vs TransactionTestCase from request.fixturenames, which does not include fixtures requested via getfixturevalue() until they finish. Walk the parent request chain so indirect parametrization of db vs transactional_db uses the right wrapper. Closes https://github.com/pytest-dev/pytest-django/issues/1157 --- docs/changelog.rst | 8 ++++++++ pytest_django/fixtures.py | 33 ++++++++++++++++++++++++++++----- tests/test_database.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e1c8a982..5e59c40d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,14 @@ Compatibility * Official Django 6.1 support. +Bugfixes +^^^^^^^^ + +* Fixed ``db`` / ``transactional_db`` (and related database fixtures) requested + via ``request.getfixturevalue()``, including indirect parametrization, always + using ``django.test.TestCase`` without transaction support + (`#1157 `__). + v4.14.0 (2026-08-10) -------------------- diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 07af3153..a5b0e8ff 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -170,6 +170,7 @@ def django_db_setup( # noqa: PLR0917 django_db_modify_db_settings: None, # noqa: ARG001 ) -> Generator[None]: """Top level fixture to ensure test databases are available""" + from django.db import DEFAULT_DB_ALIAS from django.test.utils import setup_databases, teardown_databases setup_databases_args = {} @@ -181,6 +182,11 @@ def django_db_setup( # noqa: PLR0917 setup_databases_args["keepdb"] = True aliases, serialized_aliases = _get_databases_for_setup(request.session.items) + if not aliases: + # This fixture is only requested when a test needs the database. Tests + # that obtain db fixtures via getfixturevalue() are invisible at + # collection time, so fall back to the default alias. + aliases = {DEFAULT_DB_ALIAS} with django_db_blocker.unblock(): db_cfg = setup_databases( @@ -203,6 +209,24 @@ def django_db_setup( # noqa: PLR0917 ) +def _requested_fixture_names(request: pytest.FixtureRequest) -> set[str]: + """Return fixture names for this test, including those currently being set up. + + ``request.fixturenames`` is the collected fixture closure plus fixtures that + have already finished. A fixture requested via ``request.getfixturevalue()`` + is only recorded after it finishes, so ``_django_db_helper`` (a dependency + of ``db`` / ``transactional_db``) would otherwise miss it. + """ + names = set(request.fixturenames) + current: pytest.FixtureRequest | None = request + while current is not None: + fixturename = current.fixturename + if fixturename is not None: + names.add(fixturename) + current = getattr(current, "_parent_request", None) + return names + + @pytest.fixture def _django_db_helper( request: pytest.FixtureRequest, @@ -233,14 +257,13 @@ def _django_db_helper( available_apps, ) = False, False, None, False, None + requested = _requested_fixture_names(request) + reset_sequences = reset_sequences or ("django_db_reset_sequences" in requested) + serialized_rollback = serialized_rollback or ("django_db_serialized_rollback" in requested) transactional = ( transactional or reset_sequences - or ("transactional_db" in request.fixturenames or "live_server" in request.fixturenames) - ) - reset_sequences = reset_sequences or ("django_db_reset_sequences" in request.fixturenames) - serialized_rollback = serialized_rollback or ( - "django_db_serialized_rollback" in request.fixturenames + or ("transactional_db" in requested or "live_server" in requested) ) with django_db_blocker.unblock(): diff --git a/tests/test_database.py b/tests/test_database.py index aaff070e..e57b0e60 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -101,6 +101,20 @@ def test_transactions_enabled_via_reset_seq( assert not connection.in_atomic_block + def test_transactions_match_dynamic_db_fixture( + self, + all_dbs: None, # noqa: ARG002 + request: pytest.FixtureRequest, + ) -> None: + if not connection.features.supports_transactions: + pytest.skip("transactions required for this test") + + fixture_name = request.node.callspec.params["all_dbs"] + if fixture_name in {"transactional_db", "django_db_reset_sequences"}: + assert not connection.in_atomic_block + else: + assert connection.in_atomic_block + def test_django_db_reset_sequences_fixture( self, db: None, # noqa: ARG002 @@ -213,6 +227,26 @@ def test_durable_transactions(self, all_dbs: None) -> None: # noqa: ARG002 assert Item.objects.get() == item +@pytest.mark.parametrize("db_access", ["db", "transactional_db"], indirect=True) +class TestIndirectDbParametrization: + """Indirect parametrization of db vs transactional_db (issue #1157).""" + + @pytest.fixture + def db_access(self, request: pytest.FixtureRequest) -> str: + fixture_name: str = request.param + request.getfixturevalue(fixture_name) + return fixture_name + + def test_atomic_block_matches_fixture(self, db_access: str) -> None: + if not connection.features.supports_transactions: + pytest.skip("transactions required for this test") + + if db_access == "transactional_db": + assert not connection.in_atomic_block + else: + assert connection.in_atomic_block + + class TestDatabaseFixturesAllOrder: @pytest.fixture def fixture_with_db(self, db: None) -> None: # noqa: ARG002