diff --git a/django-stubs/contrib/auth/__init__.pyi b/django-stubs/contrib/auth/__init__.pyi index 62a3beb5b..1e9413413 100644 --- a/django-stubs/contrib/auth/__init__.pyi +++ b/django-stubs/contrib/auth/__init__.pyi @@ -1,7 +1,7 @@ from typing import Any from django.contrib.auth.backends import BaseBackend -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import _UserModel from django.contrib.auth.models import AnonymousUser from django.db.models.options import Options from django.http.request import HttpRequest @@ -18,19 +18,17 @@ REDIRECT_FIELD_NAME: str def load_backend(path: str) -> BaseBackend: ... def get_backends() -> list[BaseBackend]: ... -def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> AbstractBaseUser | None: ... -async def aauthenticate(request: HttpRequest | None = ..., **credentials: Any) -> AbstractBaseUser | None: ... -def login( - request: HttpRequest, user: AbstractBaseUser | None, backend: type[BaseBackend] | str | None = ... -) -> None: ... +def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> _UserModel | None: ... +async def aauthenticate(request: HttpRequest | None = ..., **credentials: Any) -> _UserModel | None: ... +def login(request: HttpRequest, user: _UserModel | None, backend: type[BaseBackend] | str | None = ...) -> None: ... async def alogin( - request: HttpRequest, user: AbstractBaseUser | None, backend: type[BaseBackend] | str | None = ... + request: HttpRequest, user: _UserModel | None, backend: type[BaseBackend] | str | None = ... ) -> None: ... def logout(request: HttpRequest) -> None: ... async def alogout(request: HttpRequest) -> None: ... -def get_user_model() -> type[AbstractBaseUser]: ... -def get_user(request: HttpRequest | Client) -> AbstractBaseUser | AnonymousUser: ... -async def aget_user(request: HttpRequest | Client) -> AbstractBaseUser | AnonymousUser: ... +def get_user_model() -> type[_UserModel]: ... +def get_user(request: HttpRequest | Client) -> _UserModel | AnonymousUser: ... +async def aget_user(request: HttpRequest | Client) -> _UserModel | AnonymousUser: ... def get_permission_codename(action: str, opts: Options) -> str: ... -def update_session_auth_hash(request: HttpRequest, user: AbstractBaseUser) -> None: ... -async def aupdate_session_auth_hash(request: HttpRequest, user: AbstractBaseUser) -> None: ... +def update_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ... +async def aupdate_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ... diff --git a/django-stubs/contrib/auth/backends.pyi b/django-stubs/contrib/auth/backends.pyi index 87d0bf825..c63628db4 100644 --- a/django-stubs/contrib/auth/backends.pyi +++ b/django-stubs/contrib/auth/backends.pyi @@ -1,19 +1,18 @@ from typing import Any, TypeVar -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import AbstractBaseUser, _UserModel from django.contrib.auth.models import AnonymousUser, Permission from django.db.models import QuerySet from django.db.models.base import Model from django.http.request import HttpRequest from typing_extensions import TypeAlias -_AnyUser: TypeAlias = AbstractBaseUser | AnonymousUser - -UserModel: Any +UserModel: TypeAlias = type[_UserModel] +_AnyUser: TypeAlias = _UserModel | AnonymousUser class BaseBackend: - def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> AbstractBaseUser | None: ... - def get_user(self, user_id: Any) -> AbstractBaseUser | None: ... + def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ... + def get_user(self, user_id: Any) -> _UserModel | None: ... def get_user_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ... def get_group_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ... def get_all_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ... @@ -22,7 +21,7 @@ class BaseBackend: class ModelBackend(BaseBackend): def authenticate( self, request: HttpRequest | None, username: str | None = ..., password: str | None = ..., **kwargs: Any - ) -> AbstractBaseUser | None: ... + ) -> _UserModel | None: ... def has_module_perms(self, user_obj: _AnyUser, app_label: str) -> bool: ... def user_can_authenticate(self, user: _AnyUser | None) -> bool: ... def with_perm( @@ -31,7 +30,7 @@ class ModelBackend(BaseBackend): is_active: bool = ..., include_superusers: bool = ..., obj: Model | None = ..., - ) -> QuerySet[AbstractBaseUser]: ... + ) -> QuerySet[_UserModel]: ... class AllowAllUsersModelBackend(ModelBackend): ... diff --git a/django-stubs/contrib/auth/base_user.pyi b/django-stubs/contrib/auth/base_user.pyi index 4740dd504..a99bdf431 100644 --- a/django-stubs/contrib/auth/base_user.pyi +++ b/django-stubs/contrib/auth/base_user.pyi @@ -4,6 +4,7 @@ from django.db import models from django.db.models.base import Model from django.db.models.expressions import Combinable from django.db.models.fields import BooleanField +from typing_extensions import TypeAlias _T = TypeVar("_T", bound=Model) @@ -41,3 +42,8 @@ class AbstractBaseUser(models.Model): @classmethod @overload def normalize_username(cls, username: Any) -> Any: ... + +# This is our "placeholder" type the mypy plugin refines to configured 'AUTH_USER_MODEL' +# wherever it is used as a type. The most recognised example of this is (probably) +# `HttpRequest.user` +_UserModel: TypeAlias = AbstractBaseUser # noqa: PYI047 diff --git a/django-stubs/contrib/auth/decorators.pyi b/django-stubs/contrib/auth/decorators.pyi index c4954e91b..7fcbe5257 100644 --- a/django-stubs/contrib/auth/decorators.pyi +++ b/django-stubs/contrib/auth/decorators.pyi @@ -1,13 +1,14 @@ from collections.abc import Callable, Iterable from typing import TypeVar, overload -from django.contrib.auth.models import AbstractBaseUser, AnonymousUser +from django.contrib.auth.base_user import _UserModel +from django.contrib.auth.models import AnonymousUser from django.http.response import HttpResponseBase _VIEW = TypeVar("_VIEW", bound=Callable[..., HttpResponseBase]) def user_passes_test( - test_func: Callable[[AbstractBaseUser | AnonymousUser], bool], + test_func: Callable[[_UserModel | AnonymousUser], bool], login_url: str | None = ..., redirect_field_name: str | None = ..., ) -> Callable[[_VIEW], _VIEW]: ... diff --git a/django-stubs/contrib/auth/forms.pyi b/django-stubs/contrib/auth/forms.pyi index 3b673b3e8..b5e4d31a0 100644 --- a/django-stubs/contrib/auth/forms.pyi +++ b/django-stubs/contrib/auth/forms.pyi @@ -2,7 +2,7 @@ from collections.abc import Iterable from typing import Any, TypeVar from django import forms -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import AbstractBaseUser, _UserModel from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.core.exceptions import ValidationError from django.db import models @@ -10,8 +10,9 @@ from django.db.models.fields import _ErrorMessagesDict from django.forms.fields import _ClassLevelWidgetT from django.forms.widgets import Widget from django.http.request import HttpRequest +from typing_extensions import TypeAlias -UserModel: type[AbstractBaseUser] +UserModel: TypeAlias = type[_UserModel] _User = TypeVar("_User", bound=AbstractBaseUser) class ReadOnlyPasswordHashWidget(forms.Widget): @@ -47,11 +48,11 @@ class AuthenticationForm(forms.Form): password: forms.Field error_messages: _ErrorMessagesDict request: HttpRequest | None - user_cache: Any + user_cache: _UserModel | None username_field: models.Field def __init__(self, request: HttpRequest | None = ..., *args: Any, **kwargs: Any) -> None: ... def confirm_login_allowed(self, user: AbstractBaseUser) -> None: ... - def get_user(self) -> AbstractBaseUser: ... + def get_user(self) -> _UserModel: ... def get_invalid_login_error(self) -> ValidationError: ... def clean(self) -> dict[str, Any]: ... @@ -66,7 +67,7 @@ class PasswordResetForm(forms.Form): to_email: str, html_email_template_name: str | None = ..., ) -> None: ... - def get_users(self, email: str) -> Iterable[AbstractBaseUser]: ... + def get_users(self, email: str) -> Iterable[_UserModel]: ... def save( self, domain_override: str | None = ..., diff --git a/django-stubs/contrib/auth/middleware.pyi b/django-stubs/contrib/auth/middleware.pyi index 86913a4f2..0465dccf5 100644 --- a/django-stubs/contrib/auth/middleware.pyi +++ b/django-stubs/contrib/auth/middleware.pyi @@ -1,10 +1,10 @@ -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import _UserModel from django.contrib.auth.models import AnonymousUser from django.http.request import HttpRequest from django.utils.deprecation import MiddlewareMixin -def get_user(request: HttpRequest) -> AnonymousUser | AbstractBaseUser: ... -async def auser(request: HttpRequest) -> AnonymousUser | AbstractBaseUser: ... +def get_user(request: HttpRequest) -> AnonymousUser | _UserModel: ... +async def auser(request: HttpRequest) -> AnonymousUser | _UserModel: ... class AuthenticationMiddleware(MiddlewareMixin): def process_request(self, request: HttpRequest) -> None: ... diff --git a/django-stubs/contrib/auth/password_validation.pyi b/django-stubs/contrib/auth/password_validation.pyi index 3f5554347..b9318e934 100644 --- a/django-stubs/contrib/auth/password_validation.pyi +++ b/django-stubs/contrib/auth/password_validation.pyi @@ -2,10 +2,7 @@ from collections.abc import Mapping, Sequence from pathlib import Path, PosixPath from typing import Any, Protocol, type_check_only -from django.db.models.base import Model -from typing_extensions import TypeAlias - -_UserModel: TypeAlias = Model +from django.contrib.auth.base_user import _UserModel @type_check_only class PasswordValidator(Protocol): diff --git a/django-stubs/contrib/auth/tokens.pyi b/django-stubs/contrib/auth/tokens.pyi index 21692dd72..05f05b5e0 100644 --- a/django-stubs/contrib/auth/tokens.pyi +++ b/django-stubs/contrib/auth/tokens.pyi @@ -1,17 +1,17 @@ from datetime import date, datetime from typing import Any -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import _UserModel class PasswordResetTokenGenerator: key_salt: str secret: str | bytes secret_fallbacks: list[str | bytes] algorithm: str - def make_token(self, user: AbstractBaseUser) -> str: ... - def check_token(self, user: AbstractBaseUser | None, token: str | None) -> bool: ... - def _make_token_with_timestamp(self, user: AbstractBaseUser, timestamp: int, secret: str | bytes = ...) -> str: ... - def _make_hash_value(self, user: AbstractBaseUser, timestamp: int) -> str: ... + def make_token(self, user: _UserModel) -> str: ... + def check_token(self, user: _UserModel | None, token: str | None) -> bool: ... + def _make_token_with_timestamp(self, user: _UserModel, timestamp: int, secret: str | bytes = ...) -> str: ... + def _make_hash_value(self, user: _UserModel, timestamp: int) -> str: ... def _num_seconds(self, dt: datetime | date) -> int: ... def _now(self) -> datetime: ... diff --git a/django-stubs/contrib/auth/views.pyi b/django-stubs/contrib/auth/views.pyi index 8992d334a..443a7855a 100644 --- a/django-stubs/contrib/auth/views.pyi +++ b/django-stubs/contrib/auth/views.pyi @@ -1,13 +1,14 @@ from typing import Any -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import _UserModel from django.contrib.auth.forms import AuthenticationForm from django.http.request import HttpRequest from django.http.response import HttpResponse, HttpResponseRedirect from django.views.generic.base import TemplateView from django.views.generic.edit import FormView +from typing_extensions import TypeAlias -UserModel: Any +UserModel: TypeAlias = type[_UserModel] class RedirectURLMixin: next_page: str | None @@ -65,7 +66,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView): token_generator: Any validlink: bool user: Any - def get_user(self, uidb64: str) -> AbstractBaseUser | None: ... + def get_user(self, uidb64: str) -> _UserModel | None: ... class PasswordResetCompleteView(PasswordContextMixin, TemplateView): title: Any diff --git a/django-stubs/http/request.pyi b/django-stubs/http/request.pyi index 21e58ed54..520f0ba5f 100644 --- a/django-stubs/http/request.pyi +++ b/django-stubs/http/request.pyi @@ -4,7 +4,7 @@ from io import BytesIO from re import Pattern from typing import Any, Awaitable, BinaryIO, Callable, Literal, NoReturn, TypeVar, overload, type_check_only -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import _UserModel from django.contrib.auth.models import AnonymousUser from django.contrib.sessions.backends.base import SessionBase from django.contrib.sites.models import Site @@ -55,9 +55,9 @@ class HttpRequest(BytesIO): # django.contrib.admin views: current_app: str # django.contrib.auth.middleware.AuthenticationMiddleware: - user: AbstractBaseUser | AnonymousUser + user: _UserModel | AnonymousUser # django.contrib.auth.middleware.AuthenticationMiddleware: - auser: Callable[[], Awaitable[AbstractBaseUser | AnonymousUser]] + auser: Callable[[], Awaitable[_UserModel | AnonymousUser]] # django.middleware.locale.LocaleMiddleware: LANGUAGE_CODE: str # django.contrib.sites.middleware.CurrentSiteMiddleware diff --git a/django-stubs/test/client.pyi b/django-stubs/test/client.pyi index ada535a54..a67b06458 100644 --- a/django-stubs/test/client.pyi +++ b/django-stubs/test/client.pyi @@ -8,7 +8,7 @@ from types import TracebackType from typing import Any, Generic, Literal, NoReturn, TypedDict, TypeVar, type_check_only from asgiref.typing import ASGIVersions -from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.base_user import _UserModel from django.contrib.sessions.backends.base import SessionBase from django.core.handlers.asgi import ASGIRequest from django.core.handlers.base import BaseHandler @@ -213,8 +213,8 @@ class ClientMixin: async def asession(self) -> SessionBase: ... def login(self, **credentials: Any) -> bool: ... async def alogin(self, **credentials: Any) -> bool: ... - def force_login(self, user: AbstractBaseUser, backend: str | None = ...) -> None: ... - async def aforce_login(self, user: AbstractBaseUser, backend: str | None = ...) -> None: ... + def force_login(self, user: _UserModel, backend: str | None = ...) -> None: ... + async def aforce_login(self, user: _UserModel, backend: str | None = ...) -> None: ... def logout(self) -> None: ... async def alogout(self) -> None: ... diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py index 19474a8c1..e4f8c15d9 100644 --- a/mypy_django_plugin/config.py +++ b/mypy_django_plugin/config.py @@ -123,9 +123,10 @@ def parse_ini_file(self, filepath: Path) -> None: except ValueError: exit_with_error(INVALID_BOOL_SETTING.format(key="strict_settings")) - def to_json(self) -> Dict[str, Any]: + def to_json(self, extra_data: Dict[str, Any]) -> Dict[str, Any]: """We use this method to reset mypy cache via `report_config_data` hook.""" return { "django_settings_module": self.django_settings_module, "strict_settings": self.strict_settings, + **dict(sorted(extra_data.items())), } diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index b78de30d3..b3a9516e5 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -535,3 +535,7 @@ def resolve_lookup_expected_type( def resolve_f_expression_type(self, f_expression_type: Instance) -> ProperType: return AnyType(TypeOfAny.explicit) + + @cached_property + def is_contrib_auth_installed(self) -> bool: + return "django.contrib.auth" in self.settings.INSTALLED_APPS diff --git a/mypy_django_plugin/lib/fullnames.py b/mypy_django_plugin/lib/fullnames.py index 0640bce3d..0f71c6dad 100644 --- a/mypy_django_plugin/lib/fullnames.py +++ b/mypy_django_plugin/lib/fullnames.py @@ -1,3 +1,4 @@ +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" MODEL_METACLASS_FULLNAME = "django.db.models.base.ModelBase" @@ -62,7 +63,7 @@ DJANGO_ABSTRACT_MODELS = frozenset( ( - "django.contrib.auth.base_user.AbstractBaseUser", + ABSTRACT_BASE_USER_MODEL_FULLNAME, ABSTRACT_USER_MODEL_FULLNAME, PERMISSION_MIXIN_CLASS_FULLNAME, "django.contrib.sessions.base_session.AbstractBaseSession", diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 10377dae1..69bb54c06 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -32,9 +32,9 @@ meta, orm_lookups, querysets, - request, settings, ) +from mypy_django_plugin.transformers.auth import get_user_model from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute from mypy_django_plugin.transformers.managers import ( add_as_manager_to_queryset_class, @@ -110,15 +110,14 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: return [self._new_dependency("typing"), self._new_dependency("django_stubs_ext")] # 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), self._new_dependency("django_stubs_ext")] + 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), self._new_dependency("django_stubs_ext")] # ensure that all mentioned to='someapp.SomeModel' are loaded with corresponding related Fields defined_model_classes = self.django_context.model_modules.get(file.fullname) @@ -149,9 +148,6 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: ] 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) - info = self._get_typeinfo_or_none(fullname) if info: if info.has_base(fullnames.FIELD_FULLNAME): @@ -270,10 +266,6 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte 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) @@ -299,8 +291,9 @@ def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeType "django_stubs_ext.annotations.WithAnnotations", ): return partial(handle_annotated_type, fullname=fullname) - else: - return None + elif fullname == "django.contrib.auth.base_user._UserModel": + return partial(get_user_model, django_context=self.django_context) + return None 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 @@ -313,7 +306,12 @@ def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicCla def report_config_data(self, ctx: ReportConfigContext) -> Dict[str, Any]: # Cache would be cleared if any settings do change. - return self.plugin_config.to_json() + extra_data = {} + # In all places we use '_UserModel' alias as a type we want to clear cache if + # AUTH_USER_MODEL setting changes + if ctx.id.startswith("django.contrib.auth") or ctx.id in {"django.http.request", "django.test.client"}: + extra_data["AUTH_USER_MODEL"] = self.django_context.settings.AUTH_USER_MODEL + return self.plugin_config.to_json(extra_data) def plugin(version: str) -> Type[NewSemanalDjangoPlugin]: diff --git a/mypy_django_plugin/transformers/auth.py b/mypy_django_plugin/transformers/auth.py new file mode 100644 index 000000000..383a6866a --- /dev/null +++ b/mypy_django_plugin/transformers/auth.py @@ -0,0 +1,38 @@ +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 + + +def get_user_model(ctx: AnalyzeTypeContext, django_context: DjangoContext) -> MypyType: + assert isinstance(ctx.api, TypeAnalyser) + assert isinstance(ctx.api.api, SemanticAnalyzer) + + def get_abstract_base_user(api: SemanticAnalyzer) -> ProperType: + sym = api.lookup_fully_qualified(fullnames.ABSTRACT_BASE_USER_MODEL_FULLNAME) + assert isinstance(sym.node, TypeInfo) + return fill_typevars_with_any(sym.node) + + if not django_context.is_contrib_auth_installed: + return get_abstract_base_user(ctx.api.api) + + auth_user_model = django_context.settings.AUTH_USER_MODEL + model_info = helpers.resolve_lazy_reference( + auth_user_model, api=ctx.api.api, django_context=django_context, ctx=ctx.context + ) + if model_info is None: + fullname = django_context.model_class_fullnames_by_label.get(auth_user_model) + if fullname is not None: + # When we've tried to resolve 'AUTH_USER_MODEL' but got no class back but + # we notice that its value is recognised we'll return a placeholder for + # the class as we expect it to exist later on. + return PlaceholderType(fullname=fullname, args=[], line=ctx.context.line) + return get_abstract_base_user(ctx.api.api) + + return fill_typevars_with_any(model_info) diff --git a/mypy_django_plugin/transformers/request.py b/mypy_django_plugin/transformers/request.py index 872fb0cf8..25c12be77 100644 --- a/mypy_django_plugin/transformers/request.py +++ b/mypy_django_plugin/transformers/request.py @@ -1,39 +1,8 @@ -from mypy.plugin import AttributeContext, MethodContext -from mypy.types import Instance, UninhabitedType, UnionType, get_proper_type +from mypy.plugin import MethodContext from mypy.types import Type as MypyType +from mypy.types import UninhabitedType, get_proper_type from mypy_django_plugin.django.context import DjangoContext -from mypy_django_plugin.lib import helpers - - -def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType: - if not django_context.apps_registry.is_installed("django.contrib.auth"): - return ctx.default_attr_type - - # Imported here because django isn't properly loaded yet when module is loaded - from django.contrib.auth.base_user import AbstractBaseUser - from django.contrib.auth.models import AnonymousUser - - abstract_base_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AbstractBaseUser) - anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser) - - # This shouldn't be able to happen, as we managed to import the models above. - assert abstract_base_user_info is not None - assert anonymous_user_info is not None - - if ctx.default_attr_type != UnionType([Instance(abstract_base_user_info, []), Instance(anonymous_user_info, [])]): - # Type has been changed from the default in django-stubs. - # I.e. HttpRequest has been subclassed and user-type overridden, so let's leave it as is. - return ctx.default_attr_type - - auth_user_model = django_context.settings.AUTH_USER_MODEL - user_cls = django_context.apps_registry.get_model(auth_user_model) - user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), user_cls) - - if user_info is None: - 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: diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index 81056643a..43a62ff24 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -1,6 +1,6 @@ from mypy.nodes import MemberExpr -from mypy.plugin import AttributeContext, FunctionContext -from mypy.types import AnyType, Instance, TypeOfAny, TypeType +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 @@ -8,18 +8,6 @@ from mypy_django_plugin.lib import helpers -def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: - auth_user_model = django_context.settings.AUTH_USER_MODEL - model_cls = django_context.apps_registry.get_model(auth_user_model) - model_cls_fullname = helpers.get_class_fullname(model_cls) - - model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), model_cls_fullname) - if model_info is None: - return AnyType(TypeOfAny.unannotated) - - return TypeType(Instance(model_info, [])) - - def get_type_of_settings_attribute( ctx: AttributeContext, django_context: DjangoContext, plugin_config: DjangoPluginConfig ) -> MypyType: diff --git a/scripts/stubtest/allowlist.txt b/scripts/stubtest/allowlist.txt index fba160836..2ac951136 100644 --- a/scripts/stubtest/allowlist.txt +++ b/scripts/stubtest/allowlist.txt @@ -472,3 +472,8 @@ django.contrib.sessions.base_session.AbstractBaseSession@AnnotatedWith django.contrib.sessions.models.Session@AnnotatedWith django.contrib.sites.models.Site@AnnotatedWith django.db.migrations.recorder.Migration@AnnotatedWith + +# These are `UserModel = get_user_model()` lines, the plugin updates them to correct types +django.contrib.auth.backends.UserModel +django.contrib.auth.forms.UserModel +django.contrib.auth.views.UserModel diff --git a/scripts/stubtest/allowlist_todo.txt b/scripts/stubtest/allowlist_todo.txt index ef1e3d326..60f4f8998 100644 --- a/scripts/stubtest/allowlist_todo.txt +++ b/scripts/stubtest/allowlist_todo.txt @@ -85,7 +85,6 @@ django.contrib.auth.decorators.login_required django.contrib.auth.forms.BaseUserCreationForm.declared_fields django.contrib.auth.forms.UserChangeForm.declared_fields django.contrib.auth.forms.UserCreationForm.declared_fields -django.contrib.auth.forms.UserModel django.contrib.auth.hashers.Argon2PasswordHasher.params django.contrib.auth.hashers.SHA1PasswordHasher.__init__ django.contrib.auth.hashers.ScryptPasswordHasher diff --git a/tests/typecheck/contrib/auth/test_auth.yml b/tests/typecheck/contrib/auth/test_auth.yml new file mode 100644 index 000000000..752940258 --- /dev/null +++ b/tests/typecheck/contrib/auth/test_auth.yml @@ -0,0 +1,95 @@ +- case: test_objects_using_auth_user_model_picks_up_configured_type + main: | + from typing import Union + from django.contrib.auth import authenticate, aauthenticate, get_user, get_user_model, login + from django.contrib.auth.backends import ModelBackend + from django.contrib.auth.decorators import user_passes_test + from django.contrib.auth.models import AnonymousUser + from django.http import HttpRequest, HttpResponse + from myapp.models import MyUser + + reveal_type(authenticate()) # N: Revealed type is "Union[myapp.models.MyUser, None]" + async def f() -> None: + reveal_type(await aauthenticate()) # N: Revealed type is "Union[myapp.models.MyUser, None]" + reveal_type(get_user_model()) # N: Revealed type is "Type[myapp.models.MyUser]" + reveal_type(login) # N: Revealed type is "def (request: django.http.request.HttpRequest, user: Union[myapp.models.MyUser, None], backend: Union[Type[django.contrib.auth.backends.BaseBackend], builtins.str, None] =)" + reveal_type(get_user) # N: Revealed type is "def (request: Union[django.http.request.HttpRequest, django.test.client.Client]) -> Union[myapp.models.MyUser, django.contrib.auth.models.AnonymousUser]" + reveal_type(ModelBackend().authenticate(None)) # N: Revealed type is "Union[myapp.models.MyUser, None]" + + def check_user(user: Union[MyUser, AnonymousUser]) -> bool: return True + @user_passes_test(check_user) + def view(request: HttpRequest) -> HttpResponse: ... + custom_settings: | + INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "myapp") + AUTH_USER_MODEL = "myapp.MyUser" + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser(models.Model): + ... + +- case: test_objects_using_auth_user_model_uses_builtin_auth_user_per_default + main: | + from typing import Union + from django.contrib.auth import authenticate, aauthenticate, get_user, get_user_model, login + from django.contrib.auth.backends import ModelBackend + from django.contrib.auth.decorators import user_passes_test + from django.contrib.auth.models import AnonymousUser, User + from django.http import HttpRequest, HttpResponse + + reveal_type(authenticate()) # N: Revealed type is "Union[django.contrib.auth.models.User, None]" + async def f() -> None: + reveal_type(await aauthenticate()) # N: Revealed type is "Union[django.contrib.auth.models.User, None]" + reveal_type(get_user_model()) # N: Revealed type is "Type[django.contrib.auth.models.User]" + reveal_type(login) # N: Revealed type is "def (request: django.http.request.HttpRequest, user: Union[django.contrib.auth.models.User, None], backend: Union[Type[django.contrib.auth.backends.BaseBackend], builtins.str, None] =)" + reveal_type(get_user) # N: Revealed type is "def (request: Union[django.http.request.HttpRequest, django.test.client.Client]) -> Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]" + reveal_type(ModelBackend().authenticate(None)) # N: Revealed type is "Union[django.contrib.auth.models.User, None]" + + def check_user(user: Union[User, AnonymousUser]) -> bool: return True + @user_passes_test(check_user) + def view(request: HttpRequest) -> HttpResponse: ... + custom_settings: | + INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "myapp") + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser(models.Model): + ... + +- case: test_objects_for_auth_user_model_returns_stub_types_when_contrib_auth_is_not_installed + main: | + from typing import Union + from django.contrib.auth import authenticate, aauthenticate, get_user, get_user_model, login + from django.contrib.auth.backends import ModelBackend + from django.contrib.auth.base_user import AbstractBaseUser + from django.contrib.auth.decorators import user_passes_test + from django.contrib.auth.models import AnonymousUser + from django.http import HttpRequest, HttpResponse + + reveal_type(authenticate()) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, None]" + async def f() -> None: + reveal_type(await aauthenticate()) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, None]" + reveal_type(get_user_model()) # N: Revealed type is "Type[django.contrib.auth.base_user.AbstractBaseUser]" + reveal_type(login) # N: Revealed type is "def (request: django.http.request.HttpRequest, user: Union[django.contrib.auth.base_user.AbstractBaseUser, None], backend: Union[Type[django.contrib.auth.backends.BaseBackend], builtins.str, None] =)" + reveal_type(get_user) # N: Revealed type is "def (request: Union[django.http.request.HttpRequest, django.test.client.Client]) -> Union[django.contrib.auth.base_user.AbstractBaseUser, django.contrib.auth.models.AnonymousUser]" + reveal_type(ModelBackend().authenticate(None)) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, None]" + + def check_user(user: Union[AbstractBaseUser, AnonymousUser]) -> bool: return True + @user_passes_test(check_user) + def view(request: HttpRequest) -> HttpResponse: ... + custom_settings: | + INSTALLED_APPS = ("django.contrib.contenttypes", "myapp") + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser(models.Model): + ... diff --git a/tests/typecheck/test_request.yml b/tests/typecheck/test_request.yml index 4203f16e5..b3c4dacbe 100644 --- a/tests/typecheck/test_request.yml +++ b/tests/typecheck/test_request.yml @@ -1,5 +1,4 @@ - case: request_object_has_user_of_type_auth_user_model - disable_cache: true main: | from django.http.request import HttpRequest reveal_type(HttpRequest().user) # N: Revealed type is "Union[myapp.models.MyUser, django.contrib.auth.models.AnonymousUser]" @@ -16,7 +15,6 @@ class MyUser(models.Model): pass - case: request_object_user_can_be_descriminated - disable_cache: true main: | from django.http.request import HttpRequest request = HttpRequest() @@ -29,7 +27,6 @@ custom_settings: | INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth') - case: request_object_user_without_auth_and_contenttypes_apps - disable_cache: true main: | from django.http.request import HttpRequest request = HttpRequest() @@ -37,7 +34,6 @@ if request.user.is_authenticated: reveal_type(request.user) # N: Revealed type is "django.contrib.auth.base_user.AbstractBaseUser" - case: request_object_user_without_auth_but_with_contenttypes_apps - disable_cache: true main: | from django.http.request import HttpRequest request = HttpRequest() @@ -47,7 +43,6 @@ custom_settings: | INSTALLED_APPS = ('django.contrib.contenttypes',) - case: subclass_request_not_changed_user_type - disable_cache: true main: | from django.http.request import HttpRequest class MyRequest(HttpRequest): @@ -59,7 +54,6 @@ INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth') - case: subclass_request_changed_user_type - disable_cache: true main: | from django.http.request import HttpRequest from django.contrib.auth.models import User diff --git a/tests/typecheck/test_shortcuts.yml b/tests/typecheck/test_shortcuts.yml index 99769feef..438f3106f 100644 --- a/tests/typecheck/test_shortcuts.yml +++ b/tests/typecheck/test_shortcuts.yml @@ -21,13 +21,12 @@ pass - case: get_user_model_returns_proper_class - disable_cache: true main: | from django.contrib.auth import get_user_model UserModel = get_user_model() reveal_type(UserModel.objects) # N: Revealed type is "django.db.models.manager.Manager[myapp.models.MyUser]" custom_settings: | - INSTALLED_APPS = ('django.contrib.contenttypes', 'myapp') + INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth', 'myapp') AUTH_USER_MODEL = 'myapp.MyUser' files: - path: myapp/__init__.py