diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 953273dc6..45888bef0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build ext package run: python -m build ext/ - name: Publish ext to PyPI - uses: pypa/gh-action-pypi-publish@v1.10.3 + uses: pypa/gh-action-pypi-publish@v1.12.2 with: packages-dir: ext/dist/ @@ -53,4 +53,4 @@ jobs: - name: Build package run: python -m build - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.10.3 + uses: pypa/gh-action-pypi-publish@v1.12.2 diff --git a/.gitignore b/.gitignore index 1141379bd..18cff1611 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.egg-info .DS_Store .idea/ +.vscode/ .mypy_cache/ .pytest_cache/ .ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae09b1d1d..404e2be94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,14 +17,11 @@ repos: args: [--fix=lf] - id: check-case-conflict - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.8.1 hooks: - id: ruff args: ["--fix", "--exit-non-zero-on-fix"] - - repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black + - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: diff --git a/README.md b/README.md index 7af21ed55..cd62bcf73 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ We rely on different `django` and `mypy` versions: | django-stubs | Mypy version | Django version | Django partial support | Python version | |----------------|--------------|----------------|------------------------|----------------| +| 5.1.1 | 1.13.x | 5.1 | 4.2 | 3.8 - 3.12 | | 5.1.0 | 1.11.x | 5.1 | 4.2 | 3.8 - 3.12 | | 5.0.4 | 1.11.x | 5.0 | 4.2 | 3.8 - 3.12 | | 5.0.3 | 1.11.x | 5.0 | 4.2 | 3.8 - 3.12 | @@ -67,6 +68,9 @@ We rely on different `django` and `mypy` versions: | 1.15.0 | 1.0.x | 4.1 | 4.0, 3.2 | 3.7 - 3.11 | | 1.14.0 | 0.990+ | 4.1 | 4.0, 3.2 | 3.7 - 3.11 | +What "partial" support means, and why we don't pin to the exact Django/mypy version, is explained in +https://github.com/typeddjango/django-stubs/discussions/2101#discussioncomment-9276632. + ## Features ### Type checking of Model Meta attributes diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index 3a5ca8ab8..ce6eaed75 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -1,13 +1,12 @@ import enum from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence -from typing import Any, Generic, Literal, TypeVar, type_check_only +from typing import Any, Generic, Literal, TypeVar, cast, type_check_only from django import forms from django.contrib.admin.filters import FieldListFilter, ListFilter from django.contrib.admin.models import LogEntry from django.contrib.admin.sites import AdminSite from django.contrib.admin.views.main import ChangeList -from django.contrib.auth.forms import AdminPasswordChangeForm from django.contrib.contenttypes.models import ContentType from django.core.checks.messages import CheckMessage from django.core.paginator import Paginator @@ -45,9 +44,9 @@ VERTICAL: Literal[2] _Direction: TypeAlias = Literal[1, 2] class ShowFacets(enum.Enum): - NEVER: str - ALLOW: str - ALWAYS: str + NEVER = cast(str, ...) + ALLOW = cast(str, ...) + ALWAYS = cast(str, ...) def get_content_type_for_model(obj: type[Model] | Model) -> ContentType: ... def get_ul_class(radio_style: int) -> str: ... @@ -225,7 +224,7 @@ class ModelAdmin(BaseModelAdmin[_ModelT]): def _get_edited_object_pks(self, request: HttpRequest, prefix: str) -> list[str]: ... def _get_list_editable_queryset(self, request: HttpRequest, prefix: str) -> QuerySet[_ModelT]: ... def construct_change_message( - self, request: HttpRequest, form: AdminPasswordChangeForm, formsets: Iterable[BaseFormSet], add: bool = ... + self, request: HttpRequest, form: forms.Form, formsets: Iterable[BaseFormSet], add: bool = ... ) -> list[dict[str, dict[str, list[str]]]]: ... def message_user( self, diff --git a/django-stubs/contrib/admin/sites.pyi b/django-stubs/contrib/admin/sites.pyi index cb7b1963d..e1e1e0493 100644 --- a/django-stubs/contrib/admin/sites.pyi +++ b/django-stubs/contrib/admin/sites.pyi @@ -1,6 +1,6 @@ -import sys from collections.abc import Callable, Iterable from typing import Any, TypeVar +from weakref import WeakSet from django.apps.config import AppConfig from django.contrib.admin.models import LogEntry @@ -16,14 +16,7 @@ from django.urls import URLPattern, URLResolver from django.utils.functional import LazyObject, _StrOrPromise from typing_extensions import TypeAlias -if sys.version_info >= (3, 9): - from weakref import WeakSet - - all_sites: WeakSet[AdminSite] -else: - from collections.abc import MutableSet - - all_sites: MutableSet[AdminSite] +all_sites: WeakSet[AdminSite] _ViewType = TypeVar("_ViewType", bound=Callable[..., HttpResponseBase]) _ModelT = TypeVar("_ModelT", bound=Model) diff --git a/django-stubs/contrib/admin/utils.pyi b/django-stubs/contrib/admin/utils.pyi index fd3a30398..34acd72fd 100644 --- a/django-stubs/contrib/admin/utils.pyi +++ b/django-stubs/contrib/admin/utils.pyi @@ -7,14 +7,13 @@ from uuid import UUID from _typeshed import Unused from django.contrib.admin.options import BaseModelAdmin from django.contrib.admin.sites import AdminSite -from django.contrib.auth.forms import AdminPasswordChangeForm from django.db.models.base import Model from django.db.models.deletion import Collector from django.db.models.fields import Field, reverse_related from django.db.models.options import Options from django.db.models.query import QuerySet from django.db.models.query_utils import Q -from django.forms.forms import BaseForm +from django.forms.forms import BaseForm, Form from django.forms.formsets import BaseFormSet from django.http.request import HttpRequest from django.utils.datastructures import _IndexableCollection @@ -100,5 +99,5 @@ def get_model_from_relation(field: Field | reverse_related.ForeignObjectRel) -> def reverse_field_path(model: type[Model], path: str) -> tuple[type[Model], str]: ... def get_fields_from_path(model: type[Model], path: str) -> list[Field]: ... def construct_change_message( - form: AdminPasswordChangeForm, formsets: Iterable[BaseFormSet], add: bool + form: Form, formsets: Iterable[BaseFormSet], add: bool ) -> list[dict[str, dict[str, list[str]]]]: ... diff --git a/django-stubs/contrib/auth/base_user.pyi b/django-stubs/contrib/auth/base_user.pyi index c7616c402..577e1cc3f 100644 --- a/django-stubs/contrib/auth/base_user.pyi +++ b/django-stubs/contrib/auth/base_user.pyi @@ -1,4 +1,5 @@ -from typing import Any, ClassVar, Iterable, Literal, TypeVar, overload +from collections.abc import Iterable +from typing import Any, ClassVar, Literal, TypeVar, overload from django.db import models from django.db.models.base import Model diff --git a/django-stubs/contrib/gis/db/backends/postgis/operations.pyi b/django-stubs/contrib/gis/db/backends/postgis/operations.pyi index 0f2c95b77..3097bb1fb 100644 --- a/django-stubs/contrib/gis/db/backends/postgis/operations.pyi +++ b/django-stubs/contrib/gis/db/backends/postgis/operations.pyi @@ -1,4 +1,5 @@ -from typing import Any, Literal, MutableMapping +from collections.abc import MutableMapping +from typing import Any, Literal from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations from django.contrib.gis.db.backends.utils import SpatialOperator diff --git a/django-stubs/contrib/gis/db/backends/utils.pyi b/django-stubs/contrib/gis/db/backends/utils.pyi index 1632d5bab..d73e5ab8b 100644 --- a/django-stubs/contrib/gis/db/backends/utils.pyi +++ b/django-stubs/contrib/gis/db/backends/utils.pyi @@ -1,5 +1,5 @@ -from collections.abc import Sequence -from typing import Any, MutableMapping +from collections.abc import MutableMapping, Sequence +from typing import Any from django.contrib.gis.db.models.lookups import GISLookup from django.db.backends.base.base import BaseDatabaseWrapper diff --git a/django-stubs/contrib/gis/gdal/srs.pyi b/django-stubs/contrib/gis/gdal/srs.pyi index f43fab0da..0fb47286e 100644 --- a/django-stubs/contrib/gis/gdal/srs.pyi +++ b/django-stubs/contrib/gis/gdal/srs.pyi @@ -1,12 +1,12 @@ from enum import IntEnum -from typing import Any, AnyStr +from typing import Any, AnyStr, cast from django.contrib.gis.gdal.base import GDALBase from typing_extensions import Self class AxisOrder(IntEnum): - TRADITIONAL: int - AUTHORITY: int + TRADITIONAL = cast(int, ...) + AUTHORITY = cast(int, ...) class SpatialReference(GDALBase): destructor: Any diff --git a/django-stubs/contrib/gis/geoip2/base.pyi b/django-stubs/contrib/gis/geoip2/base.pyi index 8b4884238..56546e504 100644 --- a/django-stubs/contrib/gis/geoip2/base.pyi +++ b/django-stubs/contrib/gis/geoip2/base.pyi @@ -3,6 +3,7 @@ from pathlib import Path from typing import Any from django.contrib.gis.geos import Point +from django.utils.functional import cached_property GEOIP_SETTINGS: dict[str, Any] @@ -23,6 +24,10 @@ class GeoIP2: def country_code(self, query: str) -> str: ... def country_name(self, query: str) -> str: ... def country(self, query: str) -> dict[str, Any]: ... + @cached_property + def is_city(self) -> bool: ... + @cached_property + def is_country(self) -> bool: ... def coords(self, query: str, ordering: Sequence[str] = ...) -> tuple[float, float] | tuple[None, None]: ... def lon_lat(self, query: str) -> tuple[float, float] | tuple[None, None]: ... def lat_lon(self, query: str) -> tuple[float, float] | tuple[None, None]: ... diff --git a/django-stubs/contrib/messages/storage/cookie.pyi b/django-stubs/contrib/messages/storage/cookie.pyi index b6e6ae592..4428a5b87 100644 --- a/django-stubs/contrib/messages/storage/cookie.pyi +++ b/django-stubs/contrib/messages/storage/cookie.pyi @@ -1,5 +1,6 @@ import json -from typing import Any, Callable, Sequence +from collections.abc import Sequence +from typing import Any, Callable from django.contrib.messages.storage.base import BaseStorage diff --git a/django-stubs/contrib/postgres/fields/array.pyi b/django-stubs/contrib/postgres/fields/array.pyi index 8b2094ab3..de9dc38f9 100644 --- a/django-stubs/contrib/postgres/fields/array.pyi +++ b/django-stubs/contrib/postgres/fields/array.pyi @@ -1,5 +1,5 @@ from collections.abc import Iterable, Sequence -from typing import Any, TypeVar +from typing import Any, ClassVar, TypeVar from _typeshed import Unused from django.core.validators import _ValidatorCallable @@ -22,7 +22,7 @@ class ArrayField(CheckFieldDefaultMixin, Field[_ST, _GT]): _pyi_private_get_type: list[Any] empty_strings_allowed: bool - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: Field size: int | None default_validators: Sequence[_ValidatorCallable] diff --git a/django-stubs/contrib/postgres/forms/array.pyi b/django-stubs/contrib/postgres/forms/array.pyi index 65adef545..0c4ff4a72 100644 --- a/django-stubs/contrib/postgres/forms/array.pyi +++ b/django-stubs/contrib/postgres/forms/array.pyi @@ -1,5 +1,5 @@ from collections.abc import Sequence -from typing import Any +from typing import Any, ClassVar from django import forms from django.db.models.fields import _ErrorMessagesDict @@ -10,7 +10,7 @@ from django.forms.widgets import _OptAttrs from ..utils import prefix_validation_error as prefix_validation_error class SimpleArrayField(forms.CharField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: forms.Field delimiter: str min_length: int | None @@ -46,7 +46,7 @@ class SplitArrayWidget(forms.Widget): def needs_multipart_form(self) -> bool: ... # type: ignore[override] class SplitArrayField(forms.Field): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: forms.Field size: int remove_trailing_nulls: bool diff --git a/django-stubs/contrib/postgres/forms/hstore.pyi b/django-stubs/contrib/postgres/forms/hstore.pyi index d672d3b85..32c874731 100644 --- a/django-stubs/contrib/postgres/forms/hstore.pyi +++ b/django-stubs/contrib/postgres/forms/hstore.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, ClassVar from django import forms from django.db.models.fields import _ErrorMessagesDict @@ -6,7 +6,7 @@ from django.forms.fields import _ClassLevelWidgetT class HStoreField(forms.CharField): widget: _ClassLevelWidgetT - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] def prepare_value(self, value: Any) -> Any: ... def to_python(self, value: Any) -> dict[str, str | None]: ... # type: ignore[override] def has_changed(self, initial: Any, data: Any) -> bool: ... diff --git a/django-stubs/contrib/postgres/forms/ranges.pyi b/django-stubs/contrib/postgres/forms/ranges.pyi index 0a2b2c8e6..a93cb8496 100644 --- a/django-stubs/contrib/postgres/forms/ranges.pyi +++ b/django-stubs/contrib/postgres/forms/ranges.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, ClassVar from django import forms from django.db.models.fields import _ErrorMessagesDict @@ -13,7 +13,7 @@ class HiddenRangeWidget(RangeWidget): def __init__(self, attrs: _OptAttrs | None = None) -> None: ... class BaseRangeField(forms.MultiValueField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: type[forms.Field] range_type: type[Range] hidden_widget: type[forms.Widget] @@ -22,21 +22,21 @@ class BaseRangeField(forms.MultiValueField): def compress(self, values: tuple[Any | None, Any | None]) -> Range | None: ... class IntegerRangeField(BaseRangeField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: type[forms.Field] range_type: type[Range] class DecimalRangeField(BaseRangeField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: type[forms.Field] range_type: type[Range] class DateTimeRangeField(BaseRangeField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: type[forms.Field] range_type: type[Range] class DateRangeField(BaseRangeField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] base_field: type[forms.Field] range_type: type[Range] diff --git a/django-stubs/core/cache/backends/redis.pyi b/django-stubs/core/cache/backends/redis.pyi index ede8e14f9..12aee0f62 100644 --- a/django-stubs/core/cache/backends/redis.pyi +++ b/django-stubs/core/cache/backends/redis.pyi @@ -1,6 +1,6 @@ -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from datetime import timedelta -from typing import Any, Callable, Iterable, Protocol, SupportsInt, overload, type_check_only +from typing import Any, Callable, Protocol, SupportsInt, overload, type_check_only from _typeshed import ReadableBuffer from django.core.cache.backends.base import BaseCache diff --git a/django-stubs/core/handlers/exception.pyi b/django-stubs/core/handlers/exception.pyi index 7edff206a..ea2df1c47 100644 --- a/django-stubs/core/handlers/exception.pyi +++ b/django-stubs/core/handlers/exception.pyi @@ -6,7 +6,7 @@ from django.http.response import HttpResponse, HttpResponseBase from django.urls.resolvers import URLResolver def convert_exception_to_response( - get_response: Callable[[HttpRequest], HttpResponseBase | Awaitable[HttpResponseBase]] + get_response: Callable[[HttpRequest], HttpResponseBase | Awaitable[HttpResponseBase]], ) -> Callable[[HttpRequest], HttpResponseBase | Awaitable[HttpResponseBase]]: ... def response_for_exception(request: HttpRequest, exc: Exception) -> HttpResponse: ... def get_exception_response( diff --git a/django-stubs/db/models/constants.pyi b/django-stubs/db/models/constants.pyi index 14fc6195e..72ae7cdc6 100644 --- a/django-stubs/db/models/constants.pyi +++ b/django-stubs/db/models/constants.pyi @@ -1,7 +1,8 @@ from enum import Enum +from typing import cast LOOKUP_SEP: str class OnConflict(Enum): - IGNORE: str - UPDATE: str + IGNORE = cast(str, ...) + UPDATE = cast(str, ...) diff --git a/django-stubs/db/models/constraints.pyi b/django-stubs/db/models/constraints.pyi index 988f0f4b0..e3dd59585 100644 --- a/django-stubs/db/models/constraints.pyi +++ b/django-stubs/db/models/constraints.pyi @@ -1,6 +1,6 @@ from collections.abc import Sequence from enum import Enum -from typing import Any, overload +from typing import Any, cast, overload from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.models.base import Model @@ -10,8 +10,8 @@ from django.utils.functional import _StrOrPromise from typing_extensions import Self, deprecated class Deferrable(Enum): - DEFERRED: str - IMMEDIATE: str + DEFERRED = cast(str, ...) + IMMEDIATE = cast(str, ...) class BaseConstraint: name: str diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index 68a6c5678..8d45f1c35 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -26,6 +26,7 @@ BLANK_CHOICE_DASH: list[tuple[str, str]] _ChoicesList: TypeAlias = Sequence[_Choice] | Sequence[_ChoiceNamedGroup] _LimitChoicesTo: TypeAlias = Q | dict[str, Any] +_LimitChoicesToCallable: TypeAlias = Callable[[], _LimitChoicesTo] _F = TypeVar("_F", bound=Field, covariant=True) @@ -39,7 +40,7 @@ class _FieldDescriptor(Protocol[_F]): @property def field(self) -> _F: ... -_AllLimitChoicesTo: TypeAlias = _LimitChoicesTo | _ChoicesCallable # noqa: PYI047 +_AllLimitChoicesTo: TypeAlias = _LimitChoicesTo | _LimitChoicesToCallable | _ChoicesCallable # noqa: PYI047 _ErrorMessagesMapping: TypeAlias = Mapping[str, _StrOrPromise] _ErrorMessagesDict: TypeAlias = dict[str, _StrOrPromise] @@ -146,7 +147,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): creation_counter: int auto_creation_counter: int default_validators: Sequence[validators._ValidatorCallable] - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] hidden: bool system_check_removed_details: Any | None system_check_deprecated_details: Any | None @@ -442,7 +443,7 @@ class GenericIPAddressField(Field[_ST, _GT]): _pyi_private_set_type: str | int | Callable[..., Any] | Combinable _pyi_private_get_type: str - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] unpack_ipv4: bool protocol: str def __init__( diff --git a/django-stubs/db/models/fields/generated.pyi b/django-stubs/db/models/fields/generated.pyi index 1be70f2ea..8ff57c220 100644 --- a/django-stubs/db/models/fields/generated.pyi +++ b/django-stubs/db/models/fields/generated.pyi @@ -1,4 +1,5 @@ -from typing import Any, ClassVar, Iterable, Literal +from collections.abc import Iterable +from typing import Any, ClassVar, Literal from django.core.validators import _ValidatorCallable from django.db import models diff --git a/django-stubs/db/models/fields/related_descriptors.pyi b/django-stubs/db/models/fields/related_descriptors.pyi index 4deec2d44..0d5782a32 100644 --- a/django-stubs/db/models/fields/related_descriptors.pyi +++ b/django-stubs/db/models/fields/related_descriptors.pyi @@ -1,4 +1,4 @@ -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Mapping from typing import Any, Generic, NoReturn, TypeVar, overload, type_check_only from django.core.exceptions import ObjectDoesNotExist @@ -102,10 +102,22 @@ class RelatedManager(Manager[_To], Generic[_To]): async def aadd(self, *objs: _To | int, bulk: bool = ...) -> None: ... def remove(self, *objs: _To | int, bulk: bool = ...) -> None: ... async def aremove(self, *objs: _To | int, bulk: bool = ...) -> None: ... - def set(self, objs: QuerySet[_To] | Iterable[_To | int], *, bulk: bool = ..., clear: bool = ...) -> None: ... - async def aset(self, objs: QuerySet[_To] | Iterable[_To | int], *, bulk: bool = ..., clear: bool = ...) -> None: ... - def clear(self) -> None: ... - async def aclear(self) -> None: ... + def clear(self, *, clear: bool = ...) -> None: ... + async def aclear(self, *, clear: bool = ...) -> None: ... + def set( + self, + objs: QuerySet[_To] | Iterable[_To | int], + *, + bulk: bool = ..., + clear: bool = ..., + ) -> None: ... + async def aset( + self, + objs: QuerySet[_To] | Iterable[_To | int], + *, + bulk: bool = ..., + clear: bool = ..., + ) -> None: ... def __call__(self, *, manager: str) -> RelatedManager[_To]: ... def create_reverse_many_to_one_manager( @@ -142,28 +154,72 @@ class ManyToManyDescriptor(ReverseManyToOneDescriptor, Generic[_To, _Through]): class ManyRelatedManager(Manager[_To], Generic[_To, _Through]): related_val: tuple[int, ...] through: type[_Through] - def add(self, *objs: _To | int, bulk: bool = ..., through_defaults: dict[str, Any] | None = ...) -> None: ... - async def aadd(self, *objs: _To | int, bulk: bool = ..., through_defaults: dict[str, Any] | None = ...) -> None: ... - def remove(self, *objs: _To | int, bulk: bool = ...) -> None: ... - async def aremove(self, *objs: _To | int, bulk: bool = ...) -> None: ... + def add( + self, + *objs: _To | int, + through_defaults: Mapping[str, Any] | None = ..., + ) -> None: ... + async def aadd( + self, + *objs: _To | int, + through_defaults: Mapping[str, Any] | None = ..., + ) -> None: ... + def remove(self, *objs: _To | int) -> None: ... + async def aremove(self, *objs: _To | int) -> None: ... + def clear(self) -> None: ... + async def aclear(self) -> None: ... def set( self, objs: QuerySet[_To] | Iterable[_To | int], *, - bulk: bool = ..., clear: bool = ..., - through_defaults: dict[str, Any] | None = ..., + through_defaults: Mapping[str, Any] | None = ..., ) -> None: ... async def aset( self, objs: QuerySet[_To] | Iterable[_To | int], *, - bulk: bool = ..., clear: bool = ..., - through_defaults: dict[str, Any] | None = ..., + through_defaults: Mapping[str, Any] | None = ..., ) -> None: ... - def clear(self) -> None: ... - async def aclear(self) -> None: ... + def create( + self, + *, + through_defaults: Mapping[str, Any] | None = ..., + **kwargs: Any, + ) -> _To: ... + async def acreate( + self, + *, + through_defaults: Mapping[str, Any] | None = ..., + **kwargs: Any, + ) -> _To: ... + def get_or_create( + self, + defaults: Mapping[str, Any] | None = ..., + through_defaults: Mapping[str, Any] | None = ..., + **kwargs: Any, + ) -> tuple[_To, bool]: ... + async def aget_or_create( + self, + defaults: Mapping[str, Any] | None = ..., + through_defaults: Mapping[str, Any] | None = ..., + **kwargs: Any, + ) -> tuple[_To, bool]: ... + def update_or_create( + self, + defaults: Mapping[str, Any] | None = ..., + create_defaults: Mapping[str, Any] | None = ..., + through_defaults: Mapping[str, Any] | None = ..., + **kwargs: Any, + ) -> tuple[_To, bool]: ... + async def aupdate_or_create( + self, + defaults: Mapping[str, Any] | None = ..., + create_defaults: Mapping[str, Any] | None = ..., + through_defaults: Mapping[str, Any] | None = ..., + **kwargs: Any, + ) -> tuple[_To, bool]: ... def __call__(self, *, manager: str) -> ManyRelatedManager[_To, _Through]: ... def create_forward_many_to_many_manager( diff --git a/django-stubs/db/models/lookups.pyi b/django-stubs/db/models/lookups.pyi index fe4ba2bc8..46c2d7cfd 100644 --- a/django-stubs/db/models/lookups.pyi +++ b/django-stubs/db/models/lookups.pyi @@ -1,5 +1,5 @@ -from collections.abc import Iterable -from typing import Any, Generic, Literal, Sequence, TypeVar +from collections.abc import Iterable, Sequence +from typing import Any, Generic, Literal, TypeVar from django.core.exceptions import EmptyResultSet from django.db.backends.base.base import BaseDatabaseWrapper diff --git a/django-stubs/db/models/sql/compiler.pyi b/django-stubs/db/models/sql/compiler.pyi index 93a54ac57..6ed8a95ce 100644 --- a/django-stubs/db/models/sql/compiler.pyi +++ b/django-stubs/db/models/sql/compiler.pyi @@ -34,7 +34,10 @@ class SQLCompiler: col_count: int | None def setup_query(self, with_col_aliases: bool = False) -> None: ... has_extra_select: Any - def pre_sql_setup(self, with_col_aliases: bool = False) -> tuple[ + def pre_sql_setup( + self, + with_col_aliases: bool = False, + ) -> tuple[ list[tuple[Expression, _AsSqlType, None]], list[tuple[Expression, tuple[str, _ParamsT, bool]]], list[_AsSqlType], @@ -48,7 +51,8 @@ class SQLCompiler: self, expressions: list[Expression], having: list[Expression] | tuple ) -> list[Expression]: ... def get_select( - self, with_col_aliases: bool = False + self, + with_col_aliases: bool = False, ) -> tuple[list[tuple[Expression, _AsSqlType, str | None]], dict[str, Any] | None, dict[str, int]]: ... def _order_by_pairs(self) -> None: ... def get_order_by(self) -> list[tuple[Expression, tuple[str, _ParamsT, bool]]]: ... diff --git a/django-stubs/forms/fields.pyi b/django-stubs/forms/fields.pyi index 31b93ea6f..98d32b724 100644 --- a/django-stubs/forms/fields.pyi +++ b/django-stubs/forms/fields.pyi @@ -2,7 +2,7 @@ import datetime from collections.abc import Collection, Iterator, Sequence from decimal import Decimal from re import Pattern -from typing import Any, Protocol, type_check_only +from typing import Any, ClassVar, Protocol, type_check_only from uuid import UUID from django.core.files import File @@ -32,7 +32,7 @@ class Field: widget: _ClassLevelWidgetT hidden_widget: type[Widget] default_validators: list[_ValidatorCallable] - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] empty_values: Sequence[Any] show_hidden_initial: bool help_text: _StrOrPromise @@ -547,7 +547,7 @@ class InvalidJSONInput(str): ... class JSONString(str): ... class JSONField(CharField): - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] widget: _ClassLevelWidgetT encoder: Any decoder: Any diff --git a/django-stubs/forms/formsets.pyi b/django-stubs/forms/formsets.pyi index 99c2eb937..b969ec576 100644 --- a/django-stubs/forms/formsets.pyi +++ b/django-stubs/forms/formsets.pyi @@ -1,5 +1,5 @@ from collections.abc import Iterator, Mapping, Sequence, Sized -from typing import Any, Generic, TypeVar +from typing import Any, ClassVar, Generic, TypeVar from django.db.models.fields import _ErrorMessagesDict from django.forms.forms import BaseForm, Form @@ -47,7 +47,7 @@ class BaseFormSet(Generic[_F], Sized, RenderableFormMixin): error_class: type[ErrorList] deletion_widget: MediaDefiningClass ordering_widget: MediaDefiningClass - default_error_messages: _ErrorMessagesDict + default_error_messages: ClassVar[_ErrorMessagesDict] template_name_div: str template_name_p: str template_name_table: str diff --git a/django-stubs/http/request.pyi b/django-stubs/http/request.pyi index 520f0ba5f..3805f1291 100644 --- a/django-stubs/http/request.pyi +++ b/django-stubs/http/request.pyi @@ -1,8 +1,8 @@ import datetime -from collections.abc import Iterable, Mapping +from collections.abc import Awaitable, Iterable, Mapping from io import BytesIO from re import Pattern -from typing import Any, Awaitable, BinaryIO, Callable, Literal, NoReturn, TypeVar, overload, type_check_only +from typing import Any, BinaryIO, Callable, Literal, NoReturn, TypeVar, overload, type_check_only from django.contrib.auth.base_user import _UserModel from django.contrib.auth.models import AnonymousUser diff --git a/django-stubs/template/base.pyi b/django-stubs/template/base.pyi index fd4d2dc4c..aaa99594c 100644 --- a/django-stubs/template/base.pyi +++ b/django-stubs/template/base.pyi @@ -2,7 +2,7 @@ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from enum import Enum from logging import Logger from re import Pattern -from typing import Any +from typing import Any, cast from django.template.context import Context as Context # Django: imported for backwards compatibility from django.template.engine import Engine @@ -26,10 +26,10 @@ tag_re: Pattern[str] logger: Logger class TokenType(Enum): - TEXT: int - VAR: int - BLOCK: int - COMMENT: int + TEXT = cast(int, ...) + VAR = cast(int, ...) + BLOCK = cast(int, ...) + COMMENT = cast(int, ...) class VariableDoesNotExist(Exception): msg: str diff --git a/django-stubs/urls/conf.pyi b/django-stubs/urls/conf.pyi index e278805de..f30edf1a4 100644 --- a/django-stubs/urls/conf.pyi +++ b/django-stubs/urls/conf.pyi @@ -1,6 +1,6 @@ -from collections.abc import Callable, Sequence +from collections.abc import Callable, Coroutine, Sequence from types import ModuleType -from typing import Any, Coroutine, overload +from typing import Any, overload from django.urls import URLPattern, URLResolver, _AnyURL from django.utils.functional import _StrOrPromise diff --git a/django-stubs/utils/html.pyi b/django-stubs/utils/html.pyi index b72f961f0..c5ce522e5 100644 --- a/django-stubs/utils/html.pyi +++ b/django-stubs/utils/html.pyi @@ -11,6 +11,7 @@ from django.utils.safestring import SafeData, SafeString VOID_ELEMENTS: frozenset[str] MAX_URL_LENGTH: int +MAX_STRIP_TAGS_DEPTH: int def escape(text: Any) -> SafeString: ... def escapejs(value: Any) -> SafeString: ... diff --git a/django-stubs/utils/timezone.pyi b/django-stubs/utils/timezone.pyi index d91ae571a..be51cf3ec 100644 --- a/django-stubs/utils/timezone.pyi +++ b/django-stubs/utils/timezone.pyi @@ -1,10 +1,9 @@ +import zoneinfo from contextlib import ContextDecorator from datetime import date, datetime, time, timedelta, timezone, tzinfo from types import TracebackType from typing import Literal, overload -import zoneinfo # type: ignore[import-not-found,unused-ignore] - def get_fixed_timezone(offset: timedelta | int) -> timezone: ... def get_default_timezone() -> zoneinfo.ZoneInfo: ... def get_default_timezone_name() -> str: ... diff --git a/django-stubs/views/generic/base.pyi b/django-stubs/views/generic/base.pyi index 05f703a12..464bfb05b 100644 --- a/django-stubs/views/generic/base.pyi +++ b/django-stubs/views/generic/base.pyi @@ -1,6 +1,6 @@ import logging -from collections.abc import Callable, Sequence -from typing import Any, Mapping +from collections.abc import Callable, Mapping, Sequence +from typing import Any from django.http.request import HttpRequest from django.http.response import HttpResponse, HttpResponseBase diff --git a/ext/django_stubs_ext/annotations.py b/ext/django_stubs_ext/annotations.py index e9b4aff0c..f4cf7380e 100644 --- a/ext/django_stubs_ext/annotations.py +++ b/ext/django_stubs_ext/annotations.py @@ -1,7 +1,7 @@ -from typing import Any, Generic, Mapping, TypeVar +from collections.abc import Mapping +from typing import Annotated, Any, Generic, TypeVar from django.db.models.base import Model -from typing_extensions import Annotated # Really, we would like to use TypedDict as a bound, but it's not possible _Annotations = TypeVar("_Annotations", covariant=True, bound=Mapping[str, Any]) diff --git a/ext/django_stubs_ext/db/models/__init__.py b/ext/django_stubs_ext/db/models/__init__.py index edb36dc8b..f201caeb5 100644 --- a/ext/django_stubs_ext/db/models/__init__.py +++ b/ext/django_stubs_ext/db/models/__init__.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import ClassVar, List, Literal, Sequence, Tuple, Union + from collections.abc import Sequence + from typing import ClassVar, Literal, Union from django.db.models import BaseConstraint, Index, OrderBy @@ -27,16 +28,16 @@ class TypedModelMeta: managed: ClassVar[bool] # default: True order_with_respect_to: ClassVar[str] ordering: ClassVar[Sequence[Union[str, OrderBy]]] - permissions: ClassVar[List[Tuple[str, str]]] + permissions: ClassVar[list[tuple[str, str]]] default_permissions: ClassVar[Sequence[str]] # default: ("add", "change", "delete", "view") proxy: ClassVar[bool] # default: False - required_db_features: ClassVar[List[str]] + required_db_features: ClassVar[list[str]] required_db_vendor: ClassVar[Literal["sqlite", "postgresql", "mysql", "oracle"]] select_on_save: ClassVar[bool] # default: False - indexes: ClassVar[List[Index]] + indexes: ClassVar[list[Index]] unique_together: ClassVar[Union[Sequence[Sequence[str]], Sequence[str]]] index_together: ClassVar[Union[Sequence[Sequence[str]], Sequence[str]]] # Deprecated in Django 4.2 - constraints: ClassVar[List[BaseConstraint]] + constraints: ClassVar[list[BaseConstraint]] verbose_name: ClassVar[StrOrPromise] verbose_name_plural: ClassVar[StrOrPromise] diff --git a/ext/django_stubs_ext/db/router.py b/ext/django_stubs_ext/db/router.py index 94a8d47dc..1ee2ff98e 100644 --- a/ext/django_stubs_ext/db/router.py +++ b/ext/django_stubs_ext/db/router.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from typing import Any @@ -14,11 +14,11 @@ class TypedDatabaseRouter: Django documentation: https://docs.djangoproject.com/en/stable/topics/db/multi-db/#automatic-database-routing """ - def db_for_read(self, model: Type[Model], **hints: Any) -> Optional[str]: ... + def db_for_read(self, model: type[Model], **hints: Any) -> Optional[str]: ... - def db_for_write(self, model: Type[Model], **hints: Any) -> Optional[str]: ... + def db_for_write(self, model: type[Model], **hints: Any) -> Optional[str]: ... - def allow_relation(self, obj1: Type[Model], obj2: Type[Model], **hints: Any) -> Optional[bool]: ... + def allow_relation(self, obj1: type[Model], obj2: type[Model], **hints: Any) -> Optional[bool]: ... def allow_migrate( self, db: str, app_label: str, model_name: Optional[str] = None, **hints: Any diff --git a/ext/django_stubs_ext/patch.py b/ext/django_stubs_ext/patch.py index 5d583767f..5ec048be2 100644 --- a/ext/django_stubs_ext/patch.py +++ b/ext/django_stubs_ext/patch.py @@ -1,5 +1,6 @@ import builtins -from typing import Any, Generic, Iterable, List, Optional, Tuple, Type, TypeVar +from collections.abc import Iterable +from typing import Any, Generic, Optional, TypeVar from django import VERSION from django.contrib.admin import ModelAdmin @@ -26,7 +27,7 @@ __all__ = ["monkeypatch"] _T = TypeVar("_T") -_VersionSpec = Tuple[int, int] +_VersionSpec = tuple[int, int] class MPGeneric(Generic[_T]): @@ -39,7 +40,7 @@ class MPGeneric(Generic[_T]): possible issues we may run into with this method. """ - def __init__(self, cls: Type[_T], version: Optional[_VersionSpec] = None) -> None: + def __init__(self, cls: type[_T], version: Optional[_VersionSpec] = None) -> None: """Set the data fields, basic constructor.""" self.version = version self.cls = cls @@ -52,7 +53,7 @@ def __repr__(self) -> str: # certain django classes need to be generic, but lack the __class_getitem__ dunder needed to # annotate them: https://github.com/typeddjango/django-stubs/issues/507 # this list stores them so `monkeypatch` can fix them when called -_need_generic: List[MPGeneric[Any]] = [ +_need_generic: list[MPGeneric[Any]] = [ MPGeneric(ModelAdmin), MPGeneric(SingleObjectMixin), MPGeneric(FormMixin), diff --git a/ext/setup.py b/ext/setup.py index a72ca5731..fc32035cc 100755 --- a/ext/setup.py +++ b/ext/setup.py @@ -13,7 +13,7 @@ # It's fine to skip django-stubs-ext releases, but when doing a release, update this to newest django-stubs version. setup( name="django-stubs-ext", - version="5.1.0", + version="5.1.1", description="Monkey-patching and extensions for django-stubs", long_description=readme, long_description_content_type="text/markdown", diff --git a/ext/tests/test_monkeypatching.py b/ext/tests/test_monkeypatching.py index 34a6fbdbb..dbfb24648 100644 --- a/ext/tests/test_monkeypatching.py +++ b/ext/tests/test_monkeypatching.py @@ -1,6 +1,7 @@ import builtins +from collections.abc import Iterable from contextlib import suppress -from typing import Iterable, List, Optional, Protocol +from typing import Optional, Protocol import pytest from _pytest.fixtures import FixtureRequest @@ -29,7 +30,7 @@ def make_generic_classes( request: FixtureRequest, monkeypatch: MonkeyPatch, ) -> _MakeGenericClasses: - _extra_classes: List[type] = [] + _extra_classes: list[type] = [] def fin() -> None: for el in _need_generic: diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py index e4f8c15d9..cfe412826 100644 --- a/mypy_django_plugin/config.py +++ b/mypy_django_plugin/config.py @@ -4,7 +4,7 @@ import textwrap from functools import partial from pathlib import Path -from typing import Any, Callable, Dict, NoReturn, Optional +from typing import Any, Callable, NoReturn, Optional if sys.version_info[:2] >= (3, 11): import tomllib @@ -79,7 +79,7 @@ def parse_toml_file(self, filepath: Path) -> None: toml_exit(COULD_NOT_LOAD_FILE) try: - config: Dict[str, Any] = data["tool"]["django-stubs"] + config: dict[str, Any] = data["tool"]["django-stubs"] except KeyError: toml_exit(MISSING_SECTION.format(section="tool.django-stubs")) @@ -123,7 +123,7 @@ def parse_ini_file(self, filepath: Path) -> None: except ValueError: exit_with_error(INVALID_BOOL_SETTING.format(key="strict_settings")) - def to_json(self, extra_data: Dict[str, Any]) -> Dict[str, Any]: + def to_json(self, extra_data: dict[str, Any]) -> dict[str, Any]: """We use this method to reset mypy cache via `report_config_data` hook.""" return { "django_settings_module": self.django_settings_module, diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 6b4ee12aa..b08e84558 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -1,23 +1,10 @@ import os import sys from collections import defaultdict +from collections.abc import Iterable, Iterator, Mapping, Sequence from contextlib import contextmanager from functools import cached_property -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Iterable, - Iterator, - Literal, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, - Union, -) +from typing import TYPE_CHECKING, Any, Literal, Optional, Union from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models @@ -64,7 +51,7 @@ def temp_environ() -> Iterator[None]: os.environ.update(environ) -def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]: +def initialize_django(settings_module: str) -> tuple["Apps", "LazySettings"]: with temp_environ(): os.environ["DJANGO_SETTINGS_MODULE"] = settings_module @@ -132,9 +119,9 @@ def __init__(self, django_settings_module: str) -> None: self.settings = settings @cached_property - def model_modules(self) -> Dict[str, Dict[str, Type[Model]]]: + def model_modules(self) -> dict[str, dict[str, type[Model]]]: """All modules that contain Django models.""" - modules: Dict[str, Dict[str, Type[Model]]] = defaultdict(dict) + modules: dict[str, dict[str, type[Model]]] = defaultdict(dict) for concrete_model_cls in self.apps_registry.get_models(include_auto_created=True, include_swapped=True): modules[concrete_model_cls.__module__][concrete_model_cls.__name__] = concrete_model_cls # collect abstract=True models @@ -143,28 +130,28 @@ def model_modules(self) -> Dict[str, Dict[str, Type[Model]]]: modules[model_cls.__module__][model_cls.__name__] = model_cls return modules - def get_model_class_by_fullname(self, fullname: str) -> Optional[Type[Model]]: + def get_model_class_by_fullname(self, fullname: str) -> Optional[type[Model]]: """Returns None if Model is abstract""" module, _, model_cls_name = fullname.rpartition(".") return self.model_modules.get(module, {}).get(model_cls_name) - def get_model_fields(self, model_cls: Type[Model]) -> Iterator["Field[Any, Any]"]: + def get_model_fields(self, model_cls: type[Model]) -> Iterator["Field[Any, Any]"]: for field in model_cls._meta.get_fields(): if isinstance(field, Field): yield field - def get_model_foreign_keys(self, model_cls: Type[Model]) -> Iterator["ForeignKey[Any, Any]"]: + def get_model_foreign_keys(self, model_cls: type[Model]) -> Iterator["ForeignKey[Any, Any]"]: for field in model_cls._meta.get_fields(): if isinstance(field, ForeignKey): yield field - def get_model_related_fields(self, model_cls: Type[Model]) -> Iterator["RelatedField[Any, Any]"]: + def get_model_related_fields(self, model_cls: type[Model]) -> Iterator["RelatedField[Any, Any]"]: """Get model forward relations""" for field in model_cls._meta.get_fields(): if isinstance(field, RelatedField): yield field - def get_model_relations(self, model_cls: Type[Model]) -> Iterator[ForeignObjectRel]: + def get_model_relations(self, model_cls: type[Model]) -> Iterator[ForeignObjectRel]: """Get model reverse relations""" for field in model_cls._meta.get_fields(): if isinstance(field, ForeignObjectRel): @@ -191,7 +178,7 @@ def get_field_lookup_exact_type( return helpers.get_private_descriptor_type(field_info, "_pyi_lookup_exact_type", is_nullable=field.null) def get_related_target_field( - self, related_model_cls: Type[Model], field: "ForeignKey[Any, Any]" + self, related_model_cls: type[Model], field: "ForeignKey[Any, Any]" ) -> "Optional[Field[Any, Any]]": # ForeginKey only supports one `to_fields` item (ForeignObject supports many) assert len(field.to_fields) == 1 @@ -204,14 +191,14 @@ def get_related_target_field( else: return self.get_primary_key_field(related_model_cls) - def get_primary_key_field(self, model_cls: Type[Model]) -> "Field[Any, Any]": + def get_primary_key_field(self, model_cls: type[Model]) -> "Field[Any, Any]": for field in model_cls._meta.get_fields(): if isinstance(field, Field): if field.primary_key: return field raise ValueError("No primary key defined") - def get_expected_types(self, api: TypeChecker, model_cls: Type[Model], *, method: str) -> Dict[str, MypyType]: + def get_expected_types(self, api: TypeChecker, model_cls: type[Model], *, method: str) -> dict[str, MypyType]: contenttypes_in_apps = self.apps_registry.is_installed("django.contrib.contenttypes") if contenttypes_in_apps: from django.contrib.contenttypes.fields import GenericForeignKey @@ -284,7 +271,7 @@ def get_expected_types(self, api: TypeChecker, model_cls: Type[Model], *, method return expected_types @cached_property - def all_registered_model_classes(self) -> Set[Type[models.Model]]: + def all_registered_model_classes(self) -> set[type[models.Model]]: model_classes = self.apps_registry.get_models() all_model_bases = set() @@ -380,7 +367,7 @@ def get_field_get_type( else: return helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=is_nullable) - def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", ForeignObjectRel]) -> Type[Model]: + def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", ForeignObjectRel]) -> type[Model]: if isinstance(field, RelatedField): related_model_cls = field.remote_field.model else: @@ -408,8 +395,8 @@ def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", For return related_model_cls def _resolve_field_from_parts( - self, field_parts: Iterable[str], model_cls: Type[Model] - ) -> Union["Field[Any, Any]", ForeignObjectRel]: + self, field_parts: Iterable[str], model_cls: type[Model] + ) -> tuple[Union["Field[Any, Any]", ForeignObjectRel], type[Model]]: currently_observed_model = model_cls field: Union[Field[Any, Any], ForeignObjectRel, GenericForeignKey, None] = None for field_part in field_parts: @@ -429,11 +416,11 @@ def _resolve_field_from_parts( # Guaranteed by `query.solve_lookup_type` before. assert isinstance(field, (Field, ForeignObjectRel)) - return field + return field, currently_observed_model def solve_lookup_type( - self, model_cls: Type[Model], lookup: str - ) -> Optional[Tuple[Sequence[str], Sequence[str], Union[Expression, Literal[False]]]]: + self, model_cls: type[Model], lookup: str + ) -> Optional[tuple[Sequence[str], Sequence[str], Union[Expression, Literal[False]]]]: query = Query(model_cls) if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None: # Primary key lookup when no primary key field is found, model is presumably @@ -466,18 +453,18 @@ def solve_lookup_type( return sub_query[0], entire_query_parts, sub_query[2] def resolve_lookup_into_field( - self, model_cls: Type[Model], lookup: str - ) -> Union["Field[Any, Any]", ForeignObjectRel, None]: + self, model_cls: type[Model], lookup: str + ) -> tuple[Union["Field[Any, Any]", ForeignObjectRel, None], type[Model]]: solved_lookup = self.solve_lookup_type(model_cls, lookup) if solved_lookup is None: - return None + return None, model_cls lookup_parts, field_parts, is_expression = solved_lookup if lookup_parts: raise LookupsAreUnsupported() return self._resolve_field_from_parts(field_parts, model_cls) def resolve_lookup_expected_type( - self, ctx: MethodContext, model_cls: Type[Model], lookup: str, model_instance: Instance + self, ctx: MethodContext, model_cls: type[Model], lookup: str, model_instance: Instance ) -> MypyType: try: solved_lookup = self.solve_lookup_type(model_cls, lookup) @@ -501,7 +488,7 @@ def resolve_lookup_expected_type( if is_expression: return AnyType(TypeOfAny.explicit) - field = self._resolve_field_from_parts(field_parts, model_cls) + field, _ = self._resolve_field_from_parts(field_parts, model_cls) lookup_cls = None if lookup_parts: diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index cea674d4d..c81fcfc9d 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -1,5 +1,6 @@ from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Literal, Optional, Set, Union, cast +from collections.abc import Iterable, Iterator +from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast from django.db.models.fields import Field from django.db.models.fields.related import RelatedField @@ -61,13 +62,13 @@ class DjangoTypeMetadata(TypedDict, total=False): is_abstract_model: bool is_annotated_model: bool from_queryset_manager: str - reverse_managers: Dict[str, str] - baseform_bases: Dict[str, int] - manager_bases: Dict[str, int] - model_bases: Dict[str, int] - queryset_bases: Dict[str, int] - m2m_throughs: Dict[str, str] - m2m_managers: Dict[str, str] + reverse_managers: dict[str, str] + baseform_bases: dict[str, int] + manager_bases: dict[str, int] + model_bases: dict[str, int] + queryset_bases: dict[str, int] + m2m_throughs: dict[str, str] + m2m_managers: dict[str, str] manager_to_model: str @@ -75,8 +76,8 @@ def get_django_metadata(model_info: TypeInfo) -> DjangoTypeMetadata: return cast(DjangoTypeMetadata, model_info.metadata.setdefault("django", {})) -def get_django_metadata_bases(model_info: TypeInfo, key: Literal["baseform_bases", "queryset_bases"]) -> Dict[str, int]: - return get_django_metadata(model_info).setdefault(key, cast(Dict[str, int], {})) +def get_django_metadata_bases(model_info: TypeInfo, key: Literal["baseform_bases", "queryset_bases"]) -> dict[str, int]: + return get_django_metadata(model_info).setdefault(key, cast(dict[str, int], {})) def get_reverse_manager_info( @@ -127,7 +128,7 @@ class IncompleteDefnException(Exception): pass -def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]: +def lookup_fully_qualified_sym(fullname: str, all_modules: dict[str, MypyFile]) -> Optional[SymbolTableNode]: if "." not in fullname: return None if "[" in fullname and "]" in fullname: @@ -140,7 +141,7 @@ def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) else: module, cls_name = fullname.rsplit(".", 1) - parent_classes: List[str] = [] + parent_classes: list[str] = [] while True: module_file = all_modules.get(module) if module_file: @@ -166,7 +167,7 @@ def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) return sym -def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]: +def lookup_fully_qualified_generic(name: str, all_modules: dict[str, MypyFile]) -> Optional[SymbolNode]: sym = lookup_fully_qualified_sym(name, all_modules) if sym is None: return None @@ -189,7 +190,7 @@ def lookup_class_typeinfo(api: TypeChecker, klass: Optional[type]) -> Optional[T return field_info -def reparametrize_instance(instance: Instance, new_args: List[MypyType]) -> Instance: +def reparametrize_instance(instance: Instance, new_args: list[MypyType]) -> Instance: return Instance(instance.type, args=new_args, line=instance.line, column=instance.column) @@ -301,7 +302,7 @@ def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo] return None -def create_type_info(name: str, module: str, bases: List[Instance]) -> TypeInfo: +def create_type_info(name: str, module: str, bases: list[Instance]) -> TypeInfo: # make new class expression classdef = ClassDef(name, Block([])) classdef.fullname = module + "." + name @@ -320,8 +321,8 @@ def create_type_info(name: str, module: str, bases: List[Instance]) -> TypeInfo: def add_new_class_for_module( module: MypyFile, name: str, - bases: List[Instance], - fields: Optional[Dict[str, MypyType]] = None, + bases: list[Instance], + fields: Optional[dict[str, MypyType]] = None, no_serialize: bool = False, ) -> TypeInfo: new_class_unique_name = checker.gen_unique_name(name, module.names) @@ -355,7 +356,7 @@ def get_current_module(api: TypeChecker) -> MypyFile: def make_oneoff_named_tuple( - api: TypeChecker, name: str, fields: "OrderedDict[str, MypyType]", extra_bases: Optional[List[Instance]] = None + api: TypeChecker, name: str, fields: "OrderedDict[str, MypyType]", extra_bases: Optional[list[Instance]] = None ) -> TupleType: current_module = get_current_module(api) if extra_bases is None: @@ -366,7 +367,7 @@ def make_oneoff_named_tuple( return TupleType(list(fields.values()), fallback=Instance(namedtuple_info, [])) -def make_tuple(api: "TypeChecker", fields: List[MypyType]) -> TupleType: +def make_tuple(api: "TypeChecker", fields: list[MypyType]) -> TupleType: # fallback for tuples is any builtins.tuple instance fallback = api.named_generic_type("builtins.tuple", [AnyType(TypeOfAny.special_form)]) return TupleType(fields, fallback=fallback) @@ -397,9 +398,9 @@ def convert_any_to_type(typ: MypyType, referred_to_type: MypyType) -> MypyType: def make_typeddict( api: Union[SemanticAnalyzer, CheckerPluginInterface], - fields: Dict[str, MypyType], - required_keys: Set[str], - readonly_keys: Set[str], + fields: dict[str, MypyType], + required_keys: set[str], + readonly_keys: set[str], ) -> TypedDictType: if isinstance(api, CheckerPluginInterface): fallback_type = api.named_generic_type("typing._TypedDict", []) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index fac0b0655..b8290361b 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -1,7 +1,7 @@ import itertools import sys from functools import cached_property, partial -from typing import Any, Callable, Dict, List, Optional, Tuple, Type +from typing import Any, Callable, Optional from mypy.build import PRI_MED, PRI_MYPY from mypy.modulefinder import mypy_path @@ -70,7 +70,7 @@ def __init__(self, options: Options) -> None: sys.path.extend(options.mypy_path) self.django_context = DjangoContext(self.plugin_config.django_settings_module) - def _get_current_queryset_bases(self) -> Dict[str, int]: + def _get_current_queryset_bases(self) -> dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.QUERYSET_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): bases = helpers.get_django_metadata_bases(model_sym.node, "queryset_bases") @@ -79,7 +79,7 @@ def _get_current_queryset_bases(self) -> Dict[str, int]: else: return {} - def _get_current_form_bases(self) -> Dict[str, int]: + def _get_current_form_bases(self) -> dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.BASEFORM_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): bases = helpers.get_django_metadata_bases(model_sym.node, "baseform_bases") @@ -96,11 +96,11 @@ def _get_typeinfo_or_none(self, class_name: str) -> Optional[TypeInfo]: return sym.node return None - def _new_dependency(self, module: str, priority: int = PRI_MYPY) -> Tuple[int, str, int]: + def _new_dependency(self, module: str, priority: int = PRI_MYPY) -> tuple[int, str, int]: fake_lineno = -1 return (priority, module, fake_lineno) - def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: + def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]: # for settings if file.fullname == "django.conf" and self.django_context.django_settings_module: return [self._new_dependency(self.django_context.django_settings_module, PRI_MED)] @@ -162,7 +162,7 @@ def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext return None @cached_property - def manager_and_queryset_method_hooks(self) -> Dict[str, Callable[[MethodContext], MypyType]]: + def manager_and_queryset_method_hooks(self) -> dict[str, Callable[[MethodContext], MypyType]]: typecheck_filtering_method = partial(orm_lookups.typecheck_queryset_filter, django_context=self.django_context) return { "values": partial(querysets.extract_proper_type_queryset_values, django_context=self.django_context), @@ -305,7 +305,7 @@ def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicCla return create_new_manager_class_from_from_queryset_method return None - def report_config_data(self, ctx: ReportConfigContext) -> Dict[str, Any]: + def report_config_data(self, ctx: ReportConfigContext) -> dict[str, Any]: # Cache would be cleared if any settings do change. extra_data = {} # In all places we use '_UserModel' alias as a type we want to clear cache if @@ -315,5 +315,5 @@ def report_config_data(self, ctx: ReportConfigContext) -> Dict[str, Any]: return self.plugin_config.to_json(extra_data) -def plugin(version: str) -> Type[NewSemanalDjangoPlugin]: +def plugin(version: str) -> type[NewSemanalDjangoPlugin]: return NewSemanalDjangoPlugin diff --git a/mypy_django_plugin/transformers/init_create.py b/mypy_django_plugin/transformers/init_create.py index e6923becc..f26baaf6d 100644 --- a/mypy_django_plugin/transformers/init_create.py +++ b/mypy_django_plugin/transformers/init_create.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Type, Union +from typing import Union from django.db.models.base import Model from mypy.plugin import FunctionContext, MethodContext @@ -10,8 +10,8 @@ def get_actual_types( - ctx: Union[MethodContext, FunctionContext], expected_keys: List[str] -) -> List[Tuple[str, MypyType]]: + ctx: Union[MethodContext, FunctionContext], expected_keys: list[str] +) -> list[tuple[str, MypyType]]: actual_types = [] # positionals for pos, (actual_name, actual_type) in enumerate(zip(ctx.arg_names[0], ctx.arg_types[0])): @@ -32,7 +32,7 @@ def get_actual_types( def typecheck_model_method( - ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext, model_cls: Type[Model], method: str + ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext, model_cls: type[Model], method: str ) -> MypyType: typechecker_api = helpers.get_typechecker_api(ctx) expected_types = django_context.get_expected_types(typechecker_api, model_cls, method=method) diff --git a/mypy_django_plugin/transformers/manytomany.py b/mypy_django_plugin/transformers/manytomany.py index 13c1d28a4..9f0904507 100644 --- a/mypy_django_plugin/transformers/manytomany.py +++ b/mypy_django_plugin/transformers/manytomany.py @@ -1,4 +1,4 @@ -from typing import NamedTuple, Optional, Tuple +from typing import NamedTuple, Optional from mypy.nodes import AssignmentStmt, NameExpr, Node, TypeInfo from mypy.plugin import FunctionContext, MethodContext @@ -121,7 +121,7 @@ def get_m2m_arguments( return M2MArguments(to=to, through=through) -def get_related_manager_and_model(ctx: MethodContext) -> Optional[Tuple[Instance, Instance, Instance]]: +def get_related_manager_and_model(ctx: MethodContext) -> Optional[tuple[Instance, Instance, Instance]]: """ Returns a 3-tuple consisting of: 1. A `ManyRelatedManager` instance diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 4425c0fca..99701f6df 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -1,6 +1,7 @@ from collections import deque +from collections.abc import Iterable from functools import cached_property -from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast +from typing import Any, Optional, Union, cast from django.db.models import Manager, Model from django.db.models.fields import DateField, DateTimeField, Field @@ -98,7 +99,7 @@ def add_new_node_to_model_class( self.model_classdef.info, name=name, sym_type=typ, no_serialize=no_serialize, is_classvar=is_classvar ) - def add_new_class_for_current_module(self, name: str, bases: List[Instance]) -> TypeInfo: + def add_new_class_for_current_module(self, name: str, bases: list[Instance]) -> TypeInfo: current_module = self.api.modules[self.model_classdef.info.module_name] new_class_info = helpers.add_new_class_for_module(current_module, name=name, bases=bases) return new_class_info @@ -109,7 +110,7 @@ def run(self) -> None: return self.run_with_model_cls(model_cls) - def get_generated_manager_mappings(self, base_manager_fullname: str) -> Dict[str, str]: + def get_generated_manager_mappings(self, base_manager_fullname: str) -> dict[str, str]: base_manager_info = self.lookup_typeinfo(base_manager_fullname) if base_manager_info is None or "from_queryset_managers" not in base_manager_info.metadata: return {} @@ -206,7 +207,7 @@ def get_or_create_queryset_with_any_fallback(self) -> Optional[TypeInfo]: return queryset_info - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: raise NotImplementedError(f"Implement this in subclass {self.__class__.__name__}") @@ -275,7 +276,7 @@ def run(self) -> None: class AddDefaultPrimaryKey(ModelClassInitializer): - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: auto_field = model_cls._meta.auto_field if auto_field: self.create_autofield( @@ -304,7 +305,7 @@ def create_autofield( class AddPrimaryKeyAlias(AddDefaultPrimaryKey): - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: # We also need to override existing `pk` definition from `stubs`: auto_field = model_cls._meta.pk if auto_field: @@ -316,7 +317,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: class AddRelatedModelsId(ModelClassInitializer): - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: for field in self.django_context.get_model_foreign_keys(model_cls): try: related_model_cls = self.django_context.get_field_related_model_cls(field) @@ -375,7 +376,7 @@ def reparametrize_dynamically_created_manager(self, manager_name: str, manager_i manager_type = helpers.fill_manager(manager_info, Instance(self.model_classdef.info, [])) self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True) - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: manager_info: Optional[TypeInfo] incomplete_manager_defs = set() @@ -474,7 +475,7 @@ class MyModel(models.Model): class AddDefaultManagerAttribute(ModelClassInitializer): - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: if "_default_manager" in self.model_classdef.info.names: return None @@ -608,7 +609,7 @@ def process_relation(self, relation: ForeignObjectRel) -> None: fullname=new_related_manager_info.fullname, ) - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: # add related managers etc. processing_incomplete = False for relation in self.django_context.get_model_relations(model_cls): @@ -622,7 +623,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: class AddExtraFieldMethods(ModelClassInitializer): - def run_with_model_cls(self, model_cls: Type[Model]) -> None: + def run_with_model_cls(self, model_cls: type[Model]) -> None: # get_FOO_display for choices for field in self.django_context.get_model_fields(model_cls): if field.choices: @@ -907,7 +908,7 @@ def resolve_many_to_many_arguments(self, call: CallExpr, /, context: Context) -> Inspect a 'ManyToManyField(...)' call to collect argument data on any 'to' and 'through' arguments. """ - look_for: Dict[str, Optional[Expression]] = {"to": None, "through": None} + look_for: dict[str, Optional[Expression]] = {"to": None, "through": None} # Look for 'to', being declared as the first positional argument if call.arg_kinds[0].is_positional(): look_for["to"] = call.args[0] @@ -1023,7 +1024,7 @@ def adjust_model_class(cls, ctx: ClassDefContext) -> None: return - def get_exception_bases(self, name: str) -> List[Instance]: + def get_exception_bases(self, name: str) -> list[Instance]: bases = [] for model_base in self.model_classdef.info.direct_base_classes(): exception_base_sym = model_base.names.get(name) diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index 2bc98d717..09173fa1a 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -1,5 +1,6 @@ from collections import OrderedDict -from typing import Dict, List, Optional, Sequence, Type +from collections.abc import Sequence +from typing import Optional from django.core.exceptions import FieldError from django.db.models.base import Model @@ -56,14 +57,14 @@ def determine_proper_manager_type(ctx: FunctionContext) -> MypyType: def get_field_type_from_lookup( ctx: MethodContext, django_context: DjangoContext, - model_cls: Type[Model], + model_cls: type[Model], *, method: str, lookup: str, silent_on_error: bool = False, ) -> Optional[MypyType]: try: - lookup_field = django_context.resolve_lookup_into_field(model_cls, lookup) + lookup_field, model_cls = django_context.resolve_lookup_into_field(model_cls, lookup) except FieldError as exc: if not silent_on_error: ctx.api.fail(exc.args[0], ctx.context) @@ -88,7 +89,7 @@ def get_field_type_from_lookup( def get_values_list_row_type( ctx: MethodContext, django_context: DjangoContext, - model_cls: Type[Model], + model_cls: type[Model], *, is_annotated: bool, flat: bool, @@ -210,7 +211,7 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context: return helpers.reparametrize_instance(default_return_type, [model_type, row_type]) -def gather_kwargs(ctx: MethodContext) -> Optional[Dict[str, MypyType]]: +def gather_kwargs(ctx: MethodContext) -> Optional[dict[str, MypyType]]: num_args = len(ctx.arg_kinds) kwargs = {} named = (ARG_NAMED, ARG_NAMED_OPT) @@ -241,7 +242,7 @@ def extract_proper_type_queryset_annotate(ctx: MethodContext, django_context: Dj api = helpers.get_typechecker_api(ctx) - field_types: Optional[Dict[str, MypyType]] = None + field_types: Optional[dict[str, MypyType]] = None kwargs = gather_kwargs(ctx) if kwargs: # For now, we don't try to resolve the output_field of the field would be, but use Any. @@ -310,7 +311,7 @@ def extract_proper_type_queryset_annotate(ctx: MethodContext, django_context: Dj return helpers.reparametrize_instance(default_return_type, [annotated_type, row_type]) -def resolve_field_lookups(lookup_exprs: Sequence[Expression], django_context: DjangoContext) -> Optional[List[str]]: +def resolve_field_lookups(lookup_exprs: Sequence[Expression], django_context: DjangoContext) -> Optional[list[str]]: field_lookups = [] for field_lookup_expr in lookup_exprs: field_lookup = helpers.resolve_string_attribute_value(field_lookup_expr, django_context) diff --git a/pyproject.toml b/pyproject.toml index a8fccfbc4..c278234aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,10 @@ -[tool.black] -target-version = ['py38'] -line-length = 120 -include = '\.pyi?$' - [tool.codespell] ignore-words-list = "aadd,acount,nam,asend" [tool.ruff] # Adds to default excludes: https://ruff.rs/docs/settings/#exclude -extend-exclude = [ - ".*/", - "django-source", - "stubgen", - "out", -] line-length = 120 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] # See Rules in Ruff documentation: https://beta.ruff.rs/docs/rules/ diff --git a/requirements.txt b/requirements.txt index 321806964..33f21efe5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,16 @@ # Dev tools: -black==24.8.0 -pre-commit==3.5.0; python_version < '3.9' -pre-commit==4.0.1; python_version >= '3.9' -pytest==8.3.3 +pre-commit==4.0.1 +pytest==8.3.4 pytest-mypy-plugins==3.1.2 pytest-shard==0.1.2 # Django deps: psycopg2-binary Django==4.2.16; python_version < '3.10' -Django==5.1.2; python_version >= '3.10' +Django==5.1.4; python_version >= '3.10' -e ./ext -e .[redis,compatible-mypy,oracle] # Overrides: mypy==1.13.0 -pyright==1.1.386 +pyright==1.1.390 diff --git a/scripts/stubtest/allowlist.txt b/scripts/stubtest/allowlist.txt index 60a68c95b..ba056fd99 100644 --- a/scripts/stubtest/allowlist.txt +++ b/scripts/stubtest/allowlist.txt @@ -160,6 +160,8 @@ django.contrib.contenttypes.fields.ReverseGenericManyToOneDescriptor.related_man django.contrib.gis.db.backends.base.operations.BaseSpatialOperations.select_extent django.contrib.gis.db.backends.mysql.features.DatabaseFeatures.django_test_skips django.contrib.gis.db.backends.mysql.features.DatabaseFeatures.supports_geometry_field_unique_index +django.contrib.gis.geoip2.GeoIP2.is_country +django.contrib.gis.geoip2.GeoIP2.is_city django.contrib.gis.db.backends.mysql.operations.MySQLOperations.from_text django.contrib.gis.db.backends.mysql.operations.MySQLOperations.gis_operators django.contrib.gis.db.backends.mysql.operations.MySQLOperations.mariadb diff --git a/setup.py b/setup.py index 8b4ad392c..37c046639 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ #!/usr/bin/env python import os -from typing import List from setuptools import find_packages, setup -def find_stub_files(name: str) -> List[str]: +def find_stub_files(name: str) -> list[str]: result = [] for root, _dirs, files in os.walk(name): for file in files: @@ -23,7 +22,7 @@ def find_stub_files(name: str) -> List[str]: dependencies = [ "django", "asgiref", - "django-stubs-ext>=5.1.0", + "django-stubs-ext>=5.1.1", "tomli; python_version < '3.11'", # Types: "typing-extensions>=4.11.0", @@ -39,7 +38,7 @@ def find_stub_files(name: str) -> List[str]: setup( name="django-stubs", - version="5.1.0", + version="5.1.1", description="Mypy stubs for Django", long_description=readme, long_description_content_type="text/markdown", diff --git a/tests/assert_type/db/models/check_enums.py b/tests/assert_type/db/models/check_enums.py index 2c88d76f5..2d90adfbe 100644 --- a/tests/assert_type/db/models/check_enums.py +++ b/tests/assert_type/db/models/check_enums.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Tuple +from typing import Literal from django.db.models import IntegerChoices, TextChoices from django.utils.translation import gettext_lazy as _ @@ -40,7 +40,7 @@ class MyTextChoices(TextChoices): # Assertions related to the metaclass: -assert_type(MyIntegerChoices.values, List[int]) -assert_type(MyIntegerChoices.choices, List[Tuple[int, str]]) -assert_type(MyTextChoices.values, List[str]) -assert_type(MyTextChoices.choices, List[Tuple[str, str]]) +assert_type(MyIntegerChoices.values, list[int]) +assert_type(MyIntegerChoices.choices, list[tuple[int, str]]) +assert_type(MyTextChoices.values, list[str]) +assert_type(MyTextChoices.choices, list[tuple[str, str]]) diff --git a/tests/assert_type/urls/test_conf.py b/tests/assert_type/urls/test_conf.py index aa7273502..2c88f7f32 100644 --- a/tests/assert_type/urls/test_conf.py +++ b/tests/assert_type/urls/test_conf.py @@ -1,5 +1,3 @@ -from typing import List, Tuple - from django.conf.urls.i18n import urlpatterns as i18n_urlpatterns from django.contrib import admin from django.contrib.auth.views import LoginView @@ -11,11 +9,11 @@ from typing_extensions import assert_type # Test 'path' accepts mix of pattern and resolver object -include1: Tuple[List[_AnyURL], None, None] = ([], None, None) +include1: tuple[list[_AnyURL], None, None] = ([], None, None) assert_type(path("test/", include1), URLResolver) # Test 'path' accepts pattern resolver union subset -include2: Tuple[List[URLPattern], None, None] = ([], None, None) +include2: tuple[list[URLPattern], None, None] = ([], None, None) assert_type(path("test/", include2), URLResolver) # Test 'path' @@ -35,7 +33,7 @@ async def v2() -> HttpResponse: ... assert_type(re_path("^v2/", v2), URLPattern) # Test 'include' -patterns1: List[_AnyURL] = [] +patterns1: list[_AnyURL] = [] assert_type(re_path(_("^foo/"), include(patterns1)), URLResolver) assert_type(re_path("^foo/", include(patterns1, namespace="foo")), URLResolver) assert_type(re_path("^foo/", include((patterns1, "foo"), namespace="foo")), URLResolver) diff --git a/tests/assert_type/views/generic.py b/tests/assert_type/views/generic.py index 6147f02d7..9886c2235 100644 --- a/tests/assert_type/views/generic.py +++ b/tests/assert_type/views/generic.py @@ -1,4 +1,4 @@ -from typing import Optional, Type +from typing import Optional from django.db import models from django.views.generic.detail import SingleObjectMixin @@ -13,7 +13,7 @@ class MyDetailView(SingleObjectMixin[MyModel]): ... detail_view = MyDetailView() -assert_type(detail_view.model, Type[MyModel]) +assert_type(detail_view.model, type[MyModel]) assert_type(detail_view.queryset, Optional[models.QuerySet[MyModel, MyModel]]) assert_type(detail_view.get_context_object_name(MyModel()), str) assert_type(detail_view.get_context_object_name(1), Optional[str]) @@ -23,7 +23,7 @@ class MyListView(ListView[MyModel]): ... list_view = MyListView() -assert_type(list_view.model, Optional[Type[MyModel]]) +assert_type(list_view.model, Optional[type[MyModel]]) assert_type(list_view.queryset, Optional[models.QuerySet[MyModel, MyModel]]) assert_type(list_view.get_context_object_name(models.QuerySet[MyModel]()), str) assert_type(list_view.get_context_object_name(MyModel()), Optional[str]) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index a5dfaac3a..5df06823f 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -1,8 +1,9 @@ import os import tempfile import uuid +from collections.abc import Generator from contextlib import contextmanager -from typing import Any, Generator, List, Optional +from typing import Any, Optional from unittest import mock import pytest @@ -65,7 +66,7 @@ def write_to_file(file_contents: str, suffix: Optional[str] = None) -> Generator ), ], ) -def test_misconfiguration_handling(capsys: Any, config_file_contents: List[str], message_part: str) -> None: +def test_misconfiguration_handling(capsys: Any, config_file_contents: list[str], message_part: str) -> None: """Invalid configuration raises `SystemExit` with a precise error message.""" contents = "\n".join(config_file_contents).expandtabs(4) with write_to_file(contents) as filename: diff --git a/tests/typecheck/fields/test_related.yml b/tests/typecheck/fields/test_related.yml index 5ffb2eef9..bb911d6f3 100644 --- a/tests/typecheck/fields/test_related.yml +++ b/tests/typecheck/fields/test_related.yml @@ -1226,7 +1226,7 @@ main:26: note: Revealed type is "myapp.models.MyModel_auto_through" main:28: note: Revealed type is "builtins.str" main:29: note: Revealed type is "builtins.str" - main:30: note: Revealed type is "def (*objs: Union[myapp.models.MyModel, builtins.int], bulk: builtins.bool =, through_defaults: Union[builtins.dict[builtins.str, Any], None] =)" + main:30: note: Revealed type is "def (*objs: Union[myapp.models.MyModel, builtins.int], through_defaults: Union[typing.Mapping[builtins.str, Any], None] =)" main:32: note: Revealed type is "django.db.models.manager.Manager[myapp.models.MyModel_auto_through]" main:33: note: Revealed type is "django.db.models.manager.Manager[myapp.models.MyModel_auto_through]" main:35: note: Revealed type is "myapp.models.Other_ManyRelatedManager[myapp.models.MyModel_auto_through]" diff --git a/tests/typecheck/managers/querysets/test_values_list.yml b/tests/typecheck/managers/querysets/test_values_list.yml index b20935b08..9315babf6 100644 --- a/tests/typecheck/managers/querysets/test_values_list.yml +++ b/tests/typecheck/managers/querysets/test_values_list.yml @@ -322,3 +322,21 @@ class Blog(models.Model): num_posts = models.IntegerField() text = models.CharField(max_length=100, blank=True) +- case: handles_field_with_same_name_on_other_model + main: | + from myapp.models import A + reveal_type(A.objects.values_list("name", "b__name").get()) # N: Revealed type is "Tuple[builtins.int, builtins.str]" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class B(models.Model): + name = models.CharField() + + class A(models.Model): + b = models.ForeignKey(B, on_delete=models.CASCADE) + name = models.IntegerField()