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
9 changes: 8 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
Changelog
=========

v4.12.1 (unreleased)
v4.13.0 (unreleased)
--------------------

Compatibility
^^^^^^^^^^^^^

* Dropped support for Django 4.2 and 5.1.

Improvements
^^^^^^^^^^^^

* Export ``pytest_django.Settings`` from the top-level ``pytest_django``
module so the :fixture:`settings` fixture can be type-annotated
(`#1257 <https://github.com/pytest-dev/pytest-django/issues/1257>`__).

Bugfixes
^^^^^^^^

Expand Down
7 changes: 7 additions & 0 deletions docs/helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,13 @@ Example
settings.USE_TZ = True
assert settings.USE_TZ

If you use type annotations, you can annotate the fixture like this::

from pytest_django import Settings

def test_with_specific_settings(settings: Settings):
...


.. fixture:: django_assert_num_queries

Expand Down
3 changes: 2 additions & 1 deletion pytest_django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
__version__ = "unknown"


from .fixtures import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks
from .fixtures import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks, Settings
from .plugin import DjangoDbBlocker


__all__ = [
"DjangoAssertNumQueries",
"DjangoCaptureOnCommitCallbacks",
"DjangoDbBlocker",
"Settings",
"__version__",
]
21 changes: 15 additions & 6 deletions pytest_django/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,17 @@ def async_rf() -> django.test.AsyncRequestFactory:
return AsyncRequestFactory()


class SettingsWrapper:
def __init__(self) -> None:
class Settings:
"""The type of the :fixture:`settings` fixture."""

def __init__(
self,
*,
_is_pytest_django: bool = False,
) -> None:
assert _is_pytest_django, (
"Settings should only be instantiated from the `settings` fixture"
)
self._to_restore: list[django.test.override_settings]
object.__setattr__(self, "_to_restore", [])

Expand All @@ -567,21 +576,21 @@ def __getattr__(self, attr: str) -> Any:

return getattr(settings, attr)

def finalize(self) -> None:
def _finalize(self) -> None:
for override in reversed(self._to_restore):
override.disable()

del self._to_restore[:]


@pytest.fixture
def settings() -> Generator[SettingsWrapper, None, None]:
def settings() -> Generator[Settings, None, None]:
"""A Django settings object which restores changes after the testrun"""
skip_if_no_django()

wrapper = SettingsWrapper()
wrapper = Settings(_is_pytest_django=True)
yield wrapper
wrapper.finalize()
wrapper._finalize()


@pytest.fixture(scope="session")
Expand Down
24 changes: 14 additions & 10 deletions tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@

from .helpers import DjangoPytester

from pytest_django import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks, DjangoDbBlocker
from pytest_django import (
DjangoAssertNumQueries,
DjangoCaptureOnCommitCallbacks,
DjangoDbBlocker,
Settings,
)
from pytest_django_test.app.models import Item


if TYPE_CHECKING:
from pytest_django.django_compat import _User, _UserModel
from pytest_django.fixtures import SettingsWrapper
from pytest_django.live_server_helper import LiveServer


Expand Down Expand Up @@ -344,40 +348,40 @@ def test_django_capture_on_commit_callbacks_transactional(
class TestSettings:
"""Tests for the settings fixture, order matters"""

def test_modify_existing(self, settings) -> None:
def test_modify_existing(self, settings: Settings) -> None:
assert settings.SECRET_KEY == "foobar"
assert real_settings.SECRET_KEY == "foobar"
settings.SECRET_KEY = "spam"
assert settings.SECRET_KEY == "spam"
assert real_settings.SECRET_KEY == "spam"

def test_modify_existing_again(self, settings) -> None:
def test_modify_existing_again(self, settings: Settings) -> None:
assert settings.SECRET_KEY == "foobar"
assert real_settings.SECRET_KEY == "foobar"

def test_new(self, settings) -> None:
def test_new(self, settings: Settings) -> None:
assert not hasattr(settings, "SPAM")
assert not hasattr(real_settings, "SPAM")
settings.SPAM = "ham"
assert settings.SPAM == "ham"
assert real_settings.SPAM == "ham"

def test_new_again(self, settings) -> None:
def test_new_again(self, settings: Settings) -> None:
assert not hasattr(settings, "SPAM")
assert not hasattr(real_settings, "SPAM")

def test_deleted(self, settings) -> None:
def test_deleted(self, settings: Settings) -> None:
assert hasattr(settings, "SECRET_KEY")
assert hasattr(real_settings, "SECRET_KEY")
del settings.SECRET_KEY
assert not hasattr(settings, "SECRET_KEY")
assert not hasattr(real_settings, "SECRET_KEY")

def test_deleted_again(self, settings) -> None:
def test_deleted_again(self, settings: Settings) -> None:
assert hasattr(settings, "SECRET_KEY")
assert hasattr(real_settings, "SECRET_KEY")

def test_signals(self, settings) -> None:
def test_signals(self, settings: Settings) -> None:
result = []

def assert_signal(
Expand Down Expand Up @@ -481,7 +485,7 @@ def test_url(self, live_server: LiveServer) -> None:
def test_change_settings(
self,
live_server: LiveServer,
settings: SettingsWrapper, # noqa: ARG002
settings: Settings, # noqa: ARG002
) -> None:
assert live_server.url == force_str(live_server)

Expand Down