From 6c67449766b1516ee1bc35c60cc097b0151390ca Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:10:26 -0400 Subject: [PATCH 01/12] Removes all the ANN type ignores and fixes them --- pyproject.toml | 7 ---- pytest_django/asserts.py | 61 +++++++++++++++++------------ pytest_django/fixtures.py | 28 +++++++------ pytest_django/live_server_helper.py | 2 +- pytest_django/plugin.py | 12 +++--- tests/test_fixtures.py | 11 ++++-- 6 files changed, 65 insertions(+), 56 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8b53c8b9..bdc925b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -219,13 +219,6 @@ ignore = [ "S101", # Use of `assert` detected # TODO - need to fix these - "ANN001", # Missing type annotation for function argument - "ANN002", # Missing type annotation for public function - "ANN003", # Missing type annotation for public method - "ANN201", # Missing return type annotation for public function - "ANN202", # Missing return type annotation for private function - "ANN204", # Missing return type annotation for special method - "ANN401", # Dynamically typed expressions .. are disallowed "ARG001", # Unused function argument "ARG002", # Unused method argument "C901", # .. is too complex diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index f4e71dab..5be6b918 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -4,9 +4,8 @@ from __future__ import annotations -from collections.abc import Sequence from functools import wraps -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, overload from django import VERSION from django.test import LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase @@ -26,11 +25,11 @@ class MessagesTestCase(MessagesTestMixin, TestCase): test_case = TestCase("run") -def _wrapper(name: str): +def _wrapper(name: str) -> Callable[..., Any]: func = getattr(test_case, name) @wraps(func) - def assertion_func(*args, **kwargs): + def assertion_func(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) return assertion_func @@ -56,7 +55,10 @@ def assertion_func(*args, **kwargs): if TYPE_CHECKING: + from collections.abc import Collection, Iterator, Sequence + from django import forms + from django.db.models import Model, QuerySet, RawQuerySet from django.http.response import HttpResponseBase def assertRedirects( @@ -111,34 +113,34 @@ def assertTemplateUsed( template_name: str | None = ..., msg_prefix: str = ..., count: int | None = ..., - ): ... + ) -> None: ... def assertTemplateNotUsed( response: HttpResponseBase | str | None = ..., template_name: str | None = ..., msg_prefix: str = ..., - ): ... + ) -> None: ... def assertRaisesMessage( expected_exception: type[Exception], expected_message: str, - *args, - **kwargs, - ): ... + *args: Any, + **kwargs: Any, + ) -> None: ... def assertWarnsMessage( expected_warning: Warning, expected_message: str, - *args, - **kwargs, - ): ... + *args: Any, + **kwargs: Any, + ) -> None: ... def assertFieldOutput( - fieldclass, - valid, - invalid, - field_args=..., - field_kwargs=..., + fieldclass: type[forms.Field], + valid: Any, + invalid: Any, + field_args: Any = ..., + field_kwargs: Any = ..., empty_value: str = ..., ) -> None: ... @@ -194,34 +196,41 @@ def assertXMLNotEqual( # Removed in Django 5.1: use assertQuerySetEqual. def assertQuerysetEqual( - qs, - values, - transform=..., + qs: Iterator[Any] | list[Model] | QuerySet | RawQuerySet, + values: Collection[Any], + transform: Callable[[Model], Any] | type[str] | None = ..., ordered: bool = ..., msg: str | None = ..., ) -> None: ... def assertQuerySetEqual( - qs, - values, - transform=..., + qs: Iterator[Any] | list[Model] | QuerySet | RawQuerySet, + values: Collection[Any], + transform: Callable[[Model], Any] | type[str] | None = ..., ordered: bool = ..., msg: str | None = ..., ) -> None: ... + @overload + def assertNumQueries(num: int, func: None = None, *, using: str = ...) -> None: ... + @overload + def assertNumQueries( + num: int, func: Callable[..., Any], *args: Any, using: str = ..., **kwargs: Any + ) -> None: ... + def assertNumQueries( num: int, func=..., - *args, + *args: Any, using: str = ..., - **kwargs, + **kwargs: Any, ): ... # Added in Django 5.0. def assertMessages( response: HttpResponseBase, expected_messages: Sequence[Message], - *args, + *args: Any, ordered: bool = ..., ) -> None: ... diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 115dc4cc..56e2a299 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -6,7 +6,7 @@ from collections.abc import Generator, Iterable, Sequence from contextlib import AbstractContextManager, contextmanager from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Protocol, Union +from typing import TYPE_CHECKING, Callable, Literal, Optional, Protocol, Union import pytest @@ -16,6 +16,8 @@ if TYPE_CHECKING: + from typing import Any, Callable + import django import django.test @@ -337,7 +339,7 @@ def __getitem__(self, item: str) -> None: settings.MIGRATION_MODULES = DisableMigrations() class MigrateSilentCommand(migrate.Command): - def handle(self, *args, **kwargs): + def handle(self, *args: Any, **kwargs: Any) -> Any: kwargs["verbosity"] = 0 return super().handle(*args, **kwargs) @@ -456,7 +458,7 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model(db: None): +def django_user_model() -> type[django.contrib.auth.models.User]: """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -464,7 +466,7 @@ def django_user_model(db: None): @pytest.fixture -def django_username_field(django_user_model) -> str: +def django_username_field(django_user_model: type[django.contrib.auth.models.User]) -> str: """The fieldname for the username used with Django's user model.""" field: str = django_user_model.USERNAME_FIELD return field @@ -472,10 +474,9 @@ def django_username_field(django_user_model) -> str: @pytest.fixture def admin_user( - db: None, - django_user_model, + django_user_model: type[django.contrib.auth.models.User], django_username_field: str, -): +) -> django.contrib.auth.models.User: """A Django admin user. This uses an existing user with username "admin", or creates a new one with @@ -503,8 +504,7 @@ def admin_user( @pytest.fixture def admin_client( - db: None, - admin_user, + admin_user: django.contrib.auth.models.User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" from django.test import Client @@ -550,14 +550,14 @@ def __delattr__(self, attr: str) -> None: self._to_restore.append(override) - def __setattr__(self, attr: str, value) -> None: + def __setattr__(self, attr: str, value: Any) -> None: from django.test import override_settings override = override_settings(**{attr: value}) override.enable() self._to_restore.append(override) - def __getattr__(self, attr: str): + def __getattr__(self, attr: str) -> Any: from django.conf import settings return getattr(settings, attr) @@ -570,7 +570,7 @@ def finalize(self) -> None: @pytest.fixture -def settings(): +def settings() -> Generator[SettingsWrapper, None, None]: """A Django settings object which restores changes after the testrun""" skip_if_no_django() @@ -580,7 +580,9 @@ def settings(): @pytest.fixture(scope="session") -def live_server(request: pytest.FixtureRequest): +def live_server( + request: pytest.FixtureRequest, +) -> Generator[live_server_helper.LiveServer, None, None]: """Run a live Django server in the background during tests The address the server is started from is taken from the diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 03b92e1f..e43b7e7b 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -84,7 +84,7 @@ def url(self) -> str: def __str__(self) -> str: return self.url - def __add__(self, other) -> str: + def __add__(self, other: str) -> str: return f"{self}{other}" def __repr__(self) -> str: diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 9bab8971..39279374 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -15,7 +15,7 @@ from collections.abc import Generator from contextlib import AbstractContextManager from functools import reduce -from typing import TYPE_CHECKING, NoReturn +from typing import TYPE_CHECKING import pytest @@ -54,6 +54,8 @@ if TYPE_CHECKING: + from typing import Any, NoReturn, Self + import django @@ -186,7 +188,7 @@ def _handle_import_error(extra_message: str) -> Generator[None, None, None]: raise ImportError(msg) from None -def _add_django_project_to_path(args) -> str: +def _add_django_project_to_path(args: list[str]) -> str: def is_django_project(path: pathlib.Path) -> bool: try: return path.is_dir() and (path / "manage.py").exists() @@ -198,7 +200,7 @@ def arg_to_path(arg: str) -> pathlib.Path: arg = arg.split("::", 1)[0] return pathlib.Path(arg) - def find_django_path(args) -> pathlib.Path | None: + def find_django_path(args: list[str]) -> 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("-")] @@ -571,7 +573,7 @@ def _django_setup_unittest( original_runtest = TestCaseFunction.runtest - def non_debugging_runtest(self) -> None: + def non_debugging_runtest(self: Self) -> None: self._testcase(result=self) from django.test import SimpleTestCase @@ -831,7 +833,7 @@ def _dj_db_wrapper(self) -> django.db.backends.base.base.BaseDatabaseWrapper: def _save_active_wrapper(self) -> None: self._history.append(self._dj_db_wrapper.ensure_connection) - def _blocking_wrapper(*args, **kwargs) -> NoReturn: + def _blocking_wrapper(*args: Any, **kwargs: Any) -> NoReturn: __tracebackhide__ = True raise RuntimeError( "Database access not allowed, " diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 80578959..9c7bd3e2 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -8,6 +8,7 @@ import socket from collections.abc import Generator from contextlib import contextmanager +from typing import TYPE_CHECKING from urllib.error import HTTPError from urllib.request import urlopen @@ -25,6 +26,10 @@ from pytest_django_test.app.models import Item +if TYPE_CHECKING: + from django.contrib.auth.models import User + + @contextmanager def nonverbose_config(config: pytest.Config) -> Generator[None, None, None]: """Ensure that pytest's config.option.verbose is <= 0.""" @@ -60,14 +65,12 @@ def test_admin_client_no_db_marker(admin_client: Client) -> None: # For test below. @pytest.fixture -def existing_admin_user(django_user_model): +def existing_admin_user(django_user_model: type[User]) -> User: return django_user_model._default_manager.create_superuser("admin", None, None) +@pytest.mark.usefixtures("existing_admin_user", "admin_user") def test_admin_client_existing_user( - db: None, - existing_admin_user, - admin_user, admin_client: Client, ) -> None: resp = admin_client.get("/admin-required/") From d9b14c46f58b1d527d4de562f29ed22152de087b Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:24:09 -0400 Subject: [PATCH 02/12] Eek. --- pyproject.toml | 39 ++++++++++++++++++++------------------- tests/test_fixtures.py | 2 ++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bdc925b7..61a51a2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,25 +198,26 @@ extend-select = [ "RUF", # Ruff-specific rules ] ignore = [ - "D100", # Missing docstring in public module - "D101", # Missing docstring in public class - "D102", # Missing docstring in public method - "D103", # Missing docstring in public function - "D104", # Missing docstring in public package - "D105", # Missing docstring in magic method - "D107", # Missing docstring in __init__ - "D200", # One-line docstring should fit on one line - "D202", # No blank lines allowed after function docstring - "D203", # Class definitions that are not preceded by a blank line - "D205", # 1 blank line required between summary line and description - "D209", # Multi-line docstring closing quotes should be on a separate line - "D212", # Multi-line docstring summary should start at the first line - "D213", # Multi-line docstring summary should start at the second line - "D400", # First line should end with a period - "D401", # First line of docstring should be in imperative mood - "D404", # First word of the docstring should not be "This" - "D415", # First line should end with a period, question mark, or exclamation point - "S101", # Use of `assert` detected + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D200", # One-line docstring should fit on one line + "D202", # No blank lines allowed after function docstring + "D203", # Class definitions that are not preceded by a blank line + "D205", # 1 blank line required between summary line and description + "D209", # Multi-line docstring closing quotes should be on a separate line + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D404", # First word of the docstring should not be "This" + "D415", # First line should end with a period, question mark, or exclamation point + "S101", # Use of `assert` detected # TODO - need to fix these "ARG001", # Unused function argument diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 9c7bd3e2..02f39328 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -4,6 +4,8 @@ fixtures are tested in test_database. """ +from __future__ import annotations + import os import socket from collections.abc import Generator From bdcf7b2504c94e95d803a3524a25150e4b18dcab Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:41:48 -0400 Subject: [PATCH 03/12] Test work --- pytest_django/fixtures.py | 6 +++++- tests/test_fixtures.py | 7 +++++-- tests/test_without_django_loaded.py | 4 ++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 56e2a299..ad75af96 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -458,7 +458,9 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model() -> type[django.contrib.auth.models.User]: +def django_user_model( + # db: None +) -> type[django.contrib.auth.models.User]: """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -474,6 +476,7 @@ def django_username_field(django_user_model: type[django.contrib.auth.models.Use @pytest.fixture def admin_user( + # db: None, django_user_model: type[django.contrib.auth.models.User], django_username_field: str, ) -> django.contrib.auth.models.User: @@ -504,6 +507,7 @@ def admin_user( @pytest.fixture def admin_client( + # db: None, admin_user: django.contrib.auth.models.User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 02f39328..3008a489 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -59,7 +59,7 @@ def test_admin_client(admin_client: Client) -> None: assert force_str(resp.content) == "You are an admin" -def test_admin_client_no_db_marker(admin_client: Client) -> None: +def test_admin_client_no_db_marker(db: None, admin_client: Client) -> None: assert isinstance(admin_client, Client) resp = admin_client.get("/admin-required/") assert force_str(resp.content) == "You are an admin" @@ -71,6 +71,7 @@ def existing_admin_user(django_user_model: type[User]) -> User: return django_user_model._default_manager.create_superuser("admin", None, None) +@pytest.mark.django_db @pytest.mark.usefixtures("existing_admin_user", "admin_user") def test_admin_client_existing_user( admin_client: Client, @@ -84,7 +85,7 @@ def test_admin_user(admin_user, django_user_model) -> None: assert isinstance(admin_user, django_user_model) -def test_admin_user_no_db_marker(admin_user, django_user_model) -> None: +def test_admin_user_no_db_marker(db: None, admin_user, django_user_model) -> None: assert isinstance(admin_user, django_user_model) @@ -676,9 +677,11 @@ def admin_required_view(request): ) django_pytester.makepyfile( """ + import pytest from django.utils.encoding import force_str from tpkg.app.models import MyCustomUser + @pytest.mark.django_db def test_custom_user_model(admin_client): resp = admin_client.get('/admin-required/') assert force_str(resp.content) == 'You are an admin' diff --git a/tests/test_without_django_loaded.py b/tests/test_without_django_loaded.py index cd376fa6..1426d2b6 100644 --- a/tests/test_without_django_loaded.py +++ b/tests/test_without_django_loaded.py @@ -51,12 +51,16 @@ def test_transactional_db(transactional_db): r.stdout.fnmatch_lines(["*4 skipped*"]) +# @pytest.mark.django_db def test_client(pytester: pytest.Pytester) -> None: pytester.makepyfile( """ + import pytest + def test_client(client): assert 0 + @pytest.mark.django_db def test_admin_client(admin_client): assert 0 """ From af605b90a937b94da0b0cfe5d6ab36bc1bc85f84 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:43:59 -0400 Subject: [PATCH 04/12] . --- pytest_django/fixtures.py | 6 +----- tests/test_without_django_loaded.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index ad75af96..56e2a299 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -458,9 +458,7 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model( - # db: None -) -> type[django.contrib.auth.models.User]: +def django_user_model() -> type[django.contrib.auth.models.User]: """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -476,7 +474,6 @@ def django_username_field(django_user_model: type[django.contrib.auth.models.Use @pytest.fixture def admin_user( - # db: None, django_user_model: type[django.contrib.auth.models.User], django_username_field: str, ) -> django.contrib.auth.models.User: @@ -507,7 +504,6 @@ def admin_user( @pytest.fixture def admin_client( - # db: None, admin_user: django.contrib.auth.models.User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" diff --git a/tests/test_without_django_loaded.py b/tests/test_without_django_loaded.py index 1426d2b6..85a6e767 100644 --- a/tests/test_without_django_loaded.py +++ b/tests/test_without_django_loaded.py @@ -51,7 +51,6 @@ def test_transactional_db(transactional_db): r.stdout.fnmatch_lines(["*4 skipped*"]) -# @pytest.mark.django_db def test_client(pytester: pytest.Pytester) -> None: pytester.makepyfile( """ From 687abcb69fffb8e05a52b994bf23a168ecd420a6 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:50:30 -0400 Subject: [PATCH 05/12] . --- pytest_django/asserts.py | 4 ++-- tests/test_fixtures.py | 2 -- tests/test_without_django_loaded.py | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 5be6b918..82f101c2 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -5,7 +5,7 @@ from __future__ import annotations from functools import wraps -from typing import TYPE_CHECKING, Any, Callable, overload +from typing import TYPE_CHECKING, Any, Callable from django import VERSION from django.test import LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase @@ -55,7 +55,7 @@ def assertion_func(*args: Any, **kwargs: Any) -> Any: if TYPE_CHECKING: - from collections.abc import Collection, Iterator, Sequence + from collections.abc import Collection, Iterator, Sequence, overload from django import forms from django.db.models import Model, QuerySet, RawQuerySet diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 3008a489..1a6fefa5 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -677,11 +677,9 @@ def admin_required_view(request): ) django_pytester.makepyfile( """ - import pytest from django.utils.encoding import force_str from tpkg.app.models import MyCustomUser - @pytest.mark.django_db def test_custom_user_model(admin_client): resp = admin_client.get('/admin-required/') assert force_str(resp.content) == 'You are an admin' diff --git a/tests/test_without_django_loaded.py b/tests/test_without_django_loaded.py index 85a6e767..cd376fa6 100644 --- a/tests/test_without_django_loaded.py +++ b/tests/test_without_django_loaded.py @@ -54,12 +54,9 @@ def test_transactional_db(transactional_db): def test_client(pytester: pytest.Pytester) -> None: pytester.makepyfile( """ - import pytest - def test_client(client): assert 0 - @pytest.mark.django_db def test_admin_client(admin_client): assert 0 """ From 9854e91e561a03c56a73d1c937a55653f7cc8e3d Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:52:33 -0400 Subject: [PATCH 06/12] Not a fan of the 'db' but depricating it is a much bigger undertaking --- pytest_django/fixtures.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 56e2a299..1d29452e 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -458,7 +458,7 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model() -> type[django.contrib.auth.models.User]: +def django_user_model(db: None) -> type[django.contrib.auth.models.User]: """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -474,6 +474,7 @@ def django_username_field(django_user_model: type[django.contrib.auth.models.Use @pytest.fixture def admin_user( + db: None, django_user_model: type[django.contrib.auth.models.User], django_username_field: str, ) -> django.contrib.auth.models.User: @@ -504,6 +505,7 @@ def admin_user( @pytest.fixture def admin_client( + db: None, admin_user: django.contrib.auth.models.User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" From 66f9bcf70e25fb4cba5dc5c0cca62337b215d9d0 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:54:48 -0400 Subject: [PATCH 07/12] . --- pytest_django/asserts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 82f101c2..574485f5 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -55,7 +55,8 @@ def assertion_func(*args: Any, **kwargs: Any) -> Any: if TYPE_CHECKING: - from collections.abc import Collection, Iterator, Sequence, overload + from collections.abc import Collection, Iterator, Sequence + from typing import overload from django import forms from django.db.models import Model, QuerySet, RawQuerySet From 650412236a7084aa1f240fe3f0cf5705a9707289 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 09:59:53 -0400 Subject: [PATCH 08/12] Missing better handling of the current User model --- pytest_django/asserts.py | 7 +++++-- pytest_django/fixtures.py | 2 +- pytest_django/plugin.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 574485f5..d352101a 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -56,7 +56,7 @@ def assertion_func(*args: Any, **kwargs: Any) -> Any: if TYPE_CHECKING: from collections.abc import Collection, Iterator, Sequence - from typing import overload + from typing import ContextManager, overload from django import forms from django.db.models import Model, QuerySet, RawQuerySet @@ -213,7 +213,10 @@ def assertQuerySetEqual( ) -> None: ... @overload - def assertNumQueries(num: int, func: None = None, *, using: str = ...) -> None: ... + def assertNumQueries( + num: int, func: None = None, *, using: str = ... + ) -> ContextManager[None]: ... + @overload def assertNumQueries( num: int, func: Callable[..., Any], *args: Any, using: str = ..., **kwargs: Any diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 1d29452e..4c3274ef 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -462,7 +462,7 @@ def django_user_model(db: None) -> type[django.contrib.auth.models.User]: """The class of Django's user model.""" from django.contrib.auth import get_user_model - return get_user_model() + return get_user_model() # type: ignore[no-any-return] @pytest.fixture diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 39279374..6987305a 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -54,7 +54,7 @@ if TYPE_CHECKING: - from typing import Any, NoReturn, Self + from typing import Any, NoReturn import django @@ -573,7 +573,7 @@ def _django_setup_unittest( original_runtest = TestCaseFunction.runtest - def non_debugging_runtest(self: Self) -> None: + def non_debugging_runtest(self) -> None: self._testcase(result=self) from django.test import SimpleTestCase From 8e8dec1856fd5ac0d47f4f7283724856f8ea8781 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 10:13:12 -0400 Subject: [PATCH 09/12] Woohoo --- pytest_django/asserts.py | 5 +++-- pytest_django/django_compat.py | 14 ++++++++++++++ pytest_django/fixtures.py | 11 ++++++----- pytest_django/plugin.py | 2 +- tests/test_fixtures.py | 4 ++-- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index d352101a..76a45809 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -56,7 +56,8 @@ def assertion_func(*args: Any, **kwargs: Any) -> Any: if TYPE_CHECKING: from collections.abc import Collection, Iterator, Sequence - from typing import ContextManager, overload + from contextlib import AbstractContextManager + from typing import overload from django import forms from django.db.models import Model, QuerySet, RawQuerySet @@ -215,7 +216,7 @@ def assertQuerySetEqual( @overload def assertNumQueries( num: int, func: None = None, *, using: str = ... - ) -> ContextManager[None]: ... + ) -> AbstractContextManager[None]: ... @overload def assertNumQueries( diff --git a/pytest_django/django_compat.py b/pytest_django/django_compat.py index 6c877130..301114a8 100644 --- a/pytest_django/django_compat.py +++ b/pytest_django/django_compat.py @@ -2,9 +2,23 @@ # this is the case before you call them. from __future__ import annotations +from typing import TYPE_CHECKING + import pytest +if TYPE_CHECKING: + from typing import TypeAlias + + from django.contrib.auth.models import AbstractBaseUser + + _User: TypeAlias = AbstractBaseUser + + _UserModel: TypeAlias = type[_User] + + __all__ = ("_User", "_UserModel") + + 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 diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 4c3274ef..c1386026 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -22,6 +22,7 @@ import django.test from . import DjangoDbBlocker + from .django_compat import _User, _UserModel _DjangoDbDatabases = Optional[Union[Literal["__all__"], Iterable[str]]] @@ -458,7 +459,7 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model(db: None) -> type[django.contrib.auth.models.User]: +def django_user_model(db: None) -> _UserModel: """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -466,7 +467,7 @@ def django_user_model(db: None) -> type[django.contrib.auth.models.User]: @pytest.fixture -def django_username_field(django_user_model: type[django.contrib.auth.models.User]) -> str: +def django_username_field(django_user_model: _UserModel) -> str: """The fieldname for the username used with Django's user model.""" field: str = django_user_model.USERNAME_FIELD return field @@ -475,9 +476,9 @@ def django_username_field(django_user_model: type[django.contrib.auth.models.Use @pytest.fixture def admin_user( db: None, - django_user_model: type[django.contrib.auth.models.User], + django_user_model: _User, django_username_field: str, -) -> django.contrib.auth.models.User: +) -> _User: """A Django admin user. This uses an existing user with username "admin", or creates a new one with @@ -506,7 +507,7 @@ def admin_user( @pytest.fixture def admin_client( db: None, - admin_user: django.contrib.auth.models.User, + admin_user: _User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" from django.test import Client diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 6987305a..0c582403 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -573,7 +573,7 @@ def _django_setup_unittest( original_runtest = TestCaseFunction.runtest - def non_debugging_runtest(self) -> None: + def non_debugging_runtest(self) -> None: # noqa: ANN001 self._testcase(result=self) from django.test import SimpleTestCase diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 1a6fefa5..508c7787 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: - from django.contrib.auth.models import User + from pytest_django.django_compat import _User, _UserModel @contextmanager @@ -67,7 +67,7 @@ def test_admin_client_no_db_marker(db: None, admin_client: Client) -> None: # For test below. @pytest.fixture -def existing_admin_user(django_user_model: type[User]) -> User: +def existing_admin_user(django_user_model: _UserModel) -> _User: return django_user_model._default_manager.create_superuser("admin", None, None) From cf42bae7cdefb5d39a37c60fe5559c496a5d7a8a Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 10:16:02 -0400 Subject: [PATCH 10/12] oops. --- tests/test_fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 508c7787..6cb6c221 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -85,7 +85,7 @@ def test_admin_user(admin_user, django_user_model) -> None: assert isinstance(admin_user, django_user_model) -def test_admin_user_no_db_marker(db: None, admin_user, django_user_model) -> None: +def test_admin_user_no_db_marker(admin_user, django_user_model) -> None: assert isinstance(admin_user, django_user_model) From 0ffeae6a832475d509962a3ac90a5602f66f463f Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 10:52:26 -0400 Subject: [PATCH 11/12] Making copilot happy --- pytest_django/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index c1386026..9b313a4d 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -6,7 +6,7 @@ from collections.abc import Generator, Iterable, Sequence from contextlib import AbstractContextManager, contextmanager from functools import partial -from typing import TYPE_CHECKING, Callable, Literal, Optional, Protocol, Union +from typing import TYPE_CHECKING, Literal, Optional, Protocol, Union import pytest From 634ce32b60712d5f9736381a93026afa71273e10 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 14 Aug 2025 10:54:31 -0400 Subject: [PATCH 12/12] Better-errrr --- pytest_django/fixtures.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 9b313a4d..1dfa4c3f 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -6,7 +6,7 @@ from collections.abc import Generator, Iterable, Sequence from contextlib import AbstractContextManager, contextmanager from functools import partial -from typing import TYPE_CHECKING, Literal, Optional, Protocol, Union +from typing import TYPE_CHECKING, Protocol import pytest @@ -16,7 +16,7 @@ if TYPE_CHECKING: - from typing import Any, Callable + from typing import Any, Callable, Literal, Optional, Union import django import django.test @@ -24,11 +24,10 @@ from . import DjangoDbBlocker from .django_compat import _User, _UserModel - -_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] + _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__ = [