Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve configured AUTH_USER_MODEL with a get_type_analyze_hook #2335

Merged
merged 8 commits into from
Aug 12, 2024
Prev Previous commit
Next Next commit
Resolve configured AUTH_USER_MODEL with a get_type_analyze_hook
It's nothing special really. We declare a `TypeAlias` in the pyi, thus
getting a fullname, somewhere under `django.contrib.auth`. We then build
a hook for that fullname. And the hook simulates what
`django.contrib.auth.get_user_model` does

The alias is set up to point to `AbstractBaseUser`, so for a type
checker other than mypy nothing should've changed
flaeppe committed Aug 10, 2024

Verified

This commit was signed with the committer’s verified signature.
flaeppe Petter Friberg
commit ed300fdd08e2b96f6f9f9b963c9130734e12209e
22 changes: 10 additions & 12 deletions django-stubs/contrib/auth/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
15 changes: 7 additions & 8 deletions django-stubs/contrib/auth/backends.pyi
Original file line number Diff line number Diff line change
@@ -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 = _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): ...

7 changes: 6 additions & 1 deletion django-stubs/contrib/auth/base_user.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, ClassVar, Literal, TypeVar, overload
from typing import Any, ClassVar, Literal, TypeAlias, TypeVar, overload

from django.db import models
from django.db.models.base import Model
@@ -41,3 +41,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
5 changes: 3 additions & 2 deletions django-stubs/contrib/auth/decorators.pyi
Original file line number Diff line number Diff line change
@@ -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]: ...
10 changes: 5 additions & 5 deletions django-stubs/contrib/auth/forms.pyi
Original file line number Diff line number Diff line change
@@ -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
@@ -11,7 +11,7 @@ from django.forms.fields import _ClassLevelWidgetT
from django.forms.widgets import Widget
from django.http.request import HttpRequest

UserModel: type[AbstractBaseUser]
UserModel: type[_UserModel]
_User = TypeVar("_User", bound=AbstractBaseUser)

class ReadOnlyPasswordHashWidget(forms.Widget):
@@ -47,11 +47,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 +66,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 = ...,
6 changes: 3 additions & 3 deletions django-stubs/contrib/auth/middleware.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
5 changes: 1 addition & 4 deletions django-stubs/contrib/auth/password_validation.pyi
Original file line number Diff line number Diff line change
@@ -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):
10 changes: 5 additions & 5 deletions django-stubs/contrib/auth/tokens.pyi
Original file line number Diff line number Diff line change
@@ -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: ...

6 changes: 3 additions & 3 deletions django-stubs/contrib/auth/views.pyi
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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

UserModel: Any
UserModel: type[_UserModel]

class RedirectURLMixin:
next_page: str | None
@@ -65,7 +65,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
6 changes: 3 additions & 3 deletions django-stubs/http/request.pyi
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions django-stubs/test/client.pyi
Original file line number Diff line number Diff line change
@@ -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: ...

3 changes: 2 additions & 1 deletion mypy_django_plugin/lib/fullnames.py
Original file line number Diff line number Diff line change
@@ -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",
31 changes: 7 additions & 24 deletions mypy_django_plugin/main.py
Original file line number Diff line number Diff line change
@@ -32,10 +32,9 @@
meta,
orm_lookups,
querysets,
request,
settings,
)
from mypy_django_plugin.transformers.auth import update_authenticate_hook
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,
@@ -148,22 +147,7 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
self._new_dependency("django.db.models.query"),
]

@cached_property
def contrib_auth_hooks(self) -> Dict[str, Callable[[FunctionContext], MypyType]]:
authenticate_hook = partial(update_authenticate_hook, django_context=self.django_context)
return {
"django.contrib.auth.get_user_model": partial(
settings.get_user_model_hook, django_context=self.django_context
),
"django.contrib.auth.authenticate": authenticate_hook,
"django.contrib.auth.aauthenticate": authenticate_hook,
}

def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], MypyType]]:
auth_hook = self.contrib_auth_hooks.get(fullname)
if auth_hook is not None:
return auth_hook

info = self._get_typeinfo_or_none(fullname)
if info:
if info.has_base(fullnames.FIELD_FULLNAME):
@@ -282,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)
@@ -311,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
@@ -326,7 +307,9 @@ 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.
extra_data = {}
if ctx.id == "django.contrib.auth":
# 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)

42 changes: 25 additions & 17 deletions mypy_django_plugin/transformers/auth.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
from mypy.plugin import FunctionContext
from mypy.types import Instance, NoneType, UnionType, get_proper_type
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 helpers
from mypy_django_plugin.lib import fullnames, helpers


def update_authenticate_hook(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
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 ctx.default_return_type
return get_abstract_base_user(ctx.api.api)

auth_user_model = django_context.settings.AUTH_USER_MODEL
api = helpers.get_typechecker_api(ctx)
model_info = helpers.resolve_lazy_reference(
auth_user_model, api=api, django_context=django_context, ctx=ctx.context
auth_user_model, api=ctx.api.api, django_context=django_context, ctx=ctx.context
)
if model_info is None:
return ctx.default_return_type
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)

optional_model = UnionType([fill_typevars_with_any(model_info), NoneType()], ctx.context.line, ctx.context.column)
default_return_type = get_proper_type(ctx.default_return_type)
if isinstance(default_return_type, Instance) and default_return_type.type.fullname == "typing.Coroutine":
if len(default_return_type.args) == 3:
return default_return_type.copy_modified(
args=[default_return_type.args[0], default_return_type.args[1], optional_model]
)
return ctx.default_return_type
return optional_model
return fill_typevars_with_any(model_info)
35 changes: 2 additions & 33 deletions mypy_django_plugin/transformers/request.py
Original file line number Diff line number Diff line change
@@ -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:
16 changes: 2 additions & 14 deletions mypy_django_plugin/transformers/settings.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
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
from mypy_django_plugin.django.context import DjangoContext
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:
4 changes: 4 additions & 0 deletions scripts/stubtest/allowlist.txt
Original file line number Diff line number Diff line change
@@ -472,3 +472,7 @@ 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.forms.UserModel
django.contrib.auth.views.UserModel
1 change: 0 additions & 1 deletion scripts/stubtest/allowlist_todo.txt
Original file line number Diff line number Diff line change
@@ -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
40 changes: 28 additions & 12 deletions tests/typecheck/contrib/auth/test_auth.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
- case: test_authenticate_returns_configured_auth_user_model
- case: test_objects_using_auth_user_model_picks_up_configured_type
main: |
from django.contrib.auth import authenticate, aauthenticate
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"
@@ -16,14 +30,15 @@
class MyUser(models.Model):
...
- case: test_authenticate_returns_stubs_types_when_contrib_auth_is_not_installed
- case: test_objects_using_auth_user_model_uses_builtin_auth_user_per_default
main: |
from django.contrib.auth import authenticate, aauthenticate
reveal_type(authenticate()) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, None]"
from django.contrib.auth import authenticate, aauthenticate, get_user_model
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.base_user.AbstractBaseUser, 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]"
custom_settings: |
INSTALLED_APPS = ("django.contrib.contenttypes", "myapp")
INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "myapp")
files:
- path: myapp/__init__.py
- path: myapp/models.py
@@ -33,14 +48,15 @@
class MyUser(models.Model):
...
- case: test_authenticate_returns_builtin_auth_user_per_default
- case: test_objects_for_auth_user_model_returns_stub_types_when_contrib_auth_is_not_installed
main: |
from django.contrib.auth import authenticate, aauthenticate
reveal_type(authenticate()) # N: Revealed type is "Union[django.contrib.auth.models.User, None]"
from django.contrib.auth import authenticate, aauthenticate, get_user_model
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.models.User, 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]"
custom_settings: |
INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "myapp")
INSTALLED_APPS = ("django.contrib.contenttypes", "myapp")
files:
- path: myapp/__init__.py
- path: myapp/models.py
6 changes: 0 additions & 6 deletions tests/typecheck/test_request.yml
Original file line number Diff line number Diff line change
@@ -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,15 +27,13 @@
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()
reveal_type(request.user) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, django.contrib.auth.models.AnonymousUser]"
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