diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index a466ddb1..8c4cad94 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -197,66 +197,63 @@ def _django_db_helper( "django_db_serialized_rollback" in request.fixturenames ) - django_db_blocker.unblock() - - import django.db - import django.test - - if transactional: - test_case_class = django.test.TransactionTestCase - else: - test_case_class = django.test.TestCase - - _reset_sequences = reset_sequences - _serialized_rollback = serialized_rollback - _databases = databases - _available_apps = available_apps - - class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] - reset_sequences = _reset_sequences - serialized_rollback = _serialized_rollback - if _databases is not None: - databases = _databases - if _available_apps is not None: - available_apps = _available_apps - - # For non-transactional tests, skip executing `django.test.TestCase`'s - # `setUpClass`/`tearDownClass`, only execute the super class ones. - # - # `TestCase`'s class setup manages the `setUpTestData`/class-level - # transaction functionality. We don't use it; instead we (will) offer - # our own alternatives. So it only adds overhead, and does some things - # which conflict with our (planned) functionality, particularly, it - # closes all database connections in `tearDownClass` which inhibits - # wrapping tests in higher-scoped transactions. - # - # It's possible a new version of Django will add some unrelated - # functionality to these methods, in which case skipping them completely - # would not be desirable. Let's cross that bridge when we get there... - if not transactional: - - @classmethod - def setUpClass(cls) -> None: - super(django.test.TestCase, cls).setUpClass() - - @classmethod - def tearDownClass(cls) -> None: - super(django.test.TestCase, cls).tearDownClass() - - PytestDjangoTestCase.setUpClass() - - test_case = PytestDjangoTestCase(methodName="__init__") - test_case._pre_setup() + with django_db_blocker.unblock(): + import django.db + import django.test - yield + if transactional: + test_case_class = django.test.TransactionTestCase + else: + test_case_class = django.test.TestCase + + _reset_sequences = reset_sequences + _serialized_rollback = serialized_rollback + _databases = databases + _available_apps = available_apps + + class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] + reset_sequences = _reset_sequences + serialized_rollback = _serialized_rollback + if _databases is not None: + databases = _databases + if _available_apps is not None: + available_apps = _available_apps + + # For non-transactional tests, skip executing `django.test.TestCase`'s + # `setUpClass`/`tearDownClass`, only execute the super class ones. + # + # `TestCase`'s class setup manages the `setUpTestData`/class-level + # transaction functionality. We don't use it; instead we (will) offer + # our own alternatives. So it only adds overhead, and does some things + # which conflict with our (planned) functionality, particularly, it + # closes all database connections in `tearDownClass` which inhibits + # wrapping tests in higher-scoped transactions. + # + # It's possible a new version of Django will add some unrelated + # functionality to these methods, in which case skipping them completely + # would not be desirable. Let's cross that bridge when we get there... + if not transactional: + + @classmethod + def setUpClass(cls) -> None: + super(django.test.TestCase, cls).setUpClass() + + @classmethod + def tearDownClass(cls) -> None: + super(django.test.TestCase, cls).tearDownClass() + + PytestDjangoTestCase.setUpClass() + + test_case = PytestDjangoTestCase(methodName="__init__") + test_case._pre_setup() - test_case._post_teardown() + yield - PytestDjangoTestCase.tearDownClass() + test_case._post_teardown() - PytestDjangoTestCase.doClassCleanups() + PytestDjangoTestCase.tearDownClass() - django_db_blocker.restore() + PytestDjangoTestCase.doClassCleanups() def validate_django_db(marker: pytest.Mark) -> _DjangoDb: diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 62fa1791..6b1aa137 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -506,17 +506,26 @@ def django_test_environment(request: pytest.FixtureRequest) -> Generator[None, N @pytest.fixture(scope="session") def django_db_blocker(request: pytest.FixtureRequest) -> DjangoDbBlocker | None: - """Wrapper around Django's database access. + """Block or unblock database access. - This object can be used to re-enable database access. This fixture is used - internally in pytest-django to build the other fixtures and can be used for - special database handling. + This is an advanced feature for implementing database fixtures. - The object is a context manager and provides the methods - .unblock()/.block() and .restore() to temporarily enable database access. + By default, pytest-django blocks access the the database. In tests which + request access to the database, the access is automatically unblocked. - This is an advanced feature that is meant to be used to implement database - fixtures. + In a test or fixture context where database access is blocked, you can + temporarily unblock access as follows:: + + with django_db_blocker.unblock(): + ... + + In a test or fixture context where database access is not blocked, you can + temporarily block access as follows:: + + with django_db_blocker.block(): + ... + + This fixture is also used internally by pytest-django. """ if not django_settings_is_configured(): return None @@ -798,8 +807,8 @@ def __init__(self, *, _ispytest: bool = False) -> None: def _dj_db_wrapper(self) -> django.db.backends.base.base.BaseDatabaseWrapper: from django.db.backends.base.base import BaseDatabaseWrapper - # The first time the _dj_db_wrapper is accessed, we will save a - # reference to the real implementation. + # The first time the _dj_db_wrapper is accessed, save a reference to the + # real implementation. if self._real_ensure_connection is None: self._real_ensure_connection = BaseDatabaseWrapper.ensure_connection