Recover after #909 (#925)

* Fix stubs related to `(Async)RequestFactory` and `(Async)Client`

* Revert incorrect removal.

* Allow set as `unique_together`, use shared type alias.

* Revert `Q.__init__` to use only `*args, **kwargs` to remove false-positive with `Q(**{...})`

* Add abstract methods to `HttpResponseBase` to create common interface.

* Remove monkey-patched attributes from `HttpResponseBase` subclasses.

* Add QueryDict mutability checks (+ plugin support)

* Fix lint

* Return back GenericForeignKey to `Options.get_fields`

* Minor fixup

* Make plugin code typecheck with `--warn-unreachable`, minor performance increase.

* Better types for `{unique, index}_together` and Options.

* Fix odd type of `URLResolver.urlconf_name` which isn't a str actually.

* Better types for field migration operations.

* Revert form.files to `MultiValueDict[str, UploadedFile]`

* Compatibility fix (#916)

* Do not assume that `Annotated` is always related to django-stubs (fixes #893)

* Restrict `FormView.get_form` return type to `_FormT` (class type argument). Now it is resolved to `form_class` argument if present, but also errors if it is not subclass of _FormT

* Fix CI (make test runnable on 3.8)

* Fix CI (make test runnable on 3.8 _again_)
This commit is contained in:
sterliakov
2022-04-28 13:01:37 +03:00
committed by GitHub
parent 16499a22ab
commit 6226381484
29 changed files with 380 additions and 138 deletions
+6 -7
View File
@@ -35,6 +35,7 @@ except ImportError:
if TYPE_CHECKING:
from django.apps.registry import Apps # noqa: F401
from django.conf import LazySettings # noqa: F401
from django.contrib.contenttypes.fields import GenericForeignKey
@contextmanager
@@ -100,9 +101,6 @@ class DjangoContext:
@cached_property
def model_modules(self) -> Dict[str, Set[Type[Model]]]:
"""All modules that contain Django models."""
if self.apps_registry is None:
return {}
modules: Dict[str, Set[Type[Model]]] = defaultdict(set)
for concrete_model_cls in self.apps_registry.get_models():
modules[concrete_model_cls.__module__].add(concrete_model_cls)
@@ -327,7 +325,7 @@ class DjangoContext:
related_model_cls = field.field.model
if isinstance(related_model_cls, str):
if related_model_cls == "self":
if related_model_cls == "self": # type: ignore[unreachable]
# same model
related_model_cls = field.model
elif "." not in related_model_cls:
@@ -343,7 +341,7 @@ class DjangoContext:
self, field_parts: Iterable[str], model_cls: Type[Model]
) -> Union[Field, ForeignObjectRel]:
currently_observed_model = model_cls
field: Union[Field, ForeignObjectRel, None] = None
field: Union[Field, ForeignObjectRel, GenericForeignKey, None] = None
for field_part in field_parts:
if field_part == "pk":
field = self.get_primary_key_field(currently_observed_model)
@@ -359,15 +357,16 @@ class DjangoContext:
if isinstance(field, ForeignObjectRel):
currently_observed_model = field.related_model
assert field is not None
# Guaranteed by `query.solve_lookup_type` before.
assert isinstance(field, (Field, ForeignObjectRel))
return field
def resolve_lookup_into_field(self, model_cls: Type[Model], lookup: str) -> Union[Field, ForeignObjectRel]:
query = Query(model_cls)
lookup_parts, field_parts, is_expression = query.solve_lookup_type(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) -> MypyType:
+1
View File
@@ -35,6 +35,7 @@ RELATED_FIELDS_CLASSES = {FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME, MANYTOM
MIGRATION_CLASS_FULLNAME = "django.db.migrations.migration.Migration"
OPTIONS_CLASS_FULLNAME = "django.db.models.options.Options"
HTTPREQUEST_CLASS_FULLNAME = "django.http.request.HttpRequest"
QUERYDICT_CLASS_FULLNAME = "django.http.request.QueryDict"
COMBINABLE_EXPRESSION_FULLNAME = "django.db.models.expressions.Combinable"
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"
+18 -8
View File
@@ -32,6 +32,7 @@ from mypy_django_plugin.transformers.models import (
process_model_class,
set_auth_user_model_boolean_fields,
)
from mypy_django_plugin.transformers.request import check_querydict_is_mutable
def transform_model_class(ctx: ClassDefContext, django_context: DjangoContext) -> None:
@@ -187,12 +188,21 @@ class NewSemanalDjangoPlugin(Plugin):
def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], MypyType]]:
class_fullname, _, method_name = fullname.rpartition(".")
if method_name == "get_form_class":
# It is looked up very often, specialcase this method for minor speed up
if method_name == "__init_subclass__":
return None
if class_fullname.endswith("QueryDict"):
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYDICT_CLASS_FULLNAME):
return partial(check_querydict_is_mutable, django_context=self.django_context)
elif method_name == "get_form_class":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
return forms.extract_proper_type_for_get_form_class
if method_name == "get_form":
elif method_name == "get_form":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
return forms.extract_proper_type_for_get_form
@@ -204,30 +214,30 @@ class NewSemanalDjangoPlugin(Plugin):
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) or class_fullname in manager_classes:
return partial(querysets.extract_proper_type_queryset_values, django_context=self.django_context)
if method_name == "values_list":
elif method_name == "values_list":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) or class_fullname in manager_classes:
return partial(querysets.extract_proper_type_queryset_values_list, django_context=self.django_context)
if method_name == "annotate":
elif method_name == "annotate":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) or class_fullname in manager_classes:
return partial(querysets.extract_proper_type_queryset_annotate, django_context=self.django_context)
if method_name == "get_field":
elif method_name == "get_field":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.OPTIONS_CLASS_FULLNAME):
return partial(meta.return_proper_field_type_from_get_field, django_context=self.django_context)
if class_fullname in manager_classes and method_name == "create":
elif class_fullname in manager_classes and method_name == "create":
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
if class_fullname in manager_classes and method_name in {"filter", "get", "exclude"}:
elif class_fullname in manager_classes and method_name in {"filter", "get", "exclude"}:
return partial(
mypy_django_plugin.transformers.orm_lookups.typecheck_queryset_filter,
django_context=self.django_context,
)
if method_name == "from_queryset":
elif method_name == "from_queryset":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME):
return fail_if_manager_type_created_in_model_body
+5 -2
View File
@@ -1,4 +1,4 @@
from typing import Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Optional, Tuple, Union, cast
from django.db.models.fields import AutoField, Field
from django.db.models.fields.related import RelatedField
@@ -12,10 +12,13 @@ from mypy.types import TypeOfAny, UnionType
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
if TYPE_CHECKING:
from django.contrib.contenttypes.fields import GenericForeignKey
def _get_current_field_from_assignment(
ctx: FunctionContext, django_context: DjangoContext
) -> Optional[Union[Field, ForeignObjectRel]]:
) -> Optional[Union[Field, 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
@@ -263,8 +263,6 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
semanal_api.defer()
return None
original_return_type = method_type.ret_type
if original_return_type is None:
continue
# Skip any method that doesn't return _QS
original_return_type = get_proper_type(original_return_type)
+3 -3
View File
@@ -15,7 +15,7 @@ from mypy.types import TypedDictType, TypeOfAny
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.lib.fullnames import ANNOTATIONS_FULLNAME, ANY_ATTR_ALLOWED_CLASS_FULLNAME
from mypy_django_plugin.lib.fullnames import ANNOTATIONS_FULLNAME, ANY_ATTR_ALLOWED_CLASS_FULLNAME, MODEL_CLASS_FULLNAME
from mypy_django_plugin.lib.helpers import add_new_class_for_module
from mypy_django_plugin.transformers import fields
from mypy_django_plugin.transformers.fields import get_field_descriptor_types
@@ -475,8 +475,8 @@ def handle_annotated_type(ctx: AnalyzeTypeContext, django_context: DjangoContext
type_arg = ctx.api.analyze_type(args[0])
api = cast(SemanticAnalyzer, ctx.api.api) # type: ignore
if not isinstance(type_arg, Instance):
return ctx.api.analyze_type(ctx.type)
if not isinstance(type_arg, Instance) or not type_arg.type.has_base(MODEL_CLASS_FULLNAME):
return type_arg
fields_dict = None
if len(args) > 1:
+9 -2
View File
@@ -1,7 +1,7 @@
from mypy.plugin import AttributeContext
from mypy.plugin import AttributeContext, MethodContext
from mypy.types import Instance
from mypy.types import Type as MypyType
from mypy.types import UnionType
from mypy.types import UninhabitedType, UnionType
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import helpers
@@ -35,3 +35,10 @@ def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_c
return ctx.default_attr_type
return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])])
def check_querydict_is_mutable(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
ret_type = ctx.default_return_type
if isinstance(ret_type, UninhabitedType):
ctx.api.fail("This QueryDict is immutable.", ctx.context)
return ret_type