diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f181dde..f90d91cd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,17 @@ Compatibility * Added official support for Django 5.2 (`PR #1179 `__). * Dropped testing on MySQL’s MyISAM storage engine (`PR #1180 `__). +Bugfixes +^^^^^^^^ + +* Stopped setting up and serializing databases on test session setup when not needed (the database is not requested / ``serialized_rollback`` is not used). + On test databases with large amounts of pre-seeded data, this may remove a delay of a few seconds when running ``pytest --reuse-db``. + + The determination of which databases to setup is done by static inspection of the test suite. + Using pytest's dynamic features to request db access, such as :meth:`request.getfixturevalue("db") `, may throw off this analysis. + If you start seeing ``DatabaseOperationForbidden`` or "unable to open database" errors, this is likely the cause. + To fix this, decorate at least one test with the :func:`django_db ` marker with appropriate ``databases`` and ``serialized_rollback`` settings. + v4.10.0 (2025-02-10) -------------------- diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 391aede7..767862b7 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -7,6 +7,7 @@ from functools import partial from typing import ( TYPE_CHECKING, + AbstractSet, Any, Callable, ContextManager, @@ -16,6 +17,7 @@ Literal, Optional, Protocol, + Sequence, Tuple, Union, ) @@ -119,6 +121,56 @@ def django_db_createdb(request: pytest.FixtureRequest) -> bool: return create_db +def _get_databases_for_test(test: pytest.Item) -> tuple[Iterable[str], bool]: + """Get the database aliases that need to be setup for a test, and whether + they need to be serialized.""" + from django.db import DEFAULT_DB_ALIAS, connections + from django.test import TransactionTestCase + + test_cls = getattr(test, "cls", None) + if test_cls and issubclass(test_cls, TransactionTestCase): + serialized_rollback = getattr(test, "serialized_rollback", False) + databases = getattr(test, "databases", None) + else: + fixtures = getattr(test, "fixturenames", ()) + marker_db = test.get_closest_marker("django_db") + if marker_db: + ( + transaction, + reset_sequences, + databases, + serialized_rollback, + available_apps, + ) = validate_django_db(marker_db) + elif "db" in fixtures or "transactional_db" in fixtures or "live_server" in fixtures: + serialized_rollback = "django_db_serialized_rollback" in fixtures + databases = None + else: + return (), False + if databases is None: + return (DEFAULT_DB_ALIAS,), serialized_rollback + elif databases == "__all__": + return connections, serialized_rollback + else: + return databases, serialized_rollback + + +def _get_databases_for_setup( + items: Sequence[pytest.Item], +) -> tuple[AbstractSet[str], AbstractSet[str]]: + """Get the database aliases that need to be setup, and the subset that needs + to be serialized.""" + # Code derived from django.test.utils.DiscoverRunner.get_databases(). + aliases: set[str] = set() + serialized_aliases: set[str] = set() + for test in items: + databases, serialized_rollback = _get_databases_for_test(test) + aliases.update(databases) + if serialized_rollback: + serialized_aliases.update(databases) + return aliases, serialized_aliases + + @pytest.fixture(scope="session") def django_db_setup( request: pytest.FixtureRequest, @@ -140,10 +192,14 @@ def django_db_setup( if django_db_keepdb and not django_db_createdb: setup_databases_args["keepdb"] = True + aliases, serialized_aliases = _get_databases_for_setup(request.session.items) + with django_db_blocker.unblock(): db_cfg = setup_databases( verbosity=request.config.option.verbose, interactive=False, + aliases=aliases, + serialized_aliases=serialized_aliases, **setup_databases_args, )