mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-13 07:21:56 +08:00
* Type the return value of lazy translation functions as Promise. The return value of the lazy translation functions is a proxied `Promise` object. https://github.com/django/django/blob/3.2.6/django/utils/translation/__init__.py#L135-L221. Signed-off-by: Zixuan James Li <p359101898@gmail.com> * Mark unicode translation functions for deprecation. https://docs.djangoproject.com/en/4.0/releases/4.0/#features-removed-in-4-0. Signed-off-by: Zixuan James Li <p359101898@gmail.com> * Add proxied functions for Promise. Although there is nothing defined in `Promise` itself, the only instances of `Promise` are created by the `lazy` function, with magic methods defined on it. https://github.com/django/django/blob/3.2.6/django/utils/functional.py#L84-L191. Signed-off-by: Zixuan James Li <p359101898@gmail.com> * Add _StrPromise as a special type for Promise objects for str. This allows the user to access methods defined on lazy strings while still letting mypy be aware of that they are not instances of `str`. The definitions for some of the magic methods are pulled from typeshed. We need those definitions in the stubs so that `_StrPromise` objects will work properly with operators, as refining operator types is tricky with the mypy plugins API. The rest of the methods will be covered by an attribute hook. Signed-off-by: Zixuan James Li <p359101898@gmail.com> * Implement _StrPromise attribute hook. This implements an attribute hook that provides type information for methods that are available on `builtins.str` for `_StrPromise` except the supported operators. This allows us to avoid copying stubs from the builtins for all supported methods on `str`. Signed-off-by: Zixuan James Li <p359101898@gmail.com> * Allow message being a _StrPromise object for RegexValidator. One intended usage of lazystr is to postpone the translation of the error message of a validation error. It is possible that we pass a Promise (specifically _StrPromise) and only evaluate it when a ValidationError is raised. Signed-off-by: Zixuan James Li <p359101898@gmail.com> * Refactor _StrPromise attribtue hook with analyze_member_access. Signed-off-by: Zixuan James Li <p359101898@gmail.com> Signed-off-by: Zixuan James Li <p359101898@gmail.com>
314 lines
14 KiB
Python
314 lines
14 KiB
Python
import sys
|
|
from functools import partial
|
|
from typing import Callable, Dict, List, Optional, Tuple, Type
|
|
|
|
from django.db.models.fields.related import RelatedField
|
|
from mypy.modulefinder import mypy_path
|
|
from mypy.nodes import MypyFile, TypeInfo
|
|
from mypy.options import Options
|
|
from mypy.plugin import (
|
|
AnalyzeTypeContext,
|
|
AttributeContext,
|
|
ClassDefContext,
|
|
DynamicClassDefContext,
|
|
FunctionContext,
|
|
MethodContext,
|
|
Plugin,
|
|
)
|
|
from mypy.types import Type as MypyType
|
|
|
|
import mypy_django_plugin.transformers.orm_lookups
|
|
from mypy_django_plugin.config import DjangoPluginConfig
|
|
from mypy_django_plugin.django.context import DjangoContext
|
|
from mypy_django_plugin.lib import fullnames, helpers
|
|
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
|
|
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
|
|
from mypy_django_plugin.transformers.managers import (
|
|
create_new_manager_class_from_from_queryset_method,
|
|
resolve_manager_method,
|
|
)
|
|
from mypy_django_plugin.transformers.models import (
|
|
handle_annotated_type,
|
|
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:
|
|
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.MODEL_CLASS_FULLNAME)
|
|
|
|
if sym is not None and isinstance(sym.node, TypeInfo):
|
|
helpers.get_django_metadata(sym.node)["model_bases"][ctx.cls.fullname] = 1
|
|
else:
|
|
if not ctx.api.final_iteration:
|
|
ctx.api.defer()
|
|
return
|
|
|
|
process_model_class(ctx, django_context)
|
|
|
|
|
|
def transform_form_class(ctx: ClassDefContext) -> None:
|
|
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME)
|
|
if sym is not None and isinstance(sym.node, TypeInfo):
|
|
helpers.get_django_metadata(sym.node)["baseform_bases"][ctx.cls.fullname] = 1
|
|
|
|
forms.make_meta_nested_class_inherit_from_any(ctx)
|
|
|
|
|
|
def add_new_manager_base_hook(ctx: ClassDefContext) -> None:
|
|
helpers.add_new_manager_base(ctx.api, ctx.cls.fullname)
|
|
|
|
|
|
class NewSemanalDjangoPlugin(Plugin):
|
|
def __init__(self, options: Options) -> None:
|
|
super().__init__(options)
|
|
self.plugin_config = DjangoPluginConfig(options.config_file)
|
|
# Add paths from MYPYPATH env var
|
|
sys.path.extend(mypy_path())
|
|
# Add paths from mypy_path config option
|
|
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]:
|
|
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( # type: ignore[no-any-return]
|
|
"queryset_bases", {fullnames.QUERYSET_CLASS_FULLNAME: 1}
|
|
)
|
|
else:
|
|
return {}
|
|
|
|
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( # type: ignore[no-any-return]
|
|
"manager_bases", {fullnames.MANAGER_CLASS_FULLNAME: 1}
|
|
)
|
|
else:
|
|
return {}
|
|
|
|
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( # type: ignore[no-any-return]
|
|
"model_bases", {fullnames.MODEL_CLASS_FULLNAME: 1}
|
|
)
|
|
else:
|
|
return {}
|
|
|
|
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( # type: ignore[no-any-return]
|
|
"baseform_bases",
|
|
{
|
|
fullnames.BASEFORM_CLASS_FULLNAME: 1,
|
|
fullnames.FORM_CLASS_FULLNAME: 1,
|
|
fullnames.MODELFORM_CLASS_FULLNAME: 1,
|
|
},
|
|
)
|
|
else:
|
|
return {}
|
|
|
|
def _get_typeinfo_or_none(self, class_name: str) -> Optional[TypeInfo]:
|
|
sym = self.lookup_fully_qualified(class_name)
|
|
if sym is not None and isinstance(sym.node, TypeInfo):
|
|
return sym.node
|
|
return None
|
|
|
|
def _new_dependency(self, module: str) -> Tuple[int, str, int]:
|
|
return 10, module, -1
|
|
|
|
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)]
|
|
|
|
# for values / values_list
|
|
if file.fullname == "django.db.models":
|
|
return [self._new_dependency("mypy_extensions"), self._new_dependency("typing")]
|
|
|
|
# for `get_user_model()`
|
|
if self.django_context.settings:
|
|
if file.fullname == "django.contrib.auth" or file.fullname in {"django.http", "django.http.request"}:
|
|
auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
|
|
try:
|
|
auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__
|
|
except LookupError:
|
|
# get_user_model() model app is not installed
|
|
return []
|
|
return [self._new_dependency(auth_user_module)]
|
|
|
|
# ensure that all mentioned to='someapp.SomeModel' are loaded with corresponding related Fields
|
|
defined_model_classes = self.django_context.model_modules.get(file.fullname)
|
|
if not defined_model_classes:
|
|
return []
|
|
deps = set()
|
|
for model_class in defined_model_classes:
|
|
# forward relations
|
|
for field in self.django_context.get_model_fields(model_class):
|
|
if isinstance(field, RelatedField):
|
|
related_model_cls = self.django_context.get_field_related_model_cls(field)
|
|
if related_model_cls is None:
|
|
continue
|
|
related_model_module = related_model_cls.__module__
|
|
if related_model_module != file.fullname:
|
|
deps.add(self._new_dependency(related_model_module))
|
|
# reverse relations
|
|
# `related_objects` is private API (according to docstring)
|
|
for relation in model_class._meta.related_objects: # type: ignore[attr-defined]
|
|
related_model_cls = self.django_context.get_field_related_model_cls(relation)
|
|
related_model_module = related_model_cls.__module__
|
|
if related_model_module != file.fullname:
|
|
deps.add(self._new_dependency(related_model_module))
|
|
return list(deps) + [
|
|
# for QuerySet.annotate
|
|
self._new_dependency("django_stubs_ext"),
|
|
# For BaseManager.from_queryset
|
|
self._new_dependency("django.db.models.query"),
|
|
]
|
|
|
|
def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], MypyType]]:
|
|
if fullname == "django.contrib.auth.get_user_model":
|
|
return partial(settings.get_user_model_hook, django_context=self.django_context)
|
|
|
|
manager_bases = self._get_current_manager_bases()
|
|
if fullname in manager_bases:
|
|
return querysets.determine_proper_manager_type
|
|
|
|
info = self._get_typeinfo_or_none(fullname)
|
|
if info:
|
|
if info.has_base(fullnames.FIELD_FULLNAME):
|
|
return partial(fields.transform_into_proper_return_type, django_context=self.django_context)
|
|
|
|
if helpers.is_model_subclass_info(info, self.django_context):
|
|
return partial(init_create.redefine_and_typecheck_model_init, django_context=self.django_context)
|
|
return None
|
|
|
|
def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], MypyType]]:
|
|
class_fullname, _, method_name = fullname.rpartition(".")
|
|
# 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
|
|
|
|
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
|
|
|
|
manager_classes = self._get_current_manager_bases()
|
|
|
|
if method_name == "values":
|
|
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, django_context=self.django_context)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
elif class_fullname in manager_classes and method_name == "create":
|
|
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
|
|
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,
|
|
)
|
|
|
|
return None
|
|
|
|
def get_base_class_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]:
|
|
# Base class is a Model class definition
|
|
if (
|
|
fullname in self.django_context.all_registered_model_class_fullnames
|
|
or fullname in self._get_current_model_bases()
|
|
):
|
|
return partial(transform_model_class, django_context=self.django_context)
|
|
|
|
# Base class is a Manager class definition
|
|
if fullname in self._get_current_manager_bases():
|
|
return add_new_manager_base_hook
|
|
|
|
# Base class is a Form class definition
|
|
if fullname in self._get_current_form_bases():
|
|
return transform_form_class
|
|
return None
|
|
|
|
def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeContext], MypyType]]:
|
|
class_name, _, attr_name = fullname.rpartition(".")
|
|
|
|
# Lookup of a settings variable
|
|
if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS:
|
|
return partial(settings.get_type_of_settings_attribute, django_context=self.django_context)
|
|
|
|
info = self._get_typeinfo_or_none(class_name)
|
|
|
|
# Lookup of the '.is_superuser' attribute
|
|
if info and info.has_base(fullnames.PERMISSION_MIXIN_CLASS_FULLNAME) and attr_name == "is_superuser":
|
|
return partial(set_auth_user_model_boolean_fields, django_context=self.django_context)
|
|
|
|
# Lookup of the 'request.user' attribute
|
|
if info and info.has_base(fullnames.HTTPREQUEST_CLASS_FULLNAME) and attr_name == "user":
|
|
return partial(request.set_auth_user_model_as_type_for_request_user, django_context=self.django_context)
|
|
|
|
# Lookup of the 'user.is_staff' or 'user.is_active' attribute
|
|
if info and info.has_base(fullnames.ABSTRACT_USER_MODEL_FULLNAME) and attr_name in ("is_staff", "is_active"):
|
|
return partial(set_auth_user_model_boolean_fields, django_context=self.django_context)
|
|
|
|
# Lookup of a method on a dynamically generated manager class
|
|
# i.e. a manager class only existing while mypy is running, not collected from the AST
|
|
if (
|
|
info
|
|
and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME)
|
|
and "from_queryset_manager" in helpers.get_django_metadata(info)
|
|
):
|
|
return resolve_manager_method
|
|
|
|
if info and info.has_base(fullnames.STR_PROMISE_FULLNAME):
|
|
return resolve_str_promise_attribute
|
|
|
|
return None
|
|
|
|
def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
|
|
if fullname in (
|
|
"typing.Annotated",
|
|
"typing_extensions.Annotated",
|
|
"django_stubs_ext.annotations.WithAnnotations",
|
|
):
|
|
return partial(handle_annotated_type, django_context=self.django_context)
|
|
|
|
def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
|
|
# Create a new manager class definition when a manager's '.from_queryset' classmethod is called
|
|
if fullname.endswith("from_queryset"):
|
|
class_name, _, _ = fullname.rpartition(".")
|
|
info = self._get_typeinfo_or_none(class_name)
|
|
if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME):
|
|
return create_new_manager_class_from_from_queryset_method
|
|
return None
|
|
|
|
|
|
def plugin(version: str) -> Type[NewSemanalDjangoPlugin]:
|
|
return NewSemanalDjangoPlugin
|