Skip to content

Commit 81ecdcf

Browse files
authored
Resolve configured AUTH_USER_MODEL with a get_type_analyze_hook (#2335)
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 type analyze 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
1 parent 3e6f1a6 commit 81ecdcf

File tree

23 files changed

+220
-126
lines changed

23 files changed

+220
-126
lines changed
Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any
22

33
from django.contrib.auth.backends import BaseBackend
4-
from django.contrib.auth.base_user import AbstractBaseUser
4+
from django.contrib.auth.base_user import _UserModel
55
from django.contrib.auth.models import AnonymousUser
66
from django.db.models.options import Options
77
from django.http.request import HttpRequest
@@ -18,19 +18,17 @@ REDIRECT_FIELD_NAME: str
1818

1919
def load_backend(path: str) -> BaseBackend: ...
2020
def get_backends() -> list[BaseBackend]: ...
21-
def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> AbstractBaseUser | None: ...
22-
async def aauthenticate(request: HttpRequest | None = ..., **credentials: Any) -> AbstractBaseUser | None: ...
23-
def login(
24-
request: HttpRequest, user: AbstractBaseUser | None, backend: type[BaseBackend] | str | None = ...
25-
) -> None: ...
21+
def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> _UserModel | None: ...
22+
async def aauthenticate(request: HttpRequest | None = ..., **credentials: Any) -> _UserModel | None: ...
23+
def login(request: HttpRequest, user: _UserModel | None, backend: type[BaseBackend] | str | None = ...) -> None: ...
2624
async def alogin(
27-
request: HttpRequest, user: AbstractBaseUser | None, backend: type[BaseBackend] | str | None = ...
25+
request: HttpRequest, user: _UserModel | None, backend: type[BaseBackend] | str | None = ...
2826
) -> None: ...
2927
def logout(request: HttpRequest) -> None: ...
3028
async def alogout(request: HttpRequest) -> None: ...
31-
def get_user_model() -> type[AbstractBaseUser]: ...
32-
def get_user(request: HttpRequest | Client) -> AbstractBaseUser | AnonymousUser: ...
33-
async def aget_user(request: HttpRequest | Client) -> AbstractBaseUser | AnonymousUser: ...
29+
def get_user_model() -> type[_UserModel]: ...
30+
def get_user(request: HttpRequest | Client) -> _UserModel | AnonymousUser: ...
31+
async def aget_user(request: HttpRequest | Client) -> _UserModel | AnonymousUser: ...
3432
def get_permission_codename(action: str, opts: Options) -> str: ...
35-
def update_session_auth_hash(request: HttpRequest, user: AbstractBaseUser) -> None: ...
36-
async def aupdate_session_auth_hash(request: HttpRequest, user: AbstractBaseUser) -> None: ...
33+
def update_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...
34+
async def aupdate_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...

django-stubs/contrib/auth/backends.pyi

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
from typing import Any, TypeVar
22

3-
from django.contrib.auth.base_user import AbstractBaseUser
3+
from django.contrib.auth.base_user import AbstractBaseUser, _UserModel
44
from django.contrib.auth.models import AnonymousUser, Permission
55
from django.db.models import QuerySet
66
from django.db.models.base import Model
77
from django.http.request import HttpRequest
88
from typing_extensions import TypeAlias
99

10-
_AnyUser: TypeAlias = AbstractBaseUser | AnonymousUser
11-
12-
UserModel: Any
10+
UserModel: TypeAlias = type[_UserModel]
11+
_AnyUser: TypeAlias = _UserModel | AnonymousUser
1312

1413
class BaseBackend:
15-
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> AbstractBaseUser | None: ...
16-
def get_user(self, user_id: Any) -> AbstractBaseUser | None: ...
14+
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ...
15+
def get_user(self, user_id: Any) -> _UserModel | None: ...
1716
def get_user_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
1817
def get_group_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
1918
def get_all_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
@@ -22,7 +21,7 @@ class BaseBackend:
2221
class ModelBackend(BaseBackend):
2322
def authenticate(
2423
self, request: HttpRequest | None, username: str | None = ..., password: str | None = ..., **kwargs: Any
25-
) -> AbstractBaseUser | None: ...
24+
) -> _UserModel | None: ...
2625
def has_module_perms(self, user_obj: _AnyUser, app_label: str) -> bool: ...
2726
def user_can_authenticate(self, user: _AnyUser | None) -> bool: ...
2827
def with_perm(
@@ -31,7 +30,7 @@ class ModelBackend(BaseBackend):
3130
is_active: bool = ...,
3231
include_superusers: bool = ...,
3332
obj: Model | None = ...,
34-
) -> QuerySet[AbstractBaseUser]: ...
33+
) -> QuerySet[_UserModel]: ...
3534

3635
class AllowAllUsersModelBackend(ModelBackend): ...
3736

django-stubs/contrib/auth/base_user.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ from django.db import models
44
from django.db.models.base import Model
55
from django.db.models.expressions import Combinable
66
from django.db.models.fields import BooleanField
7+
from typing_extensions import TypeAlias
78

89
_T = TypeVar("_T", bound=Model)
910

@@ -41,3 +42,8 @@ class AbstractBaseUser(models.Model):
4142
@classmethod
4243
@overload
4344
def normalize_username(cls, username: Any) -> Any: ...
45+
46+
# This is our "placeholder" type the mypy plugin refines to configured 'AUTH_USER_MODEL'
47+
# wherever it is used as a type. The most recognised example of this is (probably)
48+
# `HttpRequest.user`
49+
_UserModel: TypeAlias = AbstractBaseUser # noqa: PYI047

django-stubs/contrib/auth/decorators.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from collections.abc import Callable, Iterable
22
from typing import TypeVar, overload
33

4-
from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
4+
from django.contrib.auth.base_user import _UserModel
5+
from django.contrib.auth.models import AnonymousUser
56
from django.http.response import HttpResponseBase
67

78
_VIEW = TypeVar("_VIEW", bound=Callable[..., HttpResponseBase])
89

910
def user_passes_test(
10-
test_func: Callable[[AbstractBaseUser | AnonymousUser], bool],
11+
test_func: Callable[[_UserModel | AnonymousUser], bool],
1112
login_url: str | None = ...,
1213
redirect_field_name: str | None = ...,
1314
) -> Callable[[_VIEW], _VIEW]: ...

django-stubs/contrib/auth/forms.pyi

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ from collections.abc import Iterable
22
from typing import Any, TypeVar
33

44
from django import forms
5-
from django.contrib.auth.base_user import AbstractBaseUser
5+
from django.contrib.auth.base_user import AbstractBaseUser, _UserModel
66
from django.contrib.auth.tokens import PasswordResetTokenGenerator
77
from django.core.exceptions import ValidationError
88
from django.db import models
99
from django.db.models.fields import _ErrorMessagesDict
1010
from django.forms.fields import _ClassLevelWidgetT
1111
from django.forms.widgets import Widget
1212
from django.http.request import HttpRequest
13+
from typing_extensions import TypeAlias
1314

14-
UserModel: type[AbstractBaseUser]
15+
UserModel: TypeAlias = type[_UserModel]
1516
_User = TypeVar("_User", bound=AbstractBaseUser)
1617

1718
class ReadOnlyPasswordHashWidget(forms.Widget):
@@ -47,11 +48,11 @@ class AuthenticationForm(forms.Form):
4748
password: forms.Field
4849
error_messages: _ErrorMessagesDict
4950
request: HttpRequest | None
50-
user_cache: Any
51+
user_cache: _UserModel | None
5152
username_field: models.Field
5253
def __init__(self, request: HttpRequest | None = ..., *args: Any, **kwargs: Any) -> None: ...
5354
def confirm_login_allowed(self, user: AbstractBaseUser) -> None: ...
54-
def get_user(self) -> AbstractBaseUser: ...
55+
def get_user(self) -> _UserModel: ...
5556
def get_invalid_login_error(self) -> ValidationError: ...
5657
def clean(self) -> dict[str, Any]: ...
5758

@@ -66,7 +67,7 @@ class PasswordResetForm(forms.Form):
6667
to_email: str,
6768
html_email_template_name: str | None = ...,
6869
) -> None: ...
69-
def get_users(self, email: str) -> Iterable[AbstractBaseUser]: ...
70+
def get_users(self, email: str) -> Iterable[_UserModel]: ...
7071
def save(
7172
self,
7273
domain_override: str | None = ...,

django-stubs/contrib/auth/middleware.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from django.contrib.auth.base_user import AbstractBaseUser
1+
from django.contrib.auth.base_user import _UserModel
22
from django.contrib.auth.models import AnonymousUser
33
from django.http.request import HttpRequest
44
from django.utils.deprecation import MiddlewareMixin
55

6-
def get_user(request: HttpRequest) -> AnonymousUser | AbstractBaseUser: ...
7-
async def auser(request: HttpRequest) -> AnonymousUser | AbstractBaseUser: ...
6+
def get_user(request: HttpRequest) -> AnonymousUser | _UserModel: ...
7+
async def auser(request: HttpRequest) -> AnonymousUser | _UserModel: ...
88

99
class AuthenticationMiddleware(MiddlewareMixin):
1010
def process_request(self, request: HttpRequest) -> None: ...

django-stubs/contrib/auth/password_validation.pyi

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ from collections.abc import Mapping, Sequence
22
from pathlib import Path, PosixPath
33
from typing import Any, Protocol, type_check_only
44

5-
from django.db.models.base import Model
6-
from typing_extensions import TypeAlias
7-
8-
_UserModel: TypeAlias = Model
5+
from django.contrib.auth.base_user import _UserModel
96

107
@type_check_only
118
class PasswordValidator(Protocol):

django-stubs/contrib/auth/tokens.pyi

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from datetime import date, datetime
22
from typing import Any
33

4-
from django.contrib.auth.base_user import AbstractBaseUser
4+
from django.contrib.auth.base_user import _UserModel
55

66
class PasswordResetTokenGenerator:
77
key_salt: str
88
secret: str | bytes
99
secret_fallbacks: list[str | bytes]
1010
algorithm: str
11-
def make_token(self, user: AbstractBaseUser) -> str: ...
12-
def check_token(self, user: AbstractBaseUser | None, token: str | None) -> bool: ...
13-
def _make_token_with_timestamp(self, user: AbstractBaseUser, timestamp: int, secret: str | bytes = ...) -> str: ...
14-
def _make_hash_value(self, user: AbstractBaseUser, timestamp: int) -> str: ...
11+
def make_token(self, user: _UserModel) -> str: ...
12+
def check_token(self, user: _UserModel | None, token: str | None) -> bool: ...
13+
def _make_token_with_timestamp(self, user: _UserModel, timestamp: int, secret: str | bytes = ...) -> str: ...
14+
def _make_hash_value(self, user: _UserModel, timestamp: int) -> str: ...
1515
def _num_seconds(self, dt: datetime | date) -> int: ...
1616
def _now(self) -> datetime: ...
1717

django-stubs/contrib/auth/views.pyi

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from typing import Any
22

3-
from django.contrib.auth.base_user import AbstractBaseUser
3+
from django.contrib.auth.base_user import _UserModel
44
from django.contrib.auth.forms import AuthenticationForm
55
from django.http.request import HttpRequest
66
from django.http.response import HttpResponse, HttpResponseRedirect
77
from django.views.generic.base import TemplateView
88
from django.views.generic.edit import FormView
9+
from typing_extensions import TypeAlias
910

10-
UserModel: Any
11+
UserModel: TypeAlias = type[_UserModel]
1112

1213
class RedirectURLMixin:
1314
next_page: str | None
@@ -65,7 +66,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
6566
token_generator: Any
6667
validlink: bool
6768
user: Any
68-
def get_user(self, uidb64: str) -> AbstractBaseUser | None: ...
69+
def get_user(self, uidb64: str) -> _UserModel | None: ...
6970

7071
class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
7172
title: Any

django-stubs/http/request.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from io import BytesIO
44
from re import Pattern
55
from typing import Any, Awaitable, BinaryIO, Callable, Literal, NoReturn, TypeVar, overload, type_check_only
66

7-
from django.contrib.auth.base_user import AbstractBaseUser
7+
from django.contrib.auth.base_user import _UserModel
88
from django.contrib.auth.models import AnonymousUser
99
from django.contrib.sessions.backends.base import SessionBase
1010
from django.contrib.sites.models import Site
@@ -55,9 +55,9 @@ class HttpRequest(BytesIO):
5555
# django.contrib.admin views:
5656
current_app: str
5757
# django.contrib.auth.middleware.AuthenticationMiddleware:
58-
user: AbstractBaseUser | AnonymousUser
58+
user: _UserModel | AnonymousUser
5959
# django.contrib.auth.middleware.AuthenticationMiddleware:
60-
auser: Callable[[], Awaitable[AbstractBaseUser | AnonymousUser]]
60+
auser: Callable[[], Awaitable[_UserModel | AnonymousUser]]
6161
# django.middleware.locale.LocaleMiddleware:
6262
LANGUAGE_CODE: str
6363
# django.contrib.sites.middleware.CurrentSiteMiddleware

0 commit comments

Comments
 (0)