mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 13:31:09 -05:00
okay fine
This commit is contained in:
17
.venv/lib/python3.12/site-packages/pytest_django/__init__.py
Normal file
17
.venv/lib/python3.12/site-packages/pytest_django/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError: # pragma: no cover
|
||||
# Broken installation, we don't even try.
|
||||
__version__ = "unknown"
|
||||
|
||||
|
||||
from .fixtures import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks
|
||||
from .plugin import DjangoDbBlocker
|
||||
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"DjangoAssertNumQueries",
|
||||
"DjangoCaptureOnCommitCallbacks",
|
||||
"DjangoDbBlocker",
|
||||
]
|
||||
16
.venv/lib/python3.12/site-packages/pytest_django/_version.py
Normal file
16
.venv/lib/python3.12/site-packages/pytest_django/_version.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple, Union
|
||||
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
||||
else:
|
||||
VERSION_TUPLE = object
|
||||
|
||||
version: str
|
||||
__version__: str
|
||||
__version_tuple__: VERSION_TUPLE
|
||||
version_tuple: VERSION_TUPLE
|
||||
|
||||
__version__ = version = '4.9.0'
|
||||
__version_tuple__ = version_tuple = (4, 9, 0)
|
||||
221
.venv/lib/python3.12/site-packages/pytest_django/asserts.py
Normal file
221
.venv/lib/python3.12/site-packages/pytest_django/asserts.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Dynamically load all Django assertion cases and expose them for importing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Callable, Sequence
|
||||
|
||||
from django import VERSION
|
||||
from django.test import LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase
|
||||
|
||||
|
||||
USE_CONTRIB_MESSAGES = VERSION >= (5, 0)
|
||||
|
||||
if USE_CONTRIB_MESSAGES:
|
||||
from django.contrib.messages import Message
|
||||
from django.contrib.messages.test import MessagesTestMixin
|
||||
|
||||
class MessagesTestCase(MessagesTestMixin, TestCase):
|
||||
pass
|
||||
|
||||
test_case = MessagesTestCase("run")
|
||||
else:
|
||||
test_case = TestCase("run")
|
||||
|
||||
|
||||
def _wrapper(name: str):
|
||||
func = getattr(test_case, name)
|
||||
|
||||
@wraps(func)
|
||||
def assertion_func(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return assertion_func
|
||||
|
||||
|
||||
__all__ = []
|
||||
assertions_names: set[str] = set()
|
||||
assertions_names.update(
|
||||
{attr for attr in vars(TestCase) if attr.startswith("assert")},
|
||||
{attr for attr in vars(SimpleTestCase) if attr.startswith("assert")},
|
||||
{attr for attr in vars(LiveServerTestCase) if attr.startswith("assert")},
|
||||
{attr for attr in vars(TransactionTestCase) if attr.startswith("assert")},
|
||||
)
|
||||
|
||||
if USE_CONTRIB_MESSAGES:
|
||||
assertions_names.update(
|
||||
{attr for attr in vars(MessagesTestMixin) if attr.startswith("assert")},
|
||||
)
|
||||
|
||||
for assert_func in assertions_names:
|
||||
globals()[assert_func] = _wrapper(assert_func)
|
||||
__all__.append(assert_func) # noqa: PYI056
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django import forms
|
||||
from django.http.response import HttpResponseBase
|
||||
|
||||
def assertRedirects(
|
||||
response: HttpResponseBase,
|
||||
expected_url: str,
|
||||
status_code: int = ...,
|
||||
target_status_code: int = ...,
|
||||
msg_prefix: str = ...,
|
||||
fetch_redirect_response: bool = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertURLEqual(
|
||||
url1: str,
|
||||
url2: str,
|
||||
msg_prefix: str = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertContains(
|
||||
response: HttpResponseBase,
|
||||
text: object,
|
||||
count: int | None = ...,
|
||||
status_code: int = ...,
|
||||
msg_prefix: str = ...,
|
||||
html: bool = False,
|
||||
) -> None: ...
|
||||
|
||||
def assertNotContains(
|
||||
response: HttpResponseBase,
|
||||
text: object,
|
||||
status_code: int = ...,
|
||||
msg_prefix: str = ...,
|
||||
html: bool = False,
|
||||
) -> None: ...
|
||||
|
||||
def assertFormError(
|
||||
form: forms.BaseForm,
|
||||
field: str | None,
|
||||
errors: str | Sequence[str],
|
||||
msg_prefix: str = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertFormSetError(
|
||||
formset: forms.BaseFormSet,
|
||||
form_index: int | None,
|
||||
field: str | None,
|
||||
errors: str | Sequence[str],
|
||||
msg_prefix: str = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertTemplateUsed(
|
||||
response: HttpResponseBase | str | None = ...,
|
||||
template_name: str | None = ...,
|
||||
msg_prefix: str = ...,
|
||||
count: int | None = ...,
|
||||
): ...
|
||||
|
||||
def assertTemplateNotUsed(
|
||||
response: HttpResponseBase | str | None = ...,
|
||||
template_name: str | None = ...,
|
||||
msg_prefix: str = ...,
|
||||
): ...
|
||||
|
||||
def assertRaisesMessage(
|
||||
expected_exception: type[Exception],
|
||||
expected_message: str,
|
||||
*args,
|
||||
**kwargs,
|
||||
): ...
|
||||
|
||||
def assertWarnsMessage(
|
||||
expected_warning: Warning,
|
||||
expected_message: str,
|
||||
*args,
|
||||
**kwargs,
|
||||
): ...
|
||||
|
||||
def assertFieldOutput(
|
||||
fieldclass,
|
||||
valid,
|
||||
invalid,
|
||||
field_args=...,
|
||||
field_kwargs=...,
|
||||
empty_value: str = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertHTMLEqual(
|
||||
html1: str,
|
||||
html2: str,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertHTMLNotEqual(
|
||||
html1: str,
|
||||
html2: str,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertInHTML(
|
||||
needle: str,
|
||||
haystack: str,
|
||||
count: int | None = ...,
|
||||
msg_prefix: str = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertJSONEqual(
|
||||
raw: str,
|
||||
expected_data: Any,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertJSONNotEqual(
|
||||
raw: str,
|
||||
expected_data: Any,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertXMLEqual(
|
||||
xml1: str,
|
||||
xml2: str,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertXMLNotEqual(
|
||||
xml1: str,
|
||||
xml2: str,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
# Removed in Django 5.1: use assertQuerySetEqual.
|
||||
def assertQuerysetEqual(
|
||||
qs,
|
||||
values,
|
||||
transform=...,
|
||||
ordered: bool = ...,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertQuerySetEqual(
|
||||
qs,
|
||||
values,
|
||||
transform=...,
|
||||
ordered: bool = ...,
|
||||
msg: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def assertNumQueries(
|
||||
num: int,
|
||||
func=...,
|
||||
*args,
|
||||
using: str = ...,
|
||||
**kwargs,
|
||||
): ...
|
||||
|
||||
# Added in Django 5.0.
|
||||
def assertMessages(
|
||||
response: HttpResponseBase,
|
||||
expected_messages: Sequence[Message],
|
||||
*args,
|
||||
ordered: bool = ...,
|
||||
) -> None: ...
|
||||
|
||||
# Fallback in case Django adds new asserts.
|
||||
def __getattr__(name: str) -> Callable[..., Any]: ...
|
||||
@@ -0,0 +1,17 @@
|
||||
# Note that all functions here assume django is available. So ensure
|
||||
# this is the case before you call them.
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def is_django_unittest(request_or_item: pytest.FixtureRequest | pytest.Item) -> bool:
|
||||
"""Returns whether the request or item is a Django test case."""
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
cls = getattr(request_or_item, "cls", None)
|
||||
|
||||
if cls is None:
|
||||
return False
|
||||
|
||||
return issubclass(cls, SimpleTestCase)
|
||||
691
.venv/lib/python3.12/site-packages/pytest_django/fixtures.py
Normal file
691
.venv/lib/python3.12/site-packages/pytest_django/fixtures.py
Normal file
@@ -0,0 +1,691 @@
|
||||
"""All pytest-django fixtures"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Generator,
|
||||
Iterable,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Protocol,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import pytest
|
||||
|
||||
from . import live_server_helper
|
||||
from .django_compat import is_django_unittest
|
||||
from .lazy_django import skip_if_no_django
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import django
|
||||
import django.test
|
||||
|
||||
from . import DjangoDbBlocker
|
||||
|
||||
|
||||
_DjangoDbDatabases = Optional[Union[Literal["__all__"], Iterable[str]]]
|
||||
_DjangoDbAvailableApps = Optional[List[str]]
|
||||
# transaction, reset_sequences, databases, serialized_rollback, available_apps
|
||||
_DjangoDb = Tuple[bool, bool, _DjangoDbDatabases, bool, _DjangoDbAvailableApps]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"django_db_setup",
|
||||
"db",
|
||||
"transactional_db",
|
||||
"django_db_reset_sequences",
|
||||
"django_db_serialized_rollback",
|
||||
"admin_user",
|
||||
"django_user_model",
|
||||
"django_username_field",
|
||||
"client",
|
||||
"async_client",
|
||||
"admin_client",
|
||||
"rf",
|
||||
"async_rf",
|
||||
"settings",
|
||||
"live_server",
|
||||
"_live_server_helper",
|
||||
"django_assert_num_queries",
|
||||
"django_assert_max_num_queries",
|
||||
"django_capture_on_commit_callbacks",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_modify_db_settings_tox_suffix() -> None:
|
||||
skip_if_no_django()
|
||||
|
||||
tox_environment = os.getenv("TOX_PARALLEL_ENV")
|
||||
if tox_environment:
|
||||
# Put a suffix like _py27-django21 on tox workers
|
||||
_set_suffix_to_test_databases(suffix=tox_environment)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_modify_db_settings_xdist_suffix(request: pytest.FixtureRequest) -> None:
|
||||
skip_if_no_django()
|
||||
|
||||
xdist_suffix = getattr(request.config, "workerinput", {}).get("workerid")
|
||||
if xdist_suffix:
|
||||
# Put a suffix like _gw0, _gw1 etc on xdist processes
|
||||
_set_suffix_to_test_databases(suffix=xdist_suffix)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_modify_db_settings_parallel_suffix(
|
||||
django_db_modify_db_settings_tox_suffix: None,
|
||||
django_db_modify_db_settings_xdist_suffix: None,
|
||||
) -> None:
|
||||
skip_if_no_django()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_modify_db_settings(
|
||||
django_db_modify_db_settings_parallel_suffix: None,
|
||||
) -> None:
|
||||
"""Modify db settings just before the databases are configured."""
|
||||
skip_if_no_django()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_use_migrations(request: pytest.FixtureRequest) -> bool:
|
||||
"""Return whether to use migrations to create the test databases."""
|
||||
return not request.config.getvalue("nomigrations")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_keepdb(request: pytest.FixtureRequest) -> bool:
|
||||
"""Return whether to re-use an existing database and to keep it after the test run."""
|
||||
reuse_db: bool = request.config.getvalue("reuse_db")
|
||||
return reuse_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_createdb(request: pytest.FixtureRequest) -> bool:
|
||||
"""Return whether the database is to be re-created before running any tests."""
|
||||
create_db: bool = request.config.getvalue("create_db")
|
||||
return create_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_setup(
|
||||
request: pytest.FixtureRequest,
|
||||
django_test_environment: None,
|
||||
django_db_blocker: DjangoDbBlocker,
|
||||
django_db_use_migrations: bool,
|
||||
django_db_keepdb: bool,
|
||||
django_db_createdb: bool,
|
||||
django_db_modify_db_settings: None,
|
||||
) -> Generator[None, None, None]:
|
||||
"""Top level fixture to ensure test databases are available"""
|
||||
from django.test.utils import setup_databases, teardown_databases
|
||||
|
||||
setup_databases_args = {}
|
||||
|
||||
if not django_db_use_migrations:
|
||||
_disable_migrations()
|
||||
|
||||
if django_db_keepdb and not django_db_createdb:
|
||||
setup_databases_args["keepdb"] = True
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
db_cfg = setup_databases(
|
||||
verbosity=request.config.option.verbose,
|
||||
interactive=False,
|
||||
**setup_databases_args,
|
||||
)
|
||||
|
||||
yield
|
||||
|
||||
if not django_db_keepdb:
|
||||
with django_db_blocker.unblock():
|
||||
try:
|
||||
teardown_databases(db_cfg, verbosity=request.config.option.verbose)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
request.node.warn(
|
||||
pytest.PytestWarning(f"Error when trying to teardown test databases: {exc!r}")
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def _django_db_helper(
|
||||
request: pytest.FixtureRequest,
|
||||
django_db_setup: None,
|
||||
django_db_blocker: DjangoDbBlocker,
|
||||
) -> Generator[None, None, None]:
|
||||
from django import VERSION
|
||||
|
||||
if is_django_unittest(request):
|
||||
yield
|
||||
return
|
||||
|
||||
marker = request.node.get_closest_marker("django_db")
|
||||
if marker:
|
||||
(
|
||||
transactional,
|
||||
reset_sequences,
|
||||
databases,
|
||||
serialized_rollback,
|
||||
available_apps,
|
||||
) = validate_django_db(marker)
|
||||
else:
|
||||
(
|
||||
transactional,
|
||||
reset_sequences,
|
||||
databases,
|
||||
serialized_rollback,
|
||||
available_apps,
|
||||
) = False, False, None, False, None
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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()
|
||||
if VERSION < (4, 1):
|
||||
django.db.transaction.Atomic._ensure_durability = False
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
if VERSION < (4, 1):
|
||||
django.db.transaction.Atomic._ensure_durability = True
|
||||
super(django.test.TestCase, cls).tearDownClass()
|
||||
|
||||
PytestDjangoTestCase.setUpClass()
|
||||
|
||||
test_case = PytestDjangoTestCase(methodName="__init__")
|
||||
test_case._pre_setup()
|
||||
|
||||
yield
|
||||
|
||||
test_case._post_teardown()
|
||||
|
||||
PytestDjangoTestCase.tearDownClass()
|
||||
|
||||
if VERSION >= (4, 0):
|
||||
PytestDjangoTestCase.doClassCleanups()
|
||||
|
||||
django_db_blocker.restore()
|
||||
|
||||
|
||||
def validate_django_db(marker: pytest.Mark) -> _DjangoDb:
|
||||
"""Validate the django_db marker.
|
||||
|
||||
It checks the signature and creates the ``transaction``,
|
||||
``reset_sequences``, ``databases``, ``serialized_rollback`` and
|
||||
``available_apps`` attributes on the marker which will have the correct
|
||||
values.
|
||||
|
||||
Sequence reset, serialized_rollback, and available_apps are only allowed
|
||||
when combined with transaction.
|
||||
"""
|
||||
|
||||
def apifun(
|
||||
transaction: bool = False,
|
||||
reset_sequences: bool = False,
|
||||
databases: _DjangoDbDatabases = None,
|
||||
serialized_rollback: bool = False,
|
||||
available_apps: _DjangoDbAvailableApps = None,
|
||||
) -> _DjangoDb:
|
||||
return transaction, reset_sequences, databases, serialized_rollback, available_apps
|
||||
|
||||
return apifun(*marker.args, **marker.kwargs)
|
||||
|
||||
|
||||
def _disable_migrations() -> None:
|
||||
from django.conf import settings
|
||||
from django.core.management.commands import migrate
|
||||
|
||||
class DisableMigrations:
|
||||
def __contains__(self, item: str) -> bool:
|
||||
return True
|
||||
|
||||
def __getitem__(self, item: str) -> None:
|
||||
return None
|
||||
|
||||
settings.MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
class MigrateSilentCommand(migrate.Command):
|
||||
def handle(self, *args, **kwargs):
|
||||
kwargs["verbosity"] = 0
|
||||
return super().handle(*args, **kwargs)
|
||||
|
||||
migrate.Command = MigrateSilentCommand
|
||||
|
||||
|
||||
def _set_suffix_to_test_databases(suffix: str) -> None:
|
||||
from django.conf import settings
|
||||
|
||||
for db_settings in settings.DATABASES.values():
|
||||
test_name = db_settings.get("TEST", {}).get("NAME")
|
||||
|
||||
if not test_name:
|
||||
if db_settings["ENGINE"] == "django.db.backends.sqlite3":
|
||||
continue
|
||||
test_name = f"test_{db_settings['NAME']}"
|
||||
|
||||
if test_name == ":memory:":
|
||||
continue
|
||||
|
||||
db_settings.setdefault("TEST", {})
|
||||
db_settings["TEST"]["NAME"] = f"{test_name}_{suffix}"
|
||||
|
||||
|
||||
# ############### User visible fixtures ################
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db(_django_db_helper: None) -> None:
|
||||
"""Require a django test database.
|
||||
|
||||
This database will be setup with the default fixtures and will have
|
||||
the transaction management disabled. At the end of the test the outer
|
||||
transaction that wraps the test itself will be rolled back to undo any
|
||||
changes to the database (in case the backend supports transactions).
|
||||
This is more limited than the ``transactional_db`` fixture but
|
||||
faster.
|
||||
|
||||
If both ``db`` and ``transactional_db`` are requested,
|
||||
``transactional_db`` takes precedence.
|
||||
"""
|
||||
# The `_django_db_helper` fixture checks if `db` is requested.
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def transactional_db(_django_db_helper: None) -> None:
|
||||
"""Require a django test database with transaction support.
|
||||
|
||||
This will re-initialise the django database for each test and is
|
||||
thus slower than the normal ``db`` fixture.
|
||||
|
||||
If you want to use the database with transactions you must request
|
||||
this resource.
|
||||
|
||||
If both ``db`` and ``transactional_db`` are requested,
|
||||
``transactional_db`` takes precedence.
|
||||
"""
|
||||
# The `_django_db_helper` fixture checks if `transactional_db` is requested.
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_db_reset_sequences(
|
||||
_django_db_helper: None,
|
||||
transactional_db: None,
|
||||
) -> None:
|
||||
"""Require a transactional test database with sequence reset support.
|
||||
|
||||
This requests the ``transactional_db`` fixture, and additionally
|
||||
enforces a reset of all auto increment sequences. If the enquiring
|
||||
test relies on such values (e.g. ids as primary keys), you should
|
||||
request this resource to ensure they are consistent across tests.
|
||||
"""
|
||||
# The `_django_db_helper` fixture checks if `django_db_reset_sequences`
|
||||
# is requested.
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_db_serialized_rollback(
|
||||
_django_db_helper: None,
|
||||
db: None,
|
||||
) -> None:
|
||||
"""Require a test database with serialized rollbacks.
|
||||
|
||||
This requests the ``db`` fixture, and additionally performs rollback
|
||||
emulation - serializes the database contents during setup and restores
|
||||
it during teardown.
|
||||
|
||||
This fixture may be useful for transactional tests, so is usually combined
|
||||
with ``transactional_db``, but can also be useful on databases which do not
|
||||
support transactions.
|
||||
|
||||
Note that this will slow down that test suite by approximately 3x.
|
||||
"""
|
||||
# The `_django_db_helper` fixture checks if `django_db_serialized_rollback`
|
||||
# is requested.
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client() -> django.test.Client:
|
||||
"""A Django test client instance."""
|
||||
skip_if_no_django()
|
||||
|
||||
from django.test import Client
|
||||
|
||||
return Client()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def async_client() -> django.test.AsyncClient:
|
||||
"""A Django test async client instance."""
|
||||
skip_if_no_django()
|
||||
|
||||
from django.test import AsyncClient
|
||||
|
||||
return AsyncClient()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_user_model(db: None):
|
||||
"""The class of Django's user model."""
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
return get_user_model()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_username_field(django_user_model) -> str:
|
||||
"""The fieldname for the username used with Django's user model."""
|
||||
field: str = django_user_model.USERNAME_FIELD
|
||||
return field
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def admin_user(
|
||||
db: None,
|
||||
django_user_model,
|
||||
django_username_field: str,
|
||||
):
|
||||
"""A Django admin user.
|
||||
|
||||
This uses an existing user with username "admin", or creates a new one with
|
||||
password "password".
|
||||
"""
|
||||
UserModel = django_user_model
|
||||
username_field = django_username_field
|
||||
username = "admin@example.com" if username_field == "email" else "admin"
|
||||
|
||||
try:
|
||||
# The default behavior of `get_by_natural_key()` is to look up by `username_field`.
|
||||
# However the user model is free to override it with any sort of custom behavior.
|
||||
# The Django authentication backend already assumes the lookup is by username,
|
||||
# so we can assume so as well.
|
||||
user = UserModel._default_manager.get_by_natural_key(username)
|
||||
except UserModel.DoesNotExist:
|
||||
user_data = {}
|
||||
if "email" in UserModel.REQUIRED_FIELDS:
|
||||
user_data["email"] = "admin@example.com"
|
||||
user_data["password"] = "password"
|
||||
user_data[username_field] = username
|
||||
user = UserModel._default_manager.create_superuser(**user_data)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def admin_client(
|
||||
db: None,
|
||||
admin_user,
|
||||
) -> django.test.Client:
|
||||
"""A Django test client logged in as an admin user."""
|
||||
from django.test import Client
|
||||
|
||||
client = Client()
|
||||
client.force_login(admin_user)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def rf() -> django.test.RequestFactory:
|
||||
"""RequestFactory instance"""
|
||||
skip_if_no_django()
|
||||
|
||||
from django.test import RequestFactory
|
||||
|
||||
return RequestFactory()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def async_rf() -> django.test.AsyncRequestFactory:
|
||||
"""AsyncRequestFactory instance"""
|
||||
skip_if_no_django()
|
||||
|
||||
from django.test import AsyncRequestFactory
|
||||
|
||||
return AsyncRequestFactory()
|
||||
|
||||
|
||||
class SettingsWrapper:
|
||||
def __init__(self) -> None:
|
||||
self._to_restore: list[django.test.override_settings]
|
||||
object.__setattr__(self, "_to_restore", [])
|
||||
|
||||
def __delattr__(self, attr: str) -> None:
|
||||
from django.test import override_settings
|
||||
|
||||
override = override_settings()
|
||||
override.enable()
|
||||
from django.conf import settings
|
||||
|
||||
delattr(settings, attr)
|
||||
|
||||
self._to_restore.append(override)
|
||||
|
||||
def __setattr__(self, attr: str, value) -> None:
|
||||
from django.test import override_settings
|
||||
|
||||
override = override_settings(**{attr: value})
|
||||
override.enable()
|
||||
self._to_restore.append(override)
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
from django.conf import settings
|
||||
|
||||
return getattr(settings, attr)
|
||||
|
||||
def finalize(self) -> None:
|
||||
for override in reversed(self._to_restore):
|
||||
override.disable()
|
||||
|
||||
del self._to_restore[:]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def settings():
|
||||
"""A Django settings object which restores changes after the testrun"""
|
||||
skip_if_no_django()
|
||||
|
||||
wrapper = SettingsWrapper()
|
||||
yield wrapper
|
||||
wrapper.finalize()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def live_server(request: pytest.FixtureRequest):
|
||||
"""Run a live Django server in the background during tests
|
||||
|
||||
The address the server is started from is taken from the
|
||||
--liveserver command line option or if this is not provided from
|
||||
the DJANGO_LIVE_TEST_SERVER_ADDRESS environment variable. If
|
||||
neither is provided ``localhost`` is used. See the Django
|
||||
documentation for its full syntax.
|
||||
|
||||
NOTE: If the live server needs database access to handle a request
|
||||
your test will have to request database access. Furthermore
|
||||
when the tests want to see data added by the live-server (or
|
||||
the other way around) transactional database access will be
|
||||
needed as data inside a transaction is not shared between
|
||||
the live server and test code.
|
||||
|
||||
Static assets will be automatically served when
|
||||
``django.contrib.staticfiles`` is available in INSTALLED_APPS.
|
||||
"""
|
||||
skip_if_no_django()
|
||||
|
||||
addr = (
|
||||
request.config.getvalue("liveserver")
|
||||
or os.getenv("DJANGO_LIVE_TEST_SERVER_ADDRESS")
|
||||
or "localhost"
|
||||
)
|
||||
|
||||
server = live_server_helper.LiveServer(addr)
|
||||
yield server
|
||||
server.stop()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _live_server_helper(request: pytest.FixtureRequest) -> Generator[None, None, None]:
|
||||
"""Helper to make live_server work, internal to pytest-django.
|
||||
|
||||
This helper will dynamically request the transactional_db fixture
|
||||
for a test which uses the live_server fixture. This allows the
|
||||
server and test to access the database without having to mark
|
||||
this explicitly which is handy since it is usually required and
|
||||
matches the Django behaviour.
|
||||
|
||||
The separate helper is required since live_server can not request
|
||||
transactional_db directly since it is session scoped instead of
|
||||
function-scoped.
|
||||
|
||||
It will also override settings only for the duration of the test.
|
||||
"""
|
||||
if "live_server" not in request.fixturenames:
|
||||
yield
|
||||
return
|
||||
|
||||
request.getfixturevalue("transactional_db")
|
||||
|
||||
live_server = request.getfixturevalue("live_server")
|
||||
live_server._live_server_modified_settings.enable()
|
||||
yield
|
||||
live_server._live_server_modified_settings.disable()
|
||||
|
||||
|
||||
class DjangoAssertNumQueries(Protocol):
|
||||
"""The type of the `django_assert_num_queries` and
|
||||
`django_assert_max_num_queries` fixtures."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
num: int,
|
||||
connection: Any | None = ...,
|
||||
info: str | None = ...,
|
||||
) -> django.test.utils.CaptureQueriesContext:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _assert_num_queries(
|
||||
config: pytest.Config,
|
||||
num: int,
|
||||
exact: bool = True,
|
||||
connection: Any | None = None,
|
||||
info: str | None = None,
|
||||
) -> Generator[django.test.utils.CaptureQueriesContext, None, None]:
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
if connection is None:
|
||||
from django.db import connection as conn
|
||||
else:
|
||||
conn = connection
|
||||
|
||||
verbose = config.getoption("verbose") > 0
|
||||
with CaptureQueriesContext(conn) as context:
|
||||
yield context
|
||||
num_performed = len(context)
|
||||
if exact:
|
||||
failed = num != num_performed
|
||||
else:
|
||||
failed = num_performed > num
|
||||
if failed:
|
||||
msg = f"Expected to perform {num} queries "
|
||||
if not exact:
|
||||
msg += "or less "
|
||||
verb = "was" if num_performed == 1 else "were"
|
||||
msg += f"but {num_performed} {verb} done"
|
||||
if info:
|
||||
msg += f"\n{info}"
|
||||
if verbose:
|
||||
sqls = (q["sql"] for q in context.captured_queries)
|
||||
msg += "\n\nQueries:\n========\n\n" + "\n\n".join(sqls)
|
||||
else:
|
||||
msg += " (add -v option to show queries)"
|
||||
pytest.fail(msg)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_assert_num_queries(pytestconfig: pytest.Config) -> DjangoAssertNumQueries:
|
||||
"""Allows to check for an expected number of DB queries."""
|
||||
return partial(_assert_num_queries, pytestconfig)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_assert_max_num_queries(pytestconfig: pytest.Config) -> DjangoAssertNumQueries:
|
||||
"""Allows to check for an expected maximum number of DB queries."""
|
||||
return partial(_assert_num_queries, pytestconfig, exact=False)
|
||||
|
||||
|
||||
class DjangoCaptureOnCommitCallbacks(Protocol):
|
||||
"""The type of the `django_capture_on_commit_callbacks` fixture."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
using: str = ...,
|
||||
execute: bool = ...,
|
||||
) -> ContextManager[list[Callable[[], Any]]]:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_capture_on_commit_callbacks() -> DjangoCaptureOnCommitCallbacks:
|
||||
"""Captures transaction.on_commit() callbacks for the given database connection."""
|
||||
from django.test import TestCase
|
||||
|
||||
return TestCase.captureOnCommitCallbacks # type: ignore[no-any-return]
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Helpers to load Django lazily when Django settings can't be configured.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def skip_if_no_django() -> None:
|
||||
"""Raises a skip exception when no Django settings are available"""
|
||||
if not django_settings_is_configured():
|
||||
pytest.skip("no Django settings")
|
||||
|
||||
|
||||
def django_settings_is_configured() -> bool:
|
||||
"""Return whether the Django settings module has been configured.
|
||||
|
||||
This uses either the DJANGO_SETTINGS_MODULE environment variable, or the
|
||||
configured flag in the Django settings object if django.conf has already
|
||||
been imported.
|
||||
"""
|
||||
ret = bool(os***REMOVED***iron.get("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
if not ret and "django.conf" in sys.modules:
|
||||
django_conf: Any = sys.modules["django.conf"]
|
||||
ret = django_conf.settings.configured
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_django_version() -> tuple[int, int, int, str, int]:
|
||||
import django
|
||||
|
||||
version: tuple[int, int, int, str, int] = django.VERSION
|
||||
return version
|
||||
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class LiveServer:
|
||||
"""The liveserver fixture
|
||||
|
||||
This is the object that the ``live_server`` fixture returns.
|
||||
The ``live_server`` fixture handles creation and stopping.
|
||||
"""
|
||||
|
||||
def __init__(self, addr: str, *, start: bool = True) -> None:
|
||||
from django.db import connections
|
||||
from django.test.testcases import LiveServerThread
|
||||
from django.test.utils import modify_settings
|
||||
|
||||
liveserver_kwargs: dict[str, Any] = {}
|
||||
|
||||
connections_override = {}
|
||||
for conn in connections.all():
|
||||
# If using in-memory sqlite databases, pass the connections to
|
||||
# the server thread.
|
||||
if conn.vendor == "sqlite" and conn.is_in_memory_db():
|
||||
connections_override[conn.alias] = conn
|
||||
|
||||
liveserver_kwargs["connections_override"] = connections_override
|
||||
from django.conf import settings
|
||||
|
||||
if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
|
||||
liveserver_kwargs["static_handler"] = StaticFilesHandler
|
||||
else:
|
||||
from django.test.testcases import _StaticFilesHandler
|
||||
|
||||
liveserver_kwargs["static_handler"] = _StaticFilesHandler
|
||||
|
||||
try:
|
||||
host, port = addr.split(":")
|
||||
except ValueError:
|
||||
host = addr
|
||||
else:
|
||||
liveserver_kwargs["port"] = int(port)
|
||||
self.thread = LiveServerThread(host, **liveserver_kwargs)
|
||||
|
||||
self._live_server_modified_settings = modify_settings(
|
||||
ALLOWED_HOSTS={"append": host},
|
||||
)
|
||||
# `_live_server_modified_settings` is enabled and disabled by
|
||||
# `_live_server_helper`.
|
||||
|
||||
self.thread.daemon = True
|
||||
|
||||
if start:
|
||||
self.start()
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the server"""
|
||||
for conn in self.thread.connections_override.values():
|
||||
# Explicitly enable thread-shareability for this connection.
|
||||
conn.inc_thread_sharing()
|
||||
|
||||
self.thread.start()
|
||||
self.thread.is_ready.wait()
|
||||
|
||||
if self.thread.error:
|
||||
error = self.thread.error
|
||||
self.stop()
|
||||
raise error
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the server"""
|
||||
# Terminate the live server's thread.
|
||||
self.thread.terminate()
|
||||
# Restore shared connections' non-shareability.
|
||||
for conn in self.thread.connections_override.values():
|
||||
conn.dec_thread_sharing()
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return f"http://{self.thread.host}:{self.thread.port}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.url
|
||||
|
||||
def __add__(self, other) -> str:
|
||||
return f"{self}{other}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<LiveServer listening at {self.url}>"
|
||||
849
.venv/lib/python3.12/site-packages/pytest_django/plugin.py
Normal file
849
.venv/lib/python3.12/site-packages/pytest_django/plugin.py
Normal file
@@ -0,0 +1,849 @@
|
||||
"""A pytest plugin which helps testing Django applications
|
||||
|
||||
This plugin handles creating and destroying the test environment and
|
||||
test database and provides some useful text fixtures.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import inspect
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import types
|
||||
from functools import reduce
|
||||
from typing import TYPE_CHECKING, ContextManager, Generator, List, NoReturn
|
||||
|
||||
import pytest
|
||||
|
||||
from .django_compat import is_django_unittest
|
||||
from .fixtures import (
|
||||
_django_db_helper, # noqa: F401
|
||||
_live_server_helper, # noqa: F401
|
||||
admin_client, # noqa: F401
|
||||
admin_user, # noqa: F401
|
||||
async_client, # noqa: F401
|
||||
async_rf, # noqa: F401
|
||||
client, # noqa: F401
|
||||
db, # noqa: F401
|
||||
django_assert_max_num_queries, # noqa: F401
|
||||
django_assert_num_queries, # noqa: F401
|
||||
django_capture_on_commit_callbacks, # noqa: F401
|
||||
django_db_createdb, # noqa: F401
|
||||
django_db_keepdb, # noqa: F401
|
||||
django_db_modify_db_settings, # noqa: F401
|
||||
django_db_modify_db_settings_parallel_suffix, # noqa: F401
|
||||
django_db_modify_db_settings_tox_suffix, # noqa: F401
|
||||
django_db_modify_db_settings_xdist_suffix, # noqa: F401
|
||||
django_db_reset_sequences, # noqa: F401
|
||||
django_db_serialized_rollback, # noqa: F401
|
||||
django_db_setup, # noqa: F401
|
||||
django_db_use_migrations, # noqa: F401
|
||||
django_user_model, # noqa: F401
|
||||
django_username_field, # noqa: F401
|
||||
live_server, # noqa: F401
|
||||
rf, # noqa: F401
|
||||
settings, # noqa: F401
|
||||
transactional_db, # noqa: F401
|
||||
validate_django_db,
|
||||
)
|
||||
from .lazy_django import django_settings_is_configured, skip_if_no_django
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import django
|
||||
|
||||
|
||||
SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE"
|
||||
CONFIGURATION_ENV = "DJANGO_CONFIGURATION"
|
||||
INVALID_TEMPLATE_VARS_ENV = "FAIL_INVALID_TEMPLATE_VARS"
|
||||
|
||||
|
||||
# ############### pytest hooks ################
|
||||
|
||||
|
||||
@pytest.hookimpl()
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
group = parser.getgroup("django")
|
||||
group.addoption(
|
||||
"--reuse-db",
|
||||
action="store_true",
|
||||
dest="reuse_db",
|
||||
default=False,
|
||||
help="Re-use the testing database if it already exists, "
|
||||
"and do not remove it when the test finishes.",
|
||||
)
|
||||
group.addoption(
|
||||
"--create-db",
|
||||
action="store_true",
|
||||
dest="create_db",
|
||||
default=False,
|
||||
help="Re-create the database, even if it exists. This "
|
||||
"option can be used to override --reuse-db.",
|
||||
)
|
||||
group.addoption(
|
||||
"--ds",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="ds",
|
||||
default=None,
|
||||
help="Set DJANGO_SETTINGS_MODULE.",
|
||||
)
|
||||
group.addoption(
|
||||
"--dc",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="dc",
|
||||
default=None,
|
||||
help="Set DJANGO_CONFIGURATION.",
|
||||
)
|
||||
group.addoption(
|
||||
"--nomigrations",
|
||||
"--no-migrations",
|
||||
action="store_true",
|
||||
dest="nomigrations",
|
||||
default=False,
|
||||
help="Disable Django migrations on test setup",
|
||||
)
|
||||
group.addoption(
|
||||
"--migrations",
|
||||
action="store_false",
|
||||
dest="nomigrations",
|
||||
default=False,
|
||||
help="Enable Django migrations on test setup",
|
||||
)
|
||||
parser.addini(
|
||||
CONFIGURATION_ENV,
|
||||
"django-configurations class to use by pytest-django.",
|
||||
)
|
||||
group.addoption(
|
||||
"--liveserver",
|
||||
default=None,
|
||||
help="Address and port for the live_server fixture.",
|
||||
)
|
||||
parser.addini(
|
||||
SETTINGS_MODULE_ENV,
|
||||
"Django settings module to use by pytest-django.",
|
||||
)
|
||||
|
||||
parser.addini(
|
||||
"django_find_project",
|
||||
"Automatically find and add a Django project to the " "Python path.",
|
||||
type="bool",
|
||||
default=True,
|
||||
)
|
||||
parser.addini(
|
||||
"django_debug_mode",
|
||||
"How to set the Django DEBUG setting (default `False`). " "Use `keep` to not override.",
|
||||
default="False",
|
||||
)
|
||||
group.addoption(
|
||||
"--fail-on-template-vars",
|
||||
action="store_true",
|
||||
dest="itv",
|
||||
default=False,
|
||||
help="Fail for invalid variables in templates.",
|
||||
)
|
||||
parser.addini(
|
||||
INVALID_TEMPLATE_VARS_ENV,
|
||||
"Fail for invalid variables in templates.",
|
||||
type="bool",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
PROJECT_FOUND = (
|
||||
"pytest-django found a Django project in %s "
|
||||
"(it contains manage.py) and added it to the Python path.\n"
|
||||
'If this is wrong, add "django_find_project = false" to '
|
||||
"pytest.ini and explicitly manage your Python path."
|
||||
)
|
||||
|
||||
PROJECT_NOT_FOUND = (
|
||||
"pytest-django could not find a Django project "
|
||||
"(no manage.py file could be found). You must "
|
||||
"explicitly add your Django project to the Python path "
|
||||
"to have it picked up."
|
||||
)
|
||||
|
||||
PROJECT_SCAN_DISABLED = (
|
||||
"pytest-django did not search for Django "
|
||||
"projects since it is disabled in the configuration "
|
||||
'("django_find_project = false")'
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_import_error(extra_message: str) -> Generator[None, None, None]:
|
||||
try:
|
||||
yield
|
||||
except ImportError as e:
|
||||
django_msg = (e.args[0] + "\n\n") if e.args else ""
|
||||
msg = django_msg + extra_message
|
||||
raise ImportError(msg) from None
|
||||
|
||||
|
||||
def _add_django_project_to_path(args) -> str:
|
||||
def is_django_project(path: pathlib.Path) -> bool:
|
||||
try:
|
||||
return path.is_dir() and (path / "manage.py").exists()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def arg_to_path(arg: str) -> pathlib.Path:
|
||||
# Test classes or functions can be appended to paths separated by ::
|
||||
arg = arg.split("::", 1)[0]
|
||||
return pathlib.Path(arg)
|
||||
|
||||
def find_django_path(args) -> pathlib.Path | None:
|
||||
str_args = (str(arg) for arg in args)
|
||||
path_args = [arg_to_path(x) for x in str_args if not x.startswith("-")]
|
||||
|
||||
cwd = pathlib.Path.cwd()
|
||||
if not path_args:
|
||||
path_args.append(cwd)
|
||||
elif cwd not in path_args:
|
||||
path_args.append(cwd)
|
||||
|
||||
for arg in path_args:
|
||||
if is_django_project(arg):
|
||||
return arg
|
||||
for parent in arg.parents:
|
||||
if is_django_project(parent):
|
||||
return parent
|
||||
return None
|
||||
|
||||
project_dir = find_django_path(args)
|
||||
if project_dir:
|
||||
sys.path.insert(0, str(project_dir.absolute()))
|
||||
return PROJECT_FOUND % project_dir
|
||||
return PROJECT_NOT_FOUND
|
||||
|
||||
|
||||
def _setup_django(config: pytest.Config) -> None:
|
||||
if "django" not in sys.modules:
|
||||
return
|
||||
|
||||
import django.conf
|
||||
|
||||
# Avoid force-loading Django when settings are not properly configured.
|
||||
if not django.conf.settings.configured:
|
||||
return
|
||||
|
||||
import django.apps
|
||||
|
||||
if not django.apps.apps.ready:
|
||||
django.setup()
|
||||
|
||||
blocking_manager = config.stash[blocking_manager_key]
|
||||
blocking_manager.block()
|
||||
|
||||
|
||||
def _get_boolean_value(
|
||||
x: None | (bool | str),
|
||||
name: str,
|
||||
default: bool | None = None,
|
||||
) -> bool:
|
||||
if x is None:
|
||||
return bool(default)
|
||||
if isinstance(x, bool):
|
||||
return x
|
||||
possible_values = {"true": True, "false": False, "1": True, "0": False}
|
||||
try:
|
||||
return possible_values[x.lower()]
|
||||
except KeyError:
|
||||
possible = ", ".join(possible_values)
|
||||
raise ValueError(
|
||||
f"{x} is not a valid value for {name}. It must be one of {possible}."
|
||||
) from None
|
||||
|
||||
|
||||
report_header_key = pytest.StashKey[List[str]]()
|
||||
|
||||
|
||||
@pytest.hookimpl()
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: pytest.Config,
|
||||
parser: pytest.Parser,
|
||||
args: list[str],
|
||||
) -> None:
|
||||
# Register the marks
|
||||
early_config.addinivalue_line(
|
||||
"markers",
|
||||
"django_db(transaction=False, reset_sequences=False, databases=None, "
|
||||
"serialized_rollback=False): "
|
||||
"Mark the test as using the Django test database. "
|
||||
"The *transaction* argument allows you to use real transactions "
|
||||
"in the test like Django's TransactionTestCase. "
|
||||
"The *reset_sequences* argument resets database sequences before "
|
||||
"the test. "
|
||||
"The *databases* argument sets which database aliases the test "
|
||||
"uses (by default, only 'default'). Use '__all__' for all databases. "
|
||||
"The *serialized_rollback* argument enables rollback emulation for "
|
||||
"the test.",
|
||||
)
|
||||
early_config.addinivalue_line(
|
||||
"markers",
|
||||
"urls(modstr): Use a different URLconf for this test, similar to "
|
||||
"the `urls` attribute of Django's `TestCase` objects. *modstr* is "
|
||||
"a string specifying the module of a URL config, e.g. "
|
||||
'"my_app.test_urls".',
|
||||
)
|
||||
early_config.addinivalue_line(
|
||||
"markers",
|
||||
"ignore_template_errors(): ignore errors from invalid template "
|
||||
"variables (if --fail-on-template-vars is used).",
|
||||
)
|
||||
|
||||
options = parser.parse_known_args(args)
|
||||
|
||||
if options.version or options.help:
|
||||
return
|
||||
|
||||
django_find_project = _get_boolean_value(
|
||||
early_config.getini("django_find_project"), "django_find_project"
|
||||
)
|
||||
|
||||
if django_find_project:
|
||||
_django_project_scan_outcome = _add_django_project_to_path(args)
|
||||
else:
|
||||
_django_project_scan_outcome = PROJECT_SCAN_DISABLED
|
||||
|
||||
if (
|
||||
options.itv
|
||||
or _get_boolean_value(os***REMOVED***iron.get(INVALID_TEMPLATE_VARS_ENV), INVALID_TEMPLATE_VARS_ENV)
|
||||
or early_config.getini(INVALID_TEMPLATE_VARS_ENV)
|
||||
):
|
||||
os***REMOVED***iron[INVALID_TEMPLATE_VARS_ENV] = "true"
|
||||
|
||||
def _get_option_with_source(
|
||||
option: str | None,
|
||||
envname: str,
|
||||
) -> tuple[str, str] | tuple[None, None]:
|
||||
if option:
|
||||
return option, "option"
|
||||
if envname in os***REMOVED***iron:
|
||||
return os***REMOVED***iron[envname], "env"
|
||||
cfgval = early_config.getini(envname)
|
||||
if cfgval:
|
||||
return cfgval, "ini"
|
||||
return None, None
|
||||
|
||||
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***REMOVED***iron[SETTINGS_MODULE_ENV] = ds
|
||||
|
||||
if dc:
|
||||
report_header.append(f"configuration: {dc} (from {dc_source})")
|
||||
os***REMOVED***iron[CONFIGURATION_ENV] = dc
|
||||
|
||||
# Install the django-configurations importer
|
||||
import configurations.importer
|
||||
|
||||
configurations.importer.install()
|
||||
|
||||
# Forcefully load Django settings, throws ImportError or
|
||||
# ImproperlyConfigured if settings cannot be loaded.
|
||||
from django.conf import settings as dj_settings
|
||||
|
||||
with _handle_import_error(_django_project_scan_outcome):
|
||||
dj_settings.DATABASES # noqa: B018
|
||||
|
||||
early_config.stash[blocking_manager_key] = DjangoDbBlocker(_ispytest=True)
|
||||
|
||||
_setup_django(early_config)
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
if config.getoption("version", 0) > 0 or config.getoption("help", False):
|
||||
return
|
||||
|
||||
# Normally Django is set up in `pytest_load_initial_conftests`, but we also
|
||||
# allow users to not set DJANGO_SETTINGS_MODULE/`--ds` and instead
|
||||
# configure the Django settings in a `pytest_configure` hookimpl using e.g.
|
||||
# `settings.configure(...)`. In this case, the `_setup_django` call in
|
||||
# `pytest_load_initial_conftests` only partially initializes Django, and
|
||||
# it's fully initialized here.
|
||||
_setup_django(config)
|
||||
|
||||
|
||||
@pytest.hookimpl()
|
||||
def pytest_report_header(config: pytest.Config) -> list[str] | None:
|
||||
report_header = config.stash[report_header_key]
|
||||
|
||||
if "django" in sys.modules:
|
||||
import django
|
||||
|
||||
report_header.insert(0, f"version: {django.get_version()}")
|
||||
|
||||
if report_header:
|
||||
return ["django: " + ", ".join(report_header)]
|
||||
return None
|
||||
|
||||
|
||||
# Convert Django test tags on test classes to pytest marks.
|
||||
# Unlike the Django test runner, we only check tags on Django
|
||||
# test classes, to keep the plugin's effect contained.
|
||||
def pytest_collectstart(collector: pytest.Collector) -> None:
|
||||
if "django" not in sys.modules:
|
||||
return
|
||||
|
||||
if not isinstance(collector, pytest.Class):
|
||||
return
|
||||
|
||||
tags = getattr(collector.obj, "tags", ())
|
||||
if not tags:
|
||||
return
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
if not issubclass(collector.obj, SimpleTestCase):
|
||||
return
|
||||
|
||||
for tag in tags:
|
||||
collector.add_marker(tag)
|
||||
|
||||
|
||||
# Convert Django test tags on test methods to pytest marks.
|
||||
def pytest_itemcollected(item: pytest.Item) -> None:
|
||||
if "django" not in sys.modules:
|
||||
return
|
||||
|
||||
if not isinstance(item, pytest.Function):
|
||||
return
|
||||
|
||||
tags = getattr(item.obj, "tags", ())
|
||||
if not tags:
|
||||
return
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
if not issubclass(item.cls, SimpleTestCase):
|
||||
return
|
||||
|
||||
for tag in tags:
|
||||
item.add_marker(tag)
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||
# If Django is not configured we don't need to bother
|
||||
if not django_settings_is_configured():
|
||||
return
|
||||
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
|
||||
def get_order_number(test: pytest.Item) -> int:
|
||||
test_cls = getattr(test, "cls", None)
|
||||
if test_cls and issubclass(test_cls, TransactionTestCase):
|
||||
# Note, TestCase is a subclass of TransactionTestCase.
|
||||
uses_db = True
|
||||
transactional = not issubclass(test_cls, TestCase)
|
||||
else:
|
||||
marker_db = test.get_closest_marker("django_db")
|
||||
if marker_db:
|
||||
(
|
||||
transaction,
|
||||
reset_sequences,
|
||||
databases,
|
||||
serialized_rollback,
|
||||
available_apps,
|
||||
) = validate_django_db(marker_db)
|
||||
uses_db = True
|
||||
transactional = transaction or reset_sequences
|
||||
else:
|
||||
uses_db = False
|
||||
transactional = False
|
||||
fixtures = getattr(test, "fixturenames", [])
|
||||
transactional = transactional or "transactional_db" in fixtures
|
||||
uses_db = uses_db or "db" in fixtures
|
||||
|
||||
if transactional:
|
||||
return 1
|
||||
elif uses_db:
|
||||
return 0
|
||||
else:
|
||||
return 2
|
||||
|
||||
items.sort(key=get_order_number)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def django_test_environment(request: pytest.FixtureRequest) -> Generator[None, None, None]:
|
||||
"""Setup Django's test environment for the testing session.
|
||||
|
||||
XXX It is a little dodgy that this is an autouse fixture. Perhaps
|
||||
an email fixture should be requested in order to be able to
|
||||
use the Django email machinery just like you need to request a
|
||||
db fixture for access to the Django database, etc. But
|
||||
without duplicating a lot more of Django's test support code
|
||||
we need to follow this model.
|
||||
"""
|
||||
if django_settings_is_configured():
|
||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||
|
||||
debug_ini = request.config.getini("django_debug_mode")
|
||||
if debug_ini == "keep":
|
||||
debug = None
|
||||
else:
|
||||
debug = _get_boolean_value(debug_ini, "django_debug_mode", False)
|
||||
|
||||
setup_test_environment(debug=debug)
|
||||
yield
|
||||
teardown_test_environment()
|
||||
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_db_blocker(request: pytest.FixtureRequest) -> DjangoDbBlocker | None:
|
||||
"""Wrapper around Django's 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.
|
||||
|
||||
The object is a context manager and provides the methods
|
||||
.unblock()/.block() and .restore() to temporarily enable database access.
|
||||
|
||||
This is an advanced feature that is meant to be used to implement database
|
||||
fixtures.
|
||||
"""
|
||||
if not django_settings_is_configured():
|
||||
return None
|
||||
|
||||
blocking_manager = request.config.stash[blocking_manager_key]
|
||||
return blocking_manager
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _django_db_marker(request: pytest.FixtureRequest) -> None:
|
||||
"""Implement the django_db marker, internal to pytest-django."""
|
||||
marker = request.node.get_closest_marker("django_db")
|
||||
if marker:
|
||||
request.getfixturevalue("_django_db_helper")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="class")
|
||||
def _django_setup_unittest(
|
||||
request: pytest.FixtureRequest,
|
||||
django_db_blocker: DjangoDbBlocker,
|
||||
) -> Generator[None, None, None]:
|
||||
"""Setup a django unittest, internal to pytest-django."""
|
||||
if not django_settings_is_configured() or not is_django_unittest(request):
|
||||
yield
|
||||
return
|
||||
|
||||
# Fix/patch pytest.
|
||||
# Before pytest 5.4: https://github.com/pytest-dev/pytest/issues/5991
|
||||
# After pytest 5.4: https://github.com/pytest-dev/pytest-django/issues/824
|
||||
from _pytest.unittest import TestCaseFunction
|
||||
|
||||
original_runtest = TestCaseFunction.runtest
|
||||
|
||||
def non_debugging_runtest(self) -> None:
|
||||
self._testcase(result=self)
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
assert issubclass(request.cls, SimpleTestCase) # Guarded by 'is_django_unittest'
|
||||
try:
|
||||
TestCaseFunction.runtest = non_debugging_runtest # type: ignore[method-assign]
|
||||
|
||||
# Don't set up the DB if the unittest does not require DB.
|
||||
# The `databases` propery seems like the best indicator for that.
|
||||
if request.cls.databases:
|
||||
request.getfixturevalue("django_db_setup")
|
||||
db_unblock = django_db_blocker.unblock()
|
||||
else:
|
||||
db_unblock = contextlib.nullcontext()
|
||||
|
||||
with db_unblock:
|
||||
yield
|
||||
finally:
|
||||
TestCaseFunction.runtest = original_runtest # type: ignore[method-assign]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _dj_autoclear_mailbox() -> None:
|
||||
if not django_settings_is_configured():
|
||||
return
|
||||
|
||||
from django.core import mail
|
||||
|
||||
del mail.outbox[:]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mailoutbox(
|
||||
django_mail_patch_dns: None,
|
||||
_dj_autoclear_mailbox: None,
|
||||
) -> list[django.core.mail.EmailMessage] | None:
|
||||
"""A clean email outbox to which Django-generated emails are sent."""
|
||||
if not django_settings_is_configured():
|
||||
return None
|
||||
|
||||
from django.core import mail
|
||||
|
||||
return mail.outbox # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_mail_patch_dns(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
django_mail_dnsname: str,
|
||||
) -> None:
|
||||
"""Patch the server dns name used in email messages."""
|
||||
from django.core import mail
|
||||
|
||||
monkeypatch.setattr(mail.message, "DNS_NAME", django_mail_dnsname)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def django_mail_dnsname() -> str:
|
||||
"""Return server dns name for using in email messages."""
|
||||
return "fake-tests.example.com"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _django_set_urlconf(request: pytest.FixtureRequest) -> Generator[None, None, None]:
|
||||
"""Apply the @pytest.mark.urls marker, internal to pytest-django."""
|
||||
marker: pytest.Mark | None = request.node.get_closest_marker("urls")
|
||||
if marker:
|
||||
skip_if_no_django()
|
||||
import django.conf
|
||||
from django.urls import clear_url_caches, set_urlconf
|
||||
|
||||
urls = validate_urls(marker)
|
||||
original_urlconf = django.conf.settings.ROOT_URLCONF
|
||||
django.conf.settings.ROOT_URLCONF = urls
|
||||
clear_url_caches()
|
||||
set_urlconf(None)
|
||||
|
||||
yield
|
||||
|
||||
if marker:
|
||||
django.conf.settings.ROOT_URLCONF = original_urlconf
|
||||
# Copy the pattern from
|
||||
# https://github.[AWS-SECRET-REMOVED]signals.py#L152
|
||||
clear_url_caches()
|
||||
set_urlconf(None)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _fail_for_invalid_template_variable() -> Generator[None, None, None]:
|
||||
"""Fixture that fails for invalid variables in templates.
|
||||
|
||||
This fixture will fail each test that uses django template rendering
|
||||
should a template contain an invalid template variable.
|
||||
The fail message will include the name of the invalid variable and
|
||||
in most cases the template name.
|
||||
|
||||
It does not raise an exception, but fails, as the stack trace doesn't
|
||||
offer any helpful information to debug.
|
||||
This behavior can be switched off using the marker:
|
||||
``pytest.mark.ignore_template_errors``
|
||||
"""
|
||||
|
||||
class InvalidVarException:
|
||||
"""Custom handler for invalid strings in templates."""
|
||||
|
||||
def __init__(self, *, origin_value: str) -> None:
|
||||
self.fail = True
|
||||
self.origin_value = origin_value
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key == "%s"
|
||||
|
||||
@staticmethod
|
||||
def _get_origin() -> str | None:
|
||||
stack = inspect.stack()
|
||||
|
||||
# Try to use topmost `self.origin` first (Django 1.9+, and with
|
||||
# TEMPLATE_DEBUG)..
|
||||
for frame_info in stack[2:]:
|
||||
if frame_info.function == "render":
|
||||
origin: str | None
|
||||
try:
|
||||
origin = frame_info.frame.f_locals["self"].origin
|
||||
except (AttributeError, KeyError):
|
||||
origin = None
|
||||
if origin is not None:
|
||||
return origin
|
||||
|
||||
from django.template import Template
|
||||
|
||||
# finding the ``render`` needle in the stack
|
||||
frameinfo = reduce(
|
||||
lambda x, y: y if y.function == "render" and "base.py" in y.filename else x, stack
|
||||
)
|
||||
# ``django.template.base.Template``
|
||||
template = frameinfo.frame.f_locals["self"]
|
||||
if isinstance(template, Template):
|
||||
name: str = template.name
|
||||
return name
|
||||
return None
|
||||
|
||||
def __mod__(self, var: str) -> str:
|
||||
origin = self._get_origin()
|
||||
if origin:
|
||||
msg = f"Undefined template variable '{var}' in '{origin}'"
|
||||
else:
|
||||
msg = f"Undefined template variable '{var}'"
|
||||
if self.fail:
|
||||
pytest.fail(msg)
|
||||
else:
|
||||
return self.origin_value
|
||||
|
||||
with pytest.MonkeyPatch.context() as mp:
|
||||
if (
|
||||
os***REMOVED***iron.get(INVALID_TEMPLATE_VARS_ENV, "false") == "true"
|
||||
and django_settings_is_configured()
|
||||
):
|
||||
from django.conf import settings as dj_settings
|
||||
|
||||
if dj_settings.TEMPLATES:
|
||||
mp.setitem(
|
||||
dj_settings.TEMPLATES[0]["OPTIONS"],
|
||||
"string_if_invalid",
|
||||
InvalidVarException(
|
||||
origin_value=dj_settings.TEMPLATES[0]["OPTIONS"].get(
|
||||
"string_if_invalid", ""
|
||||
)
|
||||
),
|
||||
)
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _template_string_if_invalid_marker(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
request: pytest.FixtureRequest,
|
||||
) -> None:
|
||||
"""Apply the @pytest.mark.ignore_template_errors marker,
|
||||
internal to pytest-django."""
|
||||
marker = request.keywords.get("ignore_template_errors", None)
|
||||
if os***REMOVED***iron.get(INVALID_TEMPLATE_VARS_ENV, "false") == "true":
|
||||
if marker and django_settings_is_configured():
|
||||
from django.conf import settings as dj_settings
|
||||
|
||||
if dj_settings.TEMPLATES:
|
||||
monkeypatch.setattr(
|
||||
dj_settings.TEMPLATES[0]["OPTIONS"]["string_if_invalid"],
|
||||
"fail",
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _django_clear_site_cache() -> None:
|
||||
"""Clears ``django.contrib.sites.models.SITE_CACHE`` to avoid
|
||||
unexpected behavior with cached site objects.
|
||||
"""
|
||||
|
||||
if django_settings_is_configured():
|
||||
from django.conf import settings as dj_settings
|
||||
|
||||
if "django.contrib.sites" in dj_settings.INSTALLED_APPS:
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
Site.objects.clear_cache()
|
||||
|
||||
|
||||
# ############### Helper Functions ################
|
||||
|
||||
|
||||
class _DatabaseBlockerContextManager:
|
||||
def __init__(self, db_blocker: DjangoDbBlocker) -> None:
|
||||
self._db_blocker = db_blocker
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: types.TracebackType | None,
|
||||
) -> None:
|
||||
self._db_blocker.restore()
|
||||
|
||||
|
||||
class DjangoDbBlocker:
|
||||
"""Manager for django.db.backends.base.base.BaseDatabaseWrapper.
|
||||
|
||||
This is the object returned by django_db_blocker.
|
||||
"""
|
||||
|
||||
def __init__(self, *, _ispytest: bool = False) -> None:
|
||||
if not _ispytest: # pragma: no cover
|
||||
raise TypeError(
|
||||
"The DjangoDbBlocker constructor is private. "
|
||||
"use the django_db_blocker fixture instead."
|
||||
)
|
||||
|
||||
self._history = [] # type: ignore[var-annotated]
|
||||
self._real_ensure_connection = None
|
||||
|
||||
@property
|
||||
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.
|
||||
if self._real_ensure_connection is None:
|
||||
self._real_ensure_connection = BaseDatabaseWrapper.ensure_connection
|
||||
|
||||
return BaseDatabaseWrapper
|
||||
|
||||
def _save_active_wrapper(self) -> None:
|
||||
self._history.append(self._dj_db_wrapper.ensure_connection)
|
||||
|
||||
def _blocking_wrapper(*args, **kwargs) -> NoReturn:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError(
|
||||
"Database access not allowed, "
|
||||
'use the "django_db" mark, or the '
|
||||
'"db" or "transactional_db" fixtures to enable it.'
|
||||
)
|
||||
|
||||
def unblock(self) -> ContextManager[None]:
|
||||
"""Enable access to the Django database."""
|
||||
self._save_active_wrapper()
|
||||
self._dj_db_wrapper.ensure_connection = self._real_ensure_connection
|
||||
return _DatabaseBlockerContextManager(self)
|
||||
|
||||
def block(self) -> ContextManager[None]:
|
||||
"""Disable access to the Django database."""
|
||||
self._save_active_wrapper()
|
||||
self._dj_db_wrapper.ensure_connection = self._blocking_wrapper
|
||||
return _DatabaseBlockerContextManager(self)
|
||||
|
||||
def restore(self) -> None:
|
||||
self._dj_db_wrapper.ensure_connection = self._history.pop()
|
||||
|
||||
|
||||
# On Config.stash.
|
||||
blocking_manager_key = pytest.StashKey[DjangoDbBlocker]()
|
||||
|
||||
|
||||
def validate_urls(marker: pytest.Mark) -> list[str]:
|
||||
"""Validate the urls marker.
|
||||
|
||||
It checks the signature and creates the `urls` attribute on the
|
||||
marker which will have the correct value.
|
||||
"""
|
||||
|
||||
def apifun(urls: list[str]) -> list[str]:
|
||||
return urls
|
||||
|
||||
return apifun(*marker.args, **marker.kwargs)
|
||||
Reference in New Issue
Block a user