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
107 changes: 52 additions & 55 deletions pytest_django/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 19 additions & 10 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down