diff --git a/docs/changelog.rst b/docs/changelog.rst index 9162ae31..f36bd531 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ Changelog ========= -v4.12.1 (unreleased) +v4.13.0 (unreleased) -------------------- Compatibility @@ -9,6 +9,13 @@ 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 `__). + Bugfixes ^^^^^^^^ diff --git a/docs/helpers.rst b/docs/helpers.rst index 4d129dad..688458fb 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -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 diff --git a/pytest_django/__init__.py b/pytest_django/__init__.py index e4bb08f5..045b783a 100644 --- a/pytest_django/__init__.py +++ b/pytest_django/__init__.py @@ -5,7 +5,7 @@ __version__ = "unknown" -from .fixtures import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks +from .fixtures import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks, Settings from .plugin import DjangoDbBlocker @@ -13,5 +13,6 @@ "DjangoAssertNumQueries", "DjangoCaptureOnCommitCallbacks", "DjangoDbBlocker", + "Settings", "__version__", ] diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 160da50e..aa60db16 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -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", []) @@ -567,7 +576,7 @@ 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() @@ -575,13 +584,13 @@ def finalize(self) -> None: @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") diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index bddcbc68..2bdb8828 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -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 @@ -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( @@ -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)