diff --git a/README.md b/README.md index b6151b360..83ecef99d 100644 --- a/README.md +++ b/README.md @@ -209,13 +209,7 @@ This happens because these Django classes do not support [`__class_getitem__`](h ### How can I create a HttpRequest that's guaranteed to have an authenticated user? -Django's built in [`HttpRequest`](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest) has the attribute `user` that resolves to the type - -```python -Union[User, AnonymousUser] -``` - -where `User` is the user model specified by the `AUTH_USER_MODEL` setting. +Django's built in [`HttpRequest`](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest) has the attribute `user` that resolves to the type `User | AnonymousUser` where `User` is the user model specified by the `AUTH_USER_MODEL` setting. If you want a `HttpRequest` that you can type-annotate with where you know that the user is authenticated you can subclass the normal `HttpRequest` class like so: diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index 0d4c54edb..0225484b9 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -66,12 +66,12 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): .. code:: python - from typing import Generic, Union + from typing import Generic class Model(object): ... - _SetType = Union[int, float] # You can assign ints and floats + _SetType = int | float # You can assign ints and floats _GetType = int # access type is always `int` class IntField(object): @@ -100,7 +100,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): example.count = 1.5 # ok example.count = 'a' # Incompatible types in assignment - # (expression has type "str", variable has type "Union[int, float]") + # (expression has type "str", variable has type "int | float") Notice, that this is not magic. This is how descriptors work with ``mypy``. diff --git a/django-stubs/forms/fields.pyi b/django-stubs/forms/fields.pyi index 9138a22a9..b4e1723c5 100644 --- a/django-stubs/forms/fields.pyi +++ b/django-stubs/forms/fields.pyi @@ -18,7 +18,7 @@ from typing_extensions import Self, override # Problem: attribute `widget` is always of type `Widget` after field instantiation. # However, on class level it can be set to `Type[Widget]` too. -# If we annotate it as `Union[Widget, Type[Widget]]`, every code that uses field +# If we annotate it as `Widget | Type[Widget]`, every code that uses field # instances will not typecheck. # If we annotate it as `Widget`, any widget subclasses that do e.g. # `widget = Select` will not typecheck. diff --git a/django-stubs/utils/formats.pyi b/django-stubs/utils/formats.pyi index 22fdf81e7..f8a36cfe2 100644 --- a/django-stubs/utils/formats.pyi +++ b/django-stubs/utils/formats.pyi @@ -31,7 +31,7 @@ def number_format( _T = TypeVar("_T") # Mypy considers this invalid (overlapping signatures), but thanks to implementation -# details it works as expected (all values from Union are `localize`d to str, +# details it works as expected (all values from the union are `localize`d to str, # while type of others is preserved) @overload def localize(value: builtin_datetime | date | time | Decimal | float | str, use_l10n: bool | None = None) -> str: ... diff --git a/django-stubs/utils/functional.pyi b/django-stubs/utils/functional.pyi index c1fd34b97..b8463c7e3 100644 --- a/django-stubs/utils/functional.pyi +++ b/django-stubs/utils/functional.pyi @@ -108,7 +108,7 @@ class _Getter(Protocol[_Get]): # noqa: PYI046 """ Type fake to declare some read-only properties (until `property` builtin is generic). - We can use something like `Union[_Getter[str], str]` in base class to avoid errors + We can use something like `_Getter[str] | str` in base class to avoid errors when redefining attribute with property or property with attribute. """ diff --git a/ext/django_stubs_ext/__init__.py b/ext/django_stubs_ext/__init__.py index 6e6c793aa..8b765dfce 100644 --- a/ext/django_stubs_ext/__init__.py +++ b/ext/django_stubs_ext/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .aliases import QuerySetAny as QuerySetAny from .aliases import StrOrPromise, StrPromise from .aliases import ValuesQuerySet as ValuesQuerySet diff --git a/ext/django_stubs_ext/aliases.py b/ext/django_stubs_ext/aliases.py index 8d582cf92..3a655ca3a 100644 --- a/ext/django_stubs_ext/aliases.py +++ b/ext/django_stubs_ext/aliases.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing if typing.TYPE_CHECKING: diff --git a/ext/django_stubs_ext/annotations.py b/ext/django_stubs_ext/annotations.py index f4cf7380e..c96aa0650 100644 --- a/ext/django_stubs_ext/annotations.py +++ b/ext/django_stubs_ext/annotations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Mapping from typing import Annotated, Any, Generic, TypeVar diff --git a/ext/django_stubs_ext/db/models/__init__.py b/ext/django_stubs_ext/db/models/__init__.py index 3f3d2459e..7e3530c48 100644 --- a/ext/django_stubs_ext/db/models/__init__.py +++ b/ext/django_stubs_ext/db/models/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/ext/django_stubs_ext/db/models/manager.py b/ext/django_stubs_ext/db/models/manager.py index 4e7684b08..5c9208f36 100644 --- a/ext/django_stubs_ext/db/models/manager.py +++ b/ext/django_stubs_ext/db/models/manager.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING # Re-export stubs-only classes RelatedManger and ManyRelatedManager. diff --git a/ext/django_stubs_ext/db/router.py b/ext/django_stubs_ext/db/router.py index 1f0b97bd0..bf319bc3a 100644 --- a/ext/django_stubs_ext/db/router.py +++ b/ext/django_stubs_ext/db/router.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/ext/django_stubs_ext/patch.py b/ext/django_stubs_ext/patch.py index 0b1c68efd..10c265559 100644 --- a/ext/django_stubs_ext/patch.py +++ b/ext/django_stubs_ext/patch.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import builtins import logging -from collections.abc import Iterable -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from django import VERSION from django.contrib.admin import ModelAdmin @@ -33,6 +34,9 @@ from django.views.generic.list import MultipleObjectMixin from typing_extensions import override +if TYPE_CHECKING: + from collections.abc import Iterable + __all__ = ["monkeypatch"] logger = logging.getLogger(__name__) diff --git a/ext/django_stubs_ext/settings.py b/ext/django_stubs_ext/settings.py index c01e326ca..71d117435 100644 --- a/ext/django_stubs_ext/settings.py +++ b/ext/django_stubs_ext/settings.py @@ -1,8 +1,12 @@ -from pathlib import Path -from typing import Any, TypedDict, type_check_only +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, TypedDict, type_check_only from typing_extensions import NotRequired +if TYPE_CHECKING: + from pathlib import Path + @type_check_only class TemplatesSetting(TypedDict): diff --git a/ext/django_stubs_ext/types.py b/ext/django_stubs_ext/types.py index d98d39fcd..9295cfa3c 100644 --- a/ext/django_stubs_ext/types.py +++ b/ext/django_stubs_ext/types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any, Protocol from typing_extensions import override diff --git a/ext/tests/test_aliases.py b/ext/tests/test_aliases.py index 015dad4ea..dcf42ca45 100644 --- a/ext/tests/test_aliases.py +++ b/ext/tests/test_aliases.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any from django_stubs_ext import ValuesQuerySet diff --git a/ext/tests/test_monkeypatching.py b/ext/tests/test_monkeypatching.py index cd5dddee6..3d73de8f2 100644 --- a/ext/tests/test_monkeypatching.py +++ b/ext/tests/test_monkeypatching.py @@ -1,11 +1,10 @@ +from __future__ import annotations + import builtins -from collections.abc import Iterable from contextlib import suppress -from typing import Protocol +from typing import TYPE_CHECKING, Protocol import pytest -from _pytest.fixtures import FixtureRequest -from _pytest.monkeypatch import MonkeyPatch from django.db.models import Model from django.forms.models import ModelForm @@ -13,6 +12,12 @@ from django_stubs_ext import patch from django_stubs_ext.patch import _need_generic, _VersionSpec +if TYPE_CHECKING: + from collections.abc import Iterable + + from _pytest.fixtures import FixtureRequest + from _pytest.monkeypatch import MonkeyPatch + class _MakeGenericClasses(Protocol): """Used to represent a type of ``make_generic_classes`` fixture.""" diff --git a/mypy.ini b/mypy.ini index a8586ff3c..619149f54 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,7 +8,9 @@ incremental = true # TODO: add type args to all generics disallow_any_generics = false strict = true +allow_redefinition_new = true local_partial_types = true +strict_equality_for_none = true warn_unreachable = true disable_error_code = empty-body @@ -25,6 +27,7 @@ enable_error_code = unimported-reveal, unused-awaitable, +fixed_format_cache = true show_traceback = true plugins = diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py index ac1c841f3..9d2b0bd27 100644 --- a/mypy_django_plugin/config.py +++ b/mypy_django_plugin/config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import configparser import os import sys diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index ed02a8e0a..533d04b08 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -1,24 +1,21 @@ +from __future__ import annotations + 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, Literal, Union +from typing import TYPE_CHECKING, Any, Literal from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models from django.db.models.base import Model from django.db.models.constants import LOOKUP_SEP -from django.db.models.expressions import Expression from django.db.models.fields import AutoField, CharField, Field from django.db.models.fields.related import ForeignKey, RelatedField from django.db.models.fields.reverse_related import ForeignObjectRel from django.db.models.lookups import Exact from django.db.models.sql.query import Query -from mypy.checker import TypeChecker -from mypy.nodes import TypeInfo -from mypy.plugin import MethodContext from mypy.typeanal import make_optional_type from mypy.types import AnyType, Instance, ProperType, TypeOfAny, UnionType, get_proper_type from mypy.types import Type as MypyType @@ -36,9 +33,15 @@ class ArrayField: # type: ignore[no-redef] if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Mapping, Sequence + from django.apps.registry import Apps from django.conf import LazySettings + from django.db.models.expressions import Expression from django.db.models.options import _AnyField + from mypy.checker import TypeChecker + from mypy.nodes import TypeInfo + from mypy.plugin import MethodContext @contextmanager @@ -52,7 +55,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 @@ -133,17 +136,17 @@ def get_model_class_by_fullname(self, fullname: str) -> type[Model] | None: 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): @@ -155,9 +158,7 @@ def get_model_relations(self, model_cls: type[Model]) -> Iterator[ForeignObjectR if isinstance(field, ForeignObjectRel): yield field - def get_field_lookup_exact_type( - self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel] - ) -> MypyType: + def get_field_lookup_exact_type(self, api: TypeChecker, field: Field[Any, Any] | ForeignObjectRel) -> MypyType: if isinstance(field, RelatedField | ForeignObjectRel): related_model_cls = self.get_field_related_model_cls(field) rel_model_info = helpers.lookup_class_typeinfo(api, related_model_cls) @@ -176,8 +177,8 @@ 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]" - ) -> "Field[Any, Any] | None": + self, related_model_cls: type[Model], field: ForeignKey[Any, Any] + ) -> Field[Any, Any] | None: # ForeignKey only supports one `to_fields` item (ForeignObject supports many) assert len(field.to_fields) == 1 to_field_name = field.to_fields[0] @@ -188,7 +189,7 @@ def get_related_target_field( return rel_field 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: @@ -289,7 +290,7 @@ def model_class_fullnames_by_label(self) -> Mapping[str, str]: if klass is not models.Model } - def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel], method: str | None) -> bool: + def get_field_nullability(self, field: Field[Any, Any] | ForeignObjectRel, method: str | None) -> bool: if method in ("values", "values_list"): return field.null @@ -307,7 +308,7 @@ def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel return nullable def get_field_set_type( - self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str + self, api: TypeChecker, field: Field[Any, Any] | ForeignObjectRel, *, method: str ) -> MypyType: """Get a type of __set__ for this specific Django field.""" target_field = field @@ -335,7 +336,7 @@ def get_field_get_type( self, api: TypeChecker, model_info: TypeInfo | None, - field: Union["Field[Any, Any]", ForeignObjectRel], + field: Field[Any, Any] | ForeignObjectRel, *, method: str, ) -> MypyType: @@ -365,15 +366,12 @@ def get_field_get_type( return Instance(model_info, []) 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: RelatedField[Any, Any] | ForeignObjectRel) -> type[Model]: if isinstance(field, RelatedField): related_model_cls = field.remote_field.model else: related_model_cls = field.field.model - if related_model_cls is None: - raise UnregisteredModelError - if isinstance(related_model_cls, str): if related_model_cls == "self": # type: ignore[unreachable] # same model @@ -394,7 +392,7 @@ def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", For def _resolve_field_from_parts( self, field_parts: Iterable[str], model_cls: type[Model] - ) -> tuple[Union["Field[Any, Any]", ForeignObjectRel], type[Model]]: + ) -> tuple[Field[Any, Any] | ForeignObjectRel, type[Model]]: currently_observed_model = model_cls field: _AnyField | None = None for field_part in field_parts: @@ -420,7 +418,7 @@ def solve_lookup_type( self, model_cls: type[Model], lookup: str ) -> tuple[Sequence[str], Sequence[str], Expression | Literal[False]] | None: query = Query(model_cls) - if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None: + if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None: # type: ignore[comparison-overlap] # Primary key lookup when no primary key field is found, model is presumably # abstract and we can't say anything about 'pk'. return None @@ -452,7 +450,7 @@ def solve_lookup_type( def resolve_lookup_into_field( self, model_cls: type[Model], lookup: str - ) -> tuple[Union["Field[Any, Any]", ForeignObjectRel, None], type[Model]]: + ) -> tuple[Field[Any, Any] | ForeignObjectRel | None, type[Model]]: solved_lookup = self.solve_lookup_type(model_cls, lookup) if solved_lookup is None: return None, model_cls @@ -462,7 +460,7 @@ def resolve_lookup_into_field( return self._resolve_field_from_parts(field_parts, model_cls) def _resolve_lookup_type_from_lookup_class( - self, ctx: MethodContext, lookup_cls: type, field: Union["Field[Any, Any]", ForeignObjectRel] | None = None + self, ctx: MethodContext, lookup_cls: type, field: Field[Any, Any] | ForeignObjectRel | None = None ) -> MypyType | None: """Resolve the expected type for a lookup class (used both for regular fields and annotated fields) diff --git a/mypy_django_plugin/errorcodes.py b/mypy_django_plugin/errorcodes.py index ebb06f0fa..34a3ffc0f 100644 --- a/mypy_django_plugin/errorcodes.py +++ b/mypy_django_plugin/errorcodes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from mypy.errorcodes import ErrorCode MANAGER_MISSING = ErrorCode("django-manager-missing", "Couldn't resolve manager for model", "Django") diff --git a/mypy_django_plugin/exceptions.py b/mypy_django_plugin/exceptions.py index f0a55a774..390197019 100644 --- a/mypy_django_plugin/exceptions.py +++ b/mypy_django_plugin/exceptions.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class UnregisteredModelError(Exception): """The requested model is not registered""" diff --git a/mypy_django_plugin/lib/fullnames.py b/mypy_django_plugin/lib/fullnames.py index f12bb03cb..17549d0aa 100644 --- a/mypy_django_plugin/lib/fullnames.py +++ b/mypy_django_plugin/lib/fullnames.py @@ -1,3 +1,5 @@ +from __future__ import annotations + ABSTRACT_BASE_USER_MODEL_FULLNAME = "django.contrib.auth.base_user.AbstractBaseUser" ABSTRACT_USER_MODEL_FULLNAME = "django.contrib.auth.models.AbstractUser" PERMISSION_MIXIN_CLASS_FULLNAME = "django.contrib.auth.models.PermissionsMixin" diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 3109281ae..a44034781 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -1,7 +1,7 @@ -from collections.abc import Iterable, Iterator +from __future__ import annotations + from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypedDict, cast -from django.db.models.base import Model from django.db.models.fields.related import RelatedField from django.db.models.fields.reverse_related import ForeignObjectRel from mypy import checker @@ -59,6 +59,9 @@ from mypy_django_plugin.lib import fullnames if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + from django.db.models.base import Model from django.db.models.fields import Field from mypy_django_plugin.django.context import DjangoContext @@ -211,7 +214,7 @@ def info(self) -> TypeInfo: return self.typ.type @classmethod - def from_model_type(cls, model_type: Instance, django_context: "DjangoContext") -> Self | None: + def from_model_type(cls, model_type: Instance, django_context: DjangoContext) -> Self | None: model_info = model_type.type is_annotated = is_annotated_model(model_info) @@ -249,7 +252,7 @@ def extract_model_type_from_queryset(queryset_type: Instance, api: TypeChecker) def get_model_info_from_qs_ctx( ctx: MethodContext, - django_context: "DjangoContext", + django_context: DjangoContext, ) -> DjangoModel | None: """ Extract DjangoModel details from a queryset/manager `MethodContext` @@ -411,7 +414,7 @@ def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is return AnyType(TypeOfAny.explicit) -def get_field_lookup_exact_type(api: TypeChecker, field: "Field[Any, Any]") -> MypyType: +def get_field_lookup_exact_type(api: TypeChecker, field: Field[Any, Any]) -> MypyType: if isinstance(field, RelatedField | ForeignObjectRel): # Not using field.related_model because that may have str value "self" lookup_type_class = field.remote_field.model @@ -487,7 +490,7 @@ def get_current_module(api: TypeChecker) -> MypyFile: def make_oneoff_named_tuple( - api: TypeChecker, name: str, fields: "dict[str, MypyType]", extra_bases: list[Instance] | None = None + api: TypeChecker, name: str, fields: dict[str, MypyType], extra_bases: list[Instance] | None = None ) -> TupleType: current_module = get_current_module(api) if extra_bases is None: @@ -498,7 +501,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) @@ -545,7 +548,7 @@ def make_typeddict( ) -def resolve_string_attribute_value(attr_expr: Expression, django_context: "DjangoContext") -> str | None: +def resolve_string_attribute_value(attr_expr: Expression, django_context: DjangoContext) -> str | None: if isinstance(attr_expr, StrExpr): return attr_expr.value @@ -630,7 +633,7 @@ def is_abstract_model(model: TypeInfo) -> bool: def resolve_lazy_reference( - reference: str, *, api: TypeChecker | SemanticAnalyzer, django_context: "DjangoContext", ctx: Context + reference: str, *, api: TypeChecker | SemanticAnalyzer, django_context: DjangoContext, ctx: Context ) -> TypeInfo | None: """ Attempts to resolve a lazy reference(e.g. ".") to a @@ -665,7 +668,7 @@ def get_model_from_expression( *, self_model: TypeInfo, api: TypeChecker | SemanticAnalyzer, - django_context: "DjangoContext", + django_context: DjangoContext, ) -> Instance | None: """ Attempts to resolve an expression to a 'TypeInfo' instance. Any lazy reference diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 05ef0f361..e8484a8bf 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -1,13 +1,13 @@ +from __future__ import annotations + import itertools import sys -from collections.abc import Callable from functools import cached_property, partial -from typing import Any +from typing import TYPE_CHECKING, Any from mypy.build import PRI_MED, PRI_MYPY from mypy.modulefinder import mypy_path from mypy.nodes import MypyFile, TypeInfo -from mypy.options import Options from mypy.plugin import ( AnalyzeTypeContext, AttributeContext, @@ -18,7 +18,6 @@ Plugin, ReportConfigContext, ) -from mypy.types import Type as MypyType from typing_extensions import override from mypy_django_plugin.config import DjangoPluginConfig @@ -53,6 +52,12 @@ ) from mypy_django_plugin.transformers.request import check_querydict_is_mutable +if TYPE_CHECKING: + from collections.abc import Callable + + from mypy.options import Options + from mypy.types import Type as MypyType + class NewSemanalDjangoPlugin(Plugin): def __init__(self, options: Options) -> None: diff --git a/mypy_django_plugin/transformers/auth.py b/mypy_django_plugin/transformers/auth.py index 43effb0f0..eeafb6618 100644 --- a/mypy_django_plugin/transformers/auth.py +++ b/mypy_django_plugin/transformers/auth.py @@ -1,14 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.nodes import TypeInfo -from mypy.plugin import AnalyzeTypeContext from mypy.semanal import SemanticAnalyzer from mypy.typeanal import TypeAnalyser from mypy.types import PlaceholderType, ProperType from mypy.types import Type as MypyType from mypy.typevars import fill_typevars_with_any -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.lib import fullnames, helpers +if TYPE_CHECKING: + from mypy.plugin import AnalyzeTypeContext + + from mypy_django_plugin.django.context import DjangoContext + def _get_abstract_base_user(api: SemanticAnalyzer) -> ProperType: sym = api.lookup_fully_qualified(fullnames.ABSTRACT_BASE_USER_MODEL_FULLNAME) diff --git a/mypy_django_plugin/transformers/choices.py b/mypy_django_plugin/transformers/choices.py index aa1138f2c..97333a144 100644 --- a/mypy_django_plugin/transformers/choices.py +++ b/mypy_django_plugin/transformers/choices.py @@ -1,8 +1,10 @@ +from __future__ import annotations + from enum import Enum, auto +from typing import TYPE_CHECKING from django.db.models.constants import LOOKUP_SEP from mypy.nodes import MemberExpr, NameExpr, SuperExpr, TypeAlias, TypeInfo, Var -from mypy.plugin import AttributeContext from mypy.typeanal import make_optional_type from mypy.types import ( AnyType, @@ -21,6 +23,9 @@ from mypy_django_plugin.lib import fullnames +if TYPE_CHECKING: + from mypy.plugin import AttributeContext + # TODO: [mypy 1.14+] Remove this backport of `TypeInfo.enum_members`. def _get_enum_members(info: TypeInfo) -> list[str]: @@ -47,7 +52,7 @@ class _LabelLaziness(Enum): MIXED = auto() @classmethod - def make(cls, lazy: bool | None) -> "_LabelLaziness": + def make(cls, lazy: bool | None) -> _LabelLaziness: return cls.LAZY if lazy else cls.NON_LAZY diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index 0f71a4452..55fabea98 100644 --- a/mypy_django_plugin/transformers/fields.py +++ b/mypy_django_plugin/transformers/fields.py @@ -1,24 +1,29 @@ -from typing import Any, NamedTuple, Union, cast +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple, cast from django.core.exceptions import FieldDoesNotExist from django.db.models.fields import AutoField, Field from django.db.models.fields.related import RelatedField -from django.db.models.fields.reverse_related import ForeignObjectRel from mypy.maptype import map_instance_to_supertype from mypy.nodes import AssignmentStmt, NameExpr, TypeInfo -from mypy.plugin import FunctionContext from mypy.types import AnyType, Instance, NoneType, ProperType, TypeOfAny, UninhabitedType, UnionType, get_proper_type from mypy.types import Type as MypyType -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.exceptions import UnregisteredModelError from mypy_django_plugin.lib import fullnames, helpers from mypy_django_plugin.transformers import manytomany +if TYPE_CHECKING: + from django.db.models.fields.reverse_related import ForeignObjectRel + from mypy.plugin import FunctionContext + + from mypy_django_plugin.django.context import DjangoContext + def _get_current_field_from_assignment( ctx: FunctionContext, django_context: DjangoContext -) -> Union["Field[Any, Any]", ForeignObjectRel] | None: +) -> Field[Any, Any] | ForeignObjectRel | None: outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class() if outer_model_info is None or not helpers.is_model_type(outer_model_info): return None diff --git a/mypy_django_plugin/transformers/forms.py b/mypy_django_plugin/transformers/forms.py index 4c14c4ac4..4b412060a 100644 --- a/mypy_django_plugin/transformers/forms.py +++ b/mypy_django_plugin/transformers/forms.py @@ -1,8 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.nodes import TypeInfo -from mypy.plugin import ClassDefContext from mypy_django_plugin.lib import fullnames, helpers +if TYPE_CHECKING: + from mypy.plugin import ClassDefContext + def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None: meta_node = helpers.get_nested_meta_node_for_current_class(ctx.cls.info) diff --git a/mypy_django_plugin/transformers/functional.py b/mypy_django_plugin/transformers/functional.py index 4de7b6286..2803b2202 100644 --- a/mypy_django_plugin/transformers/functional.py +++ b/mypy_django_plugin/transformers/functional.py @@ -1,15 +1,19 @@ -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any from mypy.checkmember import analyze_member_access from mypy.errorcodes import ATTR_DEFINED from mypy.nodes import CallExpr, MemberExpr -from mypy.plugin import AttributeContext from mypy.types import AnyType, Instance, TypeOfAny from mypy.types import Type as MypyType from mypy.version import __version__ as mypy_version from mypy_django_plugin.lib import helpers +if TYPE_CHECKING: + from mypy.plugin import AttributeContext + mypy_version_info = tuple(map(int, mypy_version.partition("+")[0].split("."))) diff --git a/mypy_django_plugin/transformers/init_create.py b/mypy_django_plugin/transformers/init_create.py index c9628b4a4..837c5d660 100644 --- a/mypy_django_plugin/transformers/init_create.py +++ b/mypy_django_plugin/transformers/init_create.py @@ -1,12 +1,19 @@ -from django.db.models.base import Model +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.errorcodes import CALL_ARG -from mypy.plugin import FunctionContext, MethodContext from mypy.types import Instance, get_proper_type from mypy.types import Type as MypyType -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.lib import helpers +if TYPE_CHECKING: + from django.db.models.base import Model + from mypy.plugin import FunctionContext, MethodContext + + from mypy_django_plugin.django.context import DjangoContext + def get_actual_types(ctx: MethodContext | FunctionContext, expected_keys: list[str]) -> list[tuple[str, MypyType]]: actual_types = [] diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 066584046..e594e32e8 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -1,6 +1,7 @@ -from typing import Final +from __future__ import annotations + +from typing import TYPE_CHECKING, Final -from mypy.checker import TypeChecker from mypy.copytype import copy_type from mypy.nodes import ( GDEF, @@ -17,9 +18,7 @@ SymbolTableNode, TypeInfo, ) -from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext from mypy.plugins.common import add_method_to_class -from mypy.semanal import SemanticAnalyzer from mypy.semanal_shared import has_placeholder from mypy.subtypes import find_member from mypy.types import ( @@ -41,6 +40,11 @@ from mypy_django_plugin.lib import fullnames, helpers +if TYPE_CHECKING: + from mypy.checker import TypeChecker + from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext + from mypy.semanal import SemanticAnalyzer + MANAGER_METHODS_RETURNING_QUERYSET: Final = frozenset( ( "alias", @@ -377,7 +381,7 @@ def create_manager_info_from_from_queryset_call( return None base_manager_info, queryset_info = call_expr.callee.expr.node, call_expr.args[0].node - if queryset_info.fullname is None: + if queryset_info.fullname is None: # type: ignore[comparison-overlap] # In some cases, due to the way the semantic analyzer works, only # passed_queryset.name is available. But it should be analyzed again, # so this isn't a problem. diff --git a/mypy_django_plugin/transformers/manytomany.py b/mypy_django_plugin/transformers/manytomany.py index 814edf6fd..b719edd0a 100644 --- a/mypy_django_plugin/transformers/manytomany.py +++ b/mypy_django_plugin/transformers/manytomany.py @@ -1,13 +1,18 @@ -from typing import NamedTuple +from __future__ import annotations + +from typing import TYPE_CHECKING, NamedTuple from mypy.nodes import AssignmentStmt, NameExpr, Node, TypeInfo -from mypy.plugin import FunctionContext, MethodContext from mypy.types import Instance, ProperType, UninhabitedType, get_proper_type from mypy.types import Type as MypyType -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.lib import fullnames, helpers +if TYPE_CHECKING: + from mypy.plugin import FunctionContext, MethodContext + + from mypy_django_plugin.django.context import DjangoContext + class M2MThrough(NamedTuple): arg: Node | None diff --git a/mypy_django_plugin/transformers/manytoone.py b/mypy_django_plugin/transformers/manytoone.py index e4645098a..3075afc36 100644 --- a/mypy_django_plugin/transformers/manytoone.py +++ b/mypy_django_plugin/transformers/manytoone.py @@ -1,9 +1,15 @@ -from mypy.plugin import MethodContext +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.types import Instance, get_proper_type from mypy.types import Type as MypyType from mypy_django_plugin.lib import fullnames, helpers +if TYPE_CHECKING: + from mypy.plugin import MethodContext + def get_model_of_related_manager(ctx: MethodContext) -> Instance | None: """ diff --git a/mypy_django_plugin/transformers/meta.py b/mypy_django_plugin/transformers/meta.py index 70cd623fb..f063c66ab 100644 --- a/mypy_django_plugin/transformers/meta.py +++ b/mypy_django_plugin/transformers/meta.py @@ -1,5 +1,8 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.core.exceptions import FieldDoesNotExist -from mypy.plugin import MethodContext from mypy.types import AnyType, Instance, TypeOfAny, get_proper_type from mypy.types import Type as MypyType @@ -7,6 +10,9 @@ from mypy_django_plugin.lib import helpers from mypy_django_plugin.lib.helpers import DjangoModel +if TYPE_CHECKING: + from mypy.plugin import MethodContext + def return_proper_field_type_from_get_field(ctx: MethodContext, django_context: DjangoContext) -> MypyType: if not ( diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 6db23a8ab..d2b41fe3b 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -1,12 +1,11 @@ +from __future__ import annotations + from collections import deque -from collections.abc import Iterable from functools import cached_property -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast -from django.db.models import Manager, Model from django.db.models.fields import DateField, DateTimeField, Field from django.db.models.fields.reverse_related import ForeignObjectRel, ManyToManyRel, OneToOneRel -from mypy.checker import TypeChecker from mypy.nodes import ( ARG_STAR2, MDEF, @@ -22,7 +21,6 @@ TypeInfo, Var, ) -from mypy.plugin import AnalyzeTypeContext, AttributeContext, ClassDefContext from mypy.plugins import common from mypy.semanal import SemanticAnalyzer from mypy.typeanal import TypeAnalyser @@ -31,8 +29,6 @@ from mypy.typevars import fill_typevars, fill_typevars_with_any from typing_extensions import override -from mypy_django_plugin.config import DjangoPluginConfig -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.errorcodes import MANAGER_MISSING from mypy_django_plugin.exceptions import UnregisteredModelError from mypy_django_plugin.lib import fullnames, helpers @@ -44,6 +40,16 @@ ) from mypy_django_plugin.transformers.manytomany import M2MArguments, M2MThrough, M2MTo +if TYPE_CHECKING: + from collections.abc import Iterable + + from django.db.models import Manager, Model + from mypy.checker import TypeChecker + from mypy.plugin import AnalyzeTypeContext, AttributeContext, ClassDefContext + + from mypy_django_plugin.config import DjangoPluginConfig + from mypy_django_plugin.django.context import DjangoContext + class ModelClassInitializer: api: SemanticAnalyzer @@ -295,7 +301,7 @@ def run_with_model_cls(self, model_cls: type[Model]) -> None: def create_autofield( self, - auto_field: "Field[Any, Any]", + auto_field: Field[Any, Any], dest_name: str, existing_field: bool, ) -> None: @@ -317,7 +323,7 @@ class AddPrimaryKeyAlias(AddDefaultPrimaryKey): 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 is not None: + if auto_field is not None: # type: ignore[comparison-overlap] self.create_autofield( auto_field=auto_field, dest_name="pk", @@ -365,7 +371,7 @@ def run_with_model_cls(self, model_cls: type[Model]) -> None: class AddManagers(ModelClassInitializer): - def lookup_manager(self, fullname: str, manager: "Manager[Any]") -> TypeInfo | None: + def lookup_manager(self, fullname: str, manager: Manager[Any]) -> TypeInfo | None: manager_info = self.lookup_typeinfo(fullname) if manager_info is None: manager_info = self.get_dynamic_manager(fullname, manager) @@ -450,7 +456,7 @@ def get_manager_expression(self, name: str) -> AssignmentStmt | None: return None - def get_dynamic_manager(self, fullname: str, manager: "Manager[Any]") -> TypeInfo | None: + def get_dynamic_manager(self, fullname: str, manager: Manager[Any]) -> TypeInfo | None: """ Try to get a dynamically defined manager """ diff --git a/mypy_django_plugin/transformers/orm_lookups.py b/mypy_django_plugin/transformers/orm_lookups.py index 040c5ae4d..9ec71d8e2 100644 --- a/mypy_django_plugin/transformers/orm_lookups.py +++ b/mypy_django_plugin/transformers/orm_lookups.py @@ -1,11 +1,18 @@ -from mypy.plugin import MethodContext +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.types import AnyType, Instance, ProperType, TypeOfAny, get_proper_type from mypy.types import Type as MypyType -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.exceptions import UnregisteredModelError from mypy_django_plugin.lib import fullnames, helpers +if TYPE_CHECKING: + from mypy.plugin import MethodContext + + from mypy_django_plugin.django.context import DjangoContext + def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) -> MypyType: django_model = helpers.get_model_info_from_qs_ctx(ctx, django_context) diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index d68e276d3..28668b5c3 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -1,8 +1,8 @@ -from collections.abc import Sequence +from __future__ import annotations + from typing import TYPE_CHECKING, Literal from django.core.exceptions import FieldDoesNotExist, FieldError -from django.db.models.base import Model from django.db.models.constants import LOOKUP_SEP from django.db.models.fields.related import RelatedField from django.db.models.fields.related_descriptors import ( @@ -14,20 +14,24 @@ from django.db.models.fields.reverse_related import ForeignObjectRel from django.db.models.lookups import Transform from django.db.models.sql.query import Query -from mypy.checker import TypeChecker from mypy.errorcodes import NO_REDEF from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, ARG_STAR, CallExpr, Expression, ListExpr, SetExpr, TupleExpr -from mypy.plugin import FunctionContext, MethodContext from mypy.types import AnyType, Instance, LiteralType, ProperType, TupleType, TypedDictType, TypeOfAny, get_proper_type from mypy.types import Type as MypyType from mypy_django_plugin.django.context import DjangoContext, LookupsAreUnsupported from mypy_django_plugin.lib import fullnames, helpers -from mypy_django_plugin.lib.helpers import DjangoModel from mypy_django_plugin.transformers.models import get_annotated_type if TYPE_CHECKING: + from collections.abc import Sequence + + from django.db.models.base import Model from django.db.models.options import _AnyField + from mypy.checker import TypeChecker + from mypy.plugin import FunctionContext, MethodContext + + from mypy_django_plugin.lib.helpers import DjangoModel def determine_proper_manager_type(ctx: FunctionContext) -> MypyType: @@ -531,9 +535,11 @@ def check_valid_prefetch_related_lookup( for through_attr in lookup.split(LOOKUP_SEP): rel_obj_descriptor = getattr(current_model_cls, through_attr, None) if rel_obj_descriptor is None: + # If current_model_cls is "self", we cannot use `__name__` and want "self". + model_name = getattr(current_model_cls, "__name__", current_model_cls) ctx.api.fail( ( - f'Cannot find "{through_attr}" on "{current_model_cls.__name__}" object, ' + f'Cannot find "{through_attr}" on "{model_name}" object, ' f'"{lookup}" is an invalid parameter to "prefetch_related()"' ), ctx.context, @@ -543,8 +549,10 @@ def check_valid_prefetch_related_lookup( from django.contrib.contenttypes.fields import GenericForeignKey if not isinstance(rel_obj_descriptor, GenericForeignKey): + # If current_model_cls is "self", we cannot use `__name__` and want "self". + model_name = getattr(current_model_cls, "__name__", current_model_cls) ctx.api.fail( - f'"{through_attr}" on "{current_model_cls.__name__}" is not a GenericForeignKey, ' + f'"{through_attr}" on "{model_name}" is not a GenericForeignKey, ' f"GenericPrefetch can only be used with GenericForeignKey fields", ctx.context, ) @@ -552,10 +560,10 @@ def check_valid_prefetch_related_lookup( elif isinstance(rel_obj_descriptor, ForwardManyToOneDescriptor): current_model_cls = rel_obj_descriptor.field.remote_field.model elif isinstance(rel_obj_descriptor, ReverseOneToOneDescriptor): - current_model_cls = rel_obj_descriptor.related.related_model # type:ignore[assignment] # Can't be 'self' for non abstract models + current_model_cls = rel_obj_descriptor.related.related_model elif isinstance(rel_obj_descriptor, ManyToManyDescriptor): current_model_cls = ( - rel_obj_descriptor.rel.related_model if rel_obj_descriptor.reverse else rel_obj_descriptor.rel.model # type:ignore[assignment] # Can't be 'self' for non abstract models + rel_obj_descriptor.rel.related_model if rel_obj_descriptor.reverse else rel_obj_descriptor.rel.model ) elif isinstance(rel_obj_descriptor, ReverseManyToOneDescriptor): if contenttypes_installed: @@ -564,7 +572,7 @@ def check_valid_prefetch_related_lookup( if isinstance(rel_obj_descriptor, ReverseGenericManyToOneDescriptor): current_model_cls = rel_obj_descriptor.rel.model continue - current_model_cls = rel_obj_descriptor.rel.related_model # type:ignore[assignment] # Can't be 'self' for non abstract models + current_model_cls = rel_obj_descriptor.rel.related_model else: if contenttypes_installed: from django.contrib.contenttypes.fields import GenericForeignKey @@ -708,7 +716,7 @@ def extract_prefetch_related_annotations(ctx: MethodContext, django_context: Dja def _try_get_field( ctx: MethodContext, model_cls: type[Model], field_name: str, *, resolve_pk: bool = False -) -> "_AnyField | None": +) -> _AnyField | None: opts = model_cls._meta resolved_name = opts.pk.name if resolve_pk and field_name == "pk" else field_name try: @@ -718,7 +726,7 @@ def _try_get_field( return None -def _check_field_concrete(ctx: MethodContext, field: "_AnyField", field_name: str, method: str) -> bool: +def _check_field_concrete(ctx: MethodContext, field: _AnyField, field_name: str, method: str) -> bool: if not field.concrete or field.many_to_many: ctx.api.fail(f'"{method}()" can only be used with concrete fields. Got "{field_name}"', ctx.context) return False @@ -728,7 +736,7 @@ def _check_field_concrete(ctx: MethodContext, field: "_AnyField", field_name: st def _check_field_not_pk( ctx: MethodContext, model_cls: type[Model], - field: "_AnyField", + field: _AnyField, field_name: str, method: str, *, diff --git a/mypy_django_plugin/transformers/request.py b/mypy_django_plugin/transformers/request.py index 568f73b7f..9b52c828e 100644 --- a/mypy_django_plugin/transformers/request.py +++ b/mypy_django_plugin/transformers/request.py @@ -1,7 +1,13 @@ -from mypy.plugin import MethodContext +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.types import Type as MypyType from mypy.types import UninhabitedType, get_proper_type +if TYPE_CHECKING: + from mypy.plugin import MethodContext + def check_querydict_is_mutable(ctx: MethodContext) -> MypyType: ret_type = get_proper_type(ctx.default_return_type) diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index 43a62ff24..ff944c021 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -1,12 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from mypy.nodes import MemberExpr -from mypy.plugin import AttributeContext from mypy.types import AnyType, TypeOfAny from mypy.types import Type as MypyType -from mypy_django_plugin.config import DjangoPluginConfig -from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.lib import helpers +if TYPE_CHECKING: + from mypy.plugin import AttributeContext + + from mypy_django_plugin.config import DjangoPluginConfig + from mypy_django_plugin.django.context import DjangoContext + def get_type_of_settings_attribute( ctx: AttributeContext, django_context: DjangoContext, plugin_config: DjangoPluginConfig diff --git a/pyproject.toml b/pyproject.toml index 14acbca03..fce56a44b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,4 +156,5 @@ ignore = [ [tool.ruff.lint.isort] known-first-party = ["django_stubs_ext", "mypy_django_plugin"] +required-imports = ["from __future__ import annotations"] split-on-trailing-comma = false diff --git a/pyrightconfig.testcases.json b/pyrightconfig.testcases.json index f939a0905..e57489ffa 100644 --- a/pyrightconfig.testcases.json +++ b/pyrightconfig.testcases.json @@ -5,7 +5,6 @@ ], "typeCheckingMode": "strict", // Extra strict settings - "reportShadowedImports": "error", // Don't accidentally name a file something that shadows stdlib "reportImplicitStringConcatenation": "error", "reportUninitializedInstanceVariable": "error", "reportUnnecessaryTypeIgnoreComment": "error", diff --git a/scripts/django_tests_settings.py b/scripts/django_tests_settings.py index b005ec50a..f7d92fb0a 100644 --- a/scripts/django_tests_settings.py +++ b/scripts/django_tests_settings.py @@ -1,5 +1,7 @@ # It is used in `mypy.ini` only. # The following installed apps are required for stubtest to run correctly. +from __future__ import annotations + INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.admin", diff --git a/scripts/tests_extension_hook.py b/scripts/tests_extension_hook.py index 5df62e8fc..a3c5d4958 100644 --- a/scripts/tests_extension_hook.py +++ b/scripts/tests_extension_hook.py @@ -1,5 +1,11 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from pytest_mypy_plugins.collect import File -from pytest_mypy_plugins.item import YamlTestItem + +if TYPE_CHECKING: + from pytest_mypy_plugins.item import YamlTestItem def django_plugin_hook(test_item: YamlTestItem) -> None: diff --git a/tests/assert_type/apps/test_config.py b/tests/assert_type/apps/test_config.py index 3a873ceb6..9201a1804 100644 --- a/tests/assert_type/apps/test_config.py +++ b/tests/assert_type/apps/test_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.apps.config import AppConfig from django.utils.functional import cached_property from typing_extensions import assert_type, override diff --git a/tests/assert_type/conf/settings.py b/tests/assert_type/conf/settings.py index 6ba38d3d6..93f51cbe2 100644 --- a/tests/assert_type/conf/settings.py +++ b/tests/assert_type/conf/settings.py @@ -1,6 +1,10 @@ +from __future__ import annotations + from pathlib import Path +from typing import TYPE_CHECKING -from django_stubs_ext.settings import TemplatesSetting +if TYPE_CHECKING: + from django_stubs_ext.settings import TemplatesSetting BASE_DIR = Path(__file__).resolve().parent diff --git a/tests/assert_type/contrib/admin/test_utils.py b/tests/assert_type/contrib/admin/test_utils.py index 169992996..1bec11526 100644 --- a/tests/assert_type/contrib/admin/test_utils.py +++ b/tests/assert_type/contrib/admin/test_utils.py @@ -62,11 +62,11 @@ class PersonFieldsetTupleAdmin(admin.ModelAdmin[Person]): person_fieldset_list_admin = PersonFieldsetListAdmin(Person, admin_site) person_fieldset_tuple_admin = PersonFieldsetTupleAdmin(Person, admin_site) -# For some reason, pyright cannot see that these are not `None`. -assert person_list_admin.fields is not None -assert person_tuple_admin.fields is not None -assert person_fieldset_list_admin.fieldsets is not None -assert person_fieldset_tuple_admin.fieldsets is not None +# For some reason, pyright cannot see that these are not `None` so we need to ignore these for mypy. +assert person_list_admin.fields is not None # type: ignore[comparison-overlap] +assert person_tuple_admin.fields is not None # type: ignore[comparison-overlap] +assert person_fieldset_list_admin.fieldsets is not None # type: ignore[comparison-overlap] +assert person_fieldset_tuple_admin.fieldsets is not None # type: ignore[comparison-overlap] assert_type(flatten(person_list_admin.fields), list[str]) assert_type(flatten(person_list_admin.get_fields(request)), list[str]) diff --git a/tests/assert_type/contrib/auth/test_decorators.py b/tests/assert_type/contrib/auth/test_decorators.py index 1baf551b1..2ac610324 100644 --- a/tests/assert_type/contrib/auth/test_decorators.py +++ b/tests/assert_type/contrib/auth/test_decorators.py @@ -1,7 +1,13 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.contrib.auth.decorators import user_passes_test -from django.http import HttpRequest, HttpResponse from django.urls import reverse, reverse_lazy +if TYPE_CHECKING: + from django.http import HttpRequest, HttpResponse + reversed_url = reverse("url") lazy_url = reverse_lazy("namespace:url") diff --git a/tests/assert_type/contrib/auth/test_models.py b/tests/assert_type/contrib/auth/test_models.py index 602dfae3e..a68e29a89 100644 --- a/tests/assert_type/contrib/auth/test_models.py +++ b/tests/assert_type/contrib/auth/test_models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import AbstractBaseUser, UserManager from typing_extensions import assert_type diff --git a/tests/assert_type/db/models/_enums.py b/tests/assert_type/db/models/_enums.py index 23389019c..6397f687b 100644 --- a/tests/assert_type/db/models/_enums.py +++ b/tests/assert_type/db/models/_enums.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Literal from django.db.models import TextChoices diff --git a/tests/assert_type/db/models/fields/test_files.py b/tests/assert_type/db/models/fields/test_files.py index 0993236eb..44a5be26b 100644 --- a/tests/assert_type/db/models/fields/test_files.py +++ b/tests/assert_type/db/models/fields/test_files.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models from django.db.models.fields.files import FieldFile, ImageFieldFile from typing_extensions import assert_type diff --git a/tests/assert_type/db/models/fields/test_related.py b/tests/assert_type/db/models/fields/test_related.py index f006bbc87..3c2b9e7ab 100644 --- a/tests/assert_type/db/models/fields/test_related.py +++ b/tests/assert_type/db/models/fields/test_related.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/assert_type/db/models/fields/test_related_descriptors.py b/tests/assert_type/db/models/fields/test_related_descriptors.py index 3155da4c4..a98908c1e 100644 --- a/tests/assert_type/db/models/fields/test_related_descriptors.py +++ b/tests/assert_type/db/models/fields/test_related_descriptors.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import ClassVar from django.db import models @@ -6,7 +8,7 @@ class Other(models.Model): - explicit_descriptor: ClassVar[ReverseManyToOneDescriptor["MyModel"]] + explicit_descriptor: ClassVar[ReverseManyToOneDescriptor[MyModel]] class MyModel(models.Model): diff --git a/tests/assert_type/db/models/functions/test_window.py b/tests/assert_type/db/models/functions/test_window.py index eb3ed0bb0..f0384cb22 100644 --- a/tests/assert_type/db/models/functions/test_window.py +++ b/tests/assert_type/db/models/functions/test_window.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db.models import F, Value from django.db.models.functions import Lag, Lead, NthValue diff --git a/tests/assert_type/db/models/test_constraints.py b/tests/assert_type/db/models/test_constraints.py index 148669fd9..457425972 100644 --- a/tests/assert_type/db/models/test_constraints.py +++ b/tests/assert_type/db/models/test_constraints.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db.models import CheckConstraint, Q, UniqueConstraint from django.db.models.functions import Lower diff --git a/tests/assert_type/db/models/test_enums.py b/tests/assert_type/db/models/test_enums.py index c86fa13b9..84177c7c5 100644 --- a/tests/assert_type/db/models/test_enums.py +++ b/tests/assert_type/db/models/test_enums.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import enum from typing import Any, Literal, TypeVar diff --git a/tests/assert_type/db/models/test_ordering.py b/tests/assert_type/db/models/test_ordering.py index d1dee9ce1..1e1fb82c7 100644 --- a/tests/assert_type/db/models/test_ordering.py +++ b/tests/assert_type/db/models/test_ordering.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import User from django.db.models import F, OrderBy from django.db.models.sql.query import Query diff --git a/tests/assert_type/db/models/test_query.py b/tests/assert_type/db/models/test_query.py index c92f9961a..737355935 100644 --- a/tests/assert_type/db/models/test_query.py +++ b/tests/assert_type/db/models/test_query.py @@ -1,11 +1,17 @@ -from collections.abc import Sequence +from __future__ import annotations + +from typing import TYPE_CHECKING -from django.db.models import Model from django.db.models.query import ( aprefetch_related_objects, # pyright: ignore[reportUnknownVariableType] prefetch_related_objects, # pyright: ignore[reportUnknownVariableType] ) +if TYPE_CHECKING: + from collections.abc import Sequence + + from django.db.models import Model + models_list: list[Model] = [] prefetch_related_objects(models_list, "pk") diff --git a/tests/assert_type/forms/test_add_error.py b/tests/assert_type/forms/test_add_error.py index 59b74d044..baae4c2d6 100644 --- a/tests/assert_type/forms/test_add_error.py +++ b/tests/assert_type/forms/test_add_error.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.core.exceptions import ValidationError from django.forms import BaseForm from django.utils.functional import lazystr diff --git a/tests/assert_type/http/test_redirect.py b/tests/assert_type/http/test_redirect.py index 517561f58..915563961 100644 --- a/tests/assert_type/http/test_redirect.py +++ b/tests/assert_type/http/test_redirect.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from django.http import HttpResponseRedirect from django.urls import reverse, reverse_lazy diff --git a/tests/assert_type/http/test_request.py b/tests/assert_type/http/test_request.py index 47f7457fa..a58155d23 100644 --- a/tests/assert_type/http/test_request.py +++ b/tests/assert_type/http/test_request.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Iterable, Iterator from django.http import QueryDict diff --git a/tests/assert_type/http/test_response.py b/tests/assert_type/http/test_response.py index fa7da3cf7..82a0c1b48 100644 --- a/tests/assert_type/http/test_response.py +++ b/tests/assert_type/http/test_response.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import AsyncIterable, AsyncIterator, Iterator from django.http.response import HttpResponse, StreamingHttpResponse diff --git a/tests/assert_type/middleware/test_middleware.py b/tests/assert_type/middleware/test_middleware.py index 624af2a38..b0ace44da 100644 --- a/tests/assert_type/middleware/test_middleware.py +++ b/tests/assert_type/middleware/test_middleware.py @@ -1,5 +1,8 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.contrib.redirects.middleware import RedirectFallbackMiddleware -from django.http.request import HttpRequest from django.http.response import ( FileResponse, HttpResponse, @@ -12,6 +15,9 @@ from django.middleware.locale import LocaleMiddleware from typing_extensions import override +if TYPE_CHECKING: + from django.http.request import HttpRequest + class CustomCommonMiddleware(CommonMiddleware): response_redirect_class = HttpResponsePermanentRedirect diff --git a/tests/assert_type/template/backends/test_utils.py b/tests/assert_type/template/backends/test_utils.py index b8b10d7d2..529b81905 100644 --- a/tests/assert_type/template/backends/test_utils.py +++ b/tests/assert_type/template/backends/test_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.http import HttpRequest from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy from django.utils.functional import _StrPromise diff --git a/tests/assert_type/test/client.py b/tests/assert_type/test/client.py index de63159cc..9bce987f4 100644 --- a/tests/assert_type/test/client.py +++ b/tests/assert_type/test/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.test import Client from django.urls import reverse, reverse_lazy diff --git a/tests/assert_type/urls/test_conf.py b/tests/assert_type/urls/test_conf.py index d2e266f7c..2048a545e 100644 --- a/tests/assert_type/urls/test_conf.py +++ b/tests/assert_type/urls/test_conf.py @@ -1,14 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.conf.urls.i18n import urlpatterns as i18n_urlpatterns from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth.views import LoginView from django.contrib.flatpages import urls as flatpages_urls from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.http import HttpResponse from django.urls import URLPattern, URLResolver, _AnyURL, include, path, re_path from django.utils.translation import gettext_lazy as _ from typing_extensions import assert_type +if TYPE_CHECKING: + from django.http import HttpResponse + # Test 'path' accepts mix of pattern and resolver object include1: tuple[list[_AnyURL], None, None] = ([], None, None) assert_type(path("test/", include1), URLResolver) diff --git a/tests/assert_type/urls/test_resolvers.py b/tests/assert_type/urls/test_resolvers.py index 215f06288..8d0cc2cc4 100644 --- a/tests/assert_type/urls/test_resolvers.py +++ b/tests/assert_type/urls/test_resolvers.py @@ -1,4 +1,9 @@ -from django.urls.resolvers import ResolverMatch +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from django.urls.resolvers import ResolverMatch def f(x: ResolverMatch) -> None: diff --git a/tests/assert_type/utils/test_decorators.py b/tests/assert_type/utils/test_decorators.py index b1d4a0fa2..745c782a3 100644 --- a/tests/assert_type/utils/test_decorators.py +++ b/tests/assert_type/utils/test_decorators.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Callable from django.http.response import HttpResponseBase diff --git a/tests/assert_type/utils/test_encoding.py b/tests/assert_type/utils/test_encoding.py index 8a021d5ac..df6b30e52 100644 --- a/tests/assert_type/utils/test_encoding.py +++ b/tests/assert_type/utils/test_encoding.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any from django.utils.encoding import force_bytes, force_str, smart_bytes, smart_str diff --git a/tests/assert_type/utils/test_text.py b/tests/assert_type/utils/test_text.py index 08fa13a2d..358fd5b79 100644 --- a/tests/assert_type/utils/test_text.py +++ b/tests/assert_type/utils/test_text.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.utils.functional import _StrPromise from django.utils.text import format_lazy from django.utils.translation import gettext_lazy diff --git a/tests/assert_type/views/test_decorators.py b/tests/assert_type/views/test_decorators.py index 2e4ff32a7..cc81b8b03 100644 --- a/tests/assert_type/views/test_decorators.py +++ b/tests/assert_type/views/test_decorators.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.http import HttpRequest, HttpResponse from django.views.decorators.csp import csp_override, csp_report_only_override from typing_extensions import assert_type diff --git a/tests/assert_type/views/test_generic.py b/tests/assert_type/views/test_generic.py index 3cc924580..9140f5380 100644 --- a/tests/assert_type/views/test_generic.py +++ b/tests/assert_type/views/test_generic.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django import forms from django.db import models from django.db.models import F diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index d7f8fb32f..075a1bf40 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -1,15 +1,19 @@ +from __future__ import annotations + import os import tempfile import uuid -from collections.abc import Generator from contextlib import contextmanager -from typing import Any +from typing import TYPE_CHECKING, Any from unittest import mock import pytest from mypy_django_plugin.config import DjangoPluginConfig +if TYPE_CHECKING: + from collections.abc import Generator + TEMPLATE = """ (config) ... diff --git a/tests/test_generic_consistency.py b/tests/test_generic_consistency.py index 14b6636ed..8c0ec28ad 100644 --- a/tests/test_generic_consistency.py +++ b/tests/test_generic_consistency.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import glob import importlib