* Fix CI

* Fix CI

* Fix CI

* Fix CI

* APply black

* APply black

* Fix mypy

* Fix mypy errors in django-stubs

* Fix format

* Fix plugin

* Do not patch builtins by default

* Fix mypy

* Only run mypy on 3.10 for now

* Only run mypy on 3.10 for now

* WHAT THE HELL

* Enable strict mode in mypy

* Enable strict mode in mypy

* Fix tests

* Fix tests

* Debug

* Debug

* Fix tests

* Fix tests

* Add TYPE_CHECKING debug

* Caching maybe?

* Caching maybe?

* Try explicit `${{ matrix.python-version }}`

* Remove debug

* Fix typing

* Finally
This commit is contained in:
Nikita Sobolev
2022-08-26 13:22:55 +03:00
committed by GitHub
parent d2bfd3710b
commit 0bb1182c42
80 changed files with 223 additions and 582 deletions

View File

@@ -1,9 +1,8 @@
import builtins
import os
import sys
from collections import defaultdict
from contextlib import contextmanager
from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union
from django.core.exceptions import FieldError
from django.db import models
@@ -39,7 +38,7 @@ if TYPE_CHECKING:
@contextmanager
def temp_environ():
def temp_environ() -> Iterator[None]:
"""Allow the ability to set os.environ temporarily"""
environ = dict(os.environ)
try:
@@ -56,19 +55,6 @@ def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
# add current directory to sys.path
sys.path.append(os.getcwd())
def noop_class_getitem(cls, key):
return cls
from django.db import models
models.QuerySet.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore
models.Manager.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore
# Define mypy builtins, to not cause NameError during setting up Django.
# TODO: temporary/unpatch
builtins.reveal_type = lambda _: None
builtins.reveal_locals = lambda: None
from django.apps import apps
from django.conf import settings
@@ -80,8 +66,8 @@ def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
apps.populate(settings.INSTALLED_APPS)
assert apps.apps_ready
assert settings.configured
assert apps.apps_ready, "Apps are not ready"
assert settings.configured, "Settings are not configured"
return apps, settings
@@ -127,7 +113,7 @@ class DjangoContext:
return model_cls
return None
def get_model_fields(self, model_cls: Type[Model]) -> Iterator[Field]:
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
@@ -137,7 +123,9 @@ class DjangoContext:
if isinstance(field, ForeignObjectRel):
yield field
def get_field_lookup_exact_type(self, api: TypeChecker, field: Union[Field, ForeignObjectRel]) -> MypyType:
def get_field_lookup_exact_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel]
) -> MypyType:
if isinstance(field, (RelatedField, ForeignObjectRel)):
related_model_cls = field.related_model
primary_key_field = self.get_primary_key_field(related_model_cls)
@@ -155,7 +143,7 @@ class DjangoContext:
return AnyType(TypeOfAny.explicit)
return helpers.get_private_descriptor_type(field_info, "_pyi_lookup_exact_type", is_nullable=field.null)
def get_primary_key_field(self, model_cls: Type[Model]) -> Field:
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:
@@ -258,11 +246,11 @@ class DjangoContext:
def all_registered_model_class_fullnames(self) -> Set[str]:
return {helpers.get_class_fullname(cls) for cls in self.all_registered_model_classes}
def get_attname(self, field: Field) -> str:
def get_attname(self, field: "Field[Any, Any]") -> str:
attname = field.attname
return attname
def get_field_nullability(self, field: Union[Field, ForeignObjectRel], method: Optional[str]) -> bool:
def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel], method: Optional[str]) -> bool:
if method in ("values", "values_list"):
return field.null
@@ -279,7 +267,9 @@ class DjangoContext:
return True
return nullable
def get_field_set_type(self, api: TypeChecker, field: Union[Field, ForeignObjectRel], *, method: str) -> MypyType:
def get_field_set_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str
) -> MypyType:
"""Get a type of __set__ for this specific Django field."""
target_field = field
if isinstance(field, ForeignKey):
@@ -297,7 +287,9 @@ class DjangoContext:
field_set_type = helpers.convert_any_to_type(field_set_type, argument_field_type)
return field_set_type
def get_field_get_type(self, api: TypeChecker, field: Union[Field, ForeignObjectRel], *, method: str) -> MypyType:
def get_field_get_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str
) -> MypyType:
"""Get a type of __get__ for this specific Django field."""
field_info = helpers.lookup_class_typeinfo(api, field.__class__)
if field_info is None:
@@ -321,14 +313,16 @@ class DjangoContext:
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, ForeignObjectRel]) -> Optional[Type[Model]]:
def get_field_related_model_cls(
self, field: Union["RelatedField[Any, Any]", ForeignObjectRel]
) -> Optional[Type[Model]]:
if isinstance(field, RelatedField):
related_model_cls = field.remote_field.model
else:
related_model_cls = field.field.model
if isinstance(related_model_cls, str):
if related_model_cls == "self": # type: ignore[unreachable]
if related_model_cls == "self": # type: ignore
# same model
related_model_cls = field.model
elif "." not in related_model_cls:
@@ -342,9 +336,9 @@ class DjangoContext:
def _resolve_field_from_parts(
self, field_parts: Iterable[str], model_cls: Type[Model]
) -> Union[Field, ForeignObjectRel]:
) -> Union["Field[Any, Any]", ForeignObjectRel]:
currently_observed_model = model_cls
field: Union[Field, ForeignObjectRel, GenericForeignKey, None] = None
field: Union["Field[Any, Any]", ForeignObjectRel, GenericForeignKey, None] = None
for field_part in field_parts:
if field_part == "pk":
field = self.get_primary_key_field(currently_observed_model)
@@ -364,7 +358,9 @@ class DjangoContext:
assert isinstance(field, (Field, ForeignObjectRel))
return field
def resolve_lookup_into_field(self, model_cls: Type[Model], lookup: str) -> Union[Field, ForeignObjectRel]:
def resolve_lookup_into_field(
self, model_cls: Type[Model], lookup: str
) -> Union["Field[Any, Any]", ForeignObjectRel]:
query = Query(model_cls)
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)

View File

@@ -95,7 +95,10 @@ def lookup_fully_qualified_typeinfo(api: Union[TypeChecker, SemanticAnalyzer], f
return node
def lookup_class_typeinfo(api: TypeChecker, klass: type) -> Optional[TypeInfo]:
def lookup_class_typeinfo(api: TypeChecker, klass: Optional[type]) -> Optional[TypeInfo]:
if klass is None:
return None
fullname = get_class_fullname(klass)
field_info = lookup_fully_qualified_typeinfo(api, fullname)
return field_info
@@ -183,7 +186,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) -> MypyType:
def get_field_lookup_exact_type(api: TypeChecker, field: "Field[Any, Any]") -> MypyType:
if isinstance(field, (RelatedField, ForeignObjectRel)):
lookup_type_class = field.related_model
rel_model_info = lookup_class_typeinfo(api, lookup_type_class)
@@ -318,7 +321,7 @@ def resolve_string_attribute_value(attr_expr: Expression, django_context: "Djang
member_name = attr_expr.name
if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == "django.conf.settings":
if hasattr(django_context.settings, member_name):
return getattr(django_context.settings, member_name)
return getattr(django_context.settings, member_name) # type: ignore
return None

View File

@@ -1,6 +1,6 @@
import sys
from functools import partial
from typing import Callable, Dict, List, Optional, Tuple
from typing import Callable, Dict, List, Optional, Tuple, Type
from django.db.models.fields.related import RelatedField
from mypy.modulefinder import mypy_path
@@ -72,7 +72,7 @@ class NewSemanalDjangoPlugin(Plugin):
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):
return helpers.get_django_metadata(model_sym.node).setdefault(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"queryset_bases", {fullnames.QUERYSET_CLASS_FULLNAME: 1}
)
else:
@@ -81,7 +81,7 @@ class NewSemanalDjangoPlugin(Plugin):
def _get_current_manager_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(fullnames.MANAGER_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return helpers.get_django_metadata(model_sym.node).setdefault(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"manager_bases", {fullnames.MANAGER_CLASS_FULLNAME: 1}
)
else:
@@ -90,7 +90,7 @@ class NewSemanalDjangoPlugin(Plugin):
def _get_current_model_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(fullnames.MODEL_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return helpers.get_django_metadata(model_sym.node).setdefault(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"model_bases", {fullnames.MODEL_CLASS_FULLNAME: 1}
)
else:
@@ -99,7 +99,7 @@ class NewSemanalDjangoPlugin(Plugin):
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):
return helpers.get_django_metadata(model_sym.node).setdefault(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"baseform_bases",
{
fullnames.BASEFORM_CLASS_FULLNAME: 1,
@@ -305,5 +305,5 @@ class NewSemanalDjangoPlugin(Plugin):
return None
def plugin(version):
def plugin(version: str) -> Type[NewSemanalDjangoPlugin]:
return NewSemanalDjangoPlugin

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
from django.db.models.fields import AutoField, Field
from django.db.models.fields.related import RelatedField
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
def _get_current_field_from_assignment(
ctx: FunctionContext, django_context: DjangoContext
) -> Optional[Union[Field, ForeignObjectRel, "GenericForeignKey"]]:
) -> Optional[Union["Field[Any, Any]", ForeignObjectRel, "GenericForeignKey"]]:
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
if outer_model_info is None or not helpers.is_model_subclass_info(outer_model_info, django_context):
return None
@@ -42,7 +42,7 @@ def _get_current_field_from_assignment(
return current_field
def reparametrize_related_field_type(related_field_type: Instance, set_type, get_type) -> Instance:
def reparametrize_related_field_type(related_field_type: Instance, set_type: MypyType, get_type: MypyType) -> Instance:
args = [
helpers.convert_any_to_type(related_field_type.args[0], set_type),
helpers.convert_any_to_type(related_field_type.args[1], get_type),

View File

@@ -1,6 +1,6 @@
from typing import Optional, Union
from mypy.checker import TypeChecker, fill_typevars
from mypy.checker import TypeChecker
from mypy.nodes import (
GDEF,
CallExpr,
@@ -19,6 +19,7 @@ from mypy.plugin import AttributeContext, DynamicClassDefContext, SemanticAnalyz
from mypy.types import AnyType, CallableType, Instance, ProperType
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny
from mypy.typevars import fill_typevars
from typing_extensions import Final
from mypy_django_plugin.lib import fullnames, helpers

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Type, Union, cast
from typing import Any, Dict, List, Optional, Type, Union, cast
from django.db.models import Manager, Model
from django.db.models.fields import DateField, DateTimeField, Field
@@ -166,7 +166,7 @@ class ModelClassInitializer:
return queryset_info
def run_with_model_cls(self, model_cls):
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
raise NotImplementedError("Implement this in subclasses")
@@ -202,7 +202,7 @@ class AddDefaultPrimaryKey(ModelClassInitializer):
def create_autofield(
self,
auto_field: Field,
auto_field: "Field[Any, Any]",
dest_name: str,
existing_field: bool,
) -> None:
@@ -394,7 +394,7 @@ class AddManagers(ModelClassInitializer):
return None
def get_dynamic_manager(self, fullname: str, manager: Manager) -> Optional[TypeInfo]:
def get_dynamic_manager(self, fullname: str, manager: "Manager[Any]") -> Optional[TypeInfo]:
"""
Try to get a dynamically defined manager
"""