Skip to content

Commit 06a4f41

Browse files
committed
update default AUTH_USER_MODEL to be AbstractUser and reuse aliases
This change updates the previous ``AUTH_USER_MODEL`` placeholder alias to default to ``AbstractUser`` instead of ``AbstractBaseUser`` which does not include the ``PermissionsMixin``, so that the type alias works better even without the mypy plugin. This change also adds a few user type vars and alises to cover the common use cases of ``User``, ``User | AnonymousUser``, and their ``TypeVar`` forms for using in generic contexts.
1 parent 2968730 commit 06a4f41

File tree

13 files changed

+84
-91
lines changed

13 files changed

+84
-91
lines changed
Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from typing import Any
22

33
from django.contrib.auth.backends import BaseBackend
4-
from django.contrib.auth.base_user import _UserModel
5-
from django.contrib.auth.models import AnonymousUser
4+
from django.contrib.auth.models import _AnyUser, _User
65
from django.db.models.options import Options
76
from django.http.request import HttpRequest
87
from django.test.client import Client
@@ -18,17 +17,15 @@ REDIRECT_FIELD_NAME: str
1817

1918
def load_backend(path: str) -> BaseBackend: ...
2019
def get_backends() -> list[BaseBackend]: ...
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: ...
24-
async def alogin(
25-
request: HttpRequest, user: _UserModel | None, backend: type[BaseBackend] | str | None = ...
26-
) -> None: ...
20+
def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> _User | None: ...
21+
async def aauthenticate(request: HttpRequest | None = ..., **credentials: Any) -> _User | None: ...
22+
def login(request: HttpRequest, user: _User | None, backend: type[BaseBackend] | str | None = ...) -> None: ...
23+
async def alogin(request: HttpRequest, user: _User | None, backend: type[BaseBackend] | str | None = ...) -> None: ...
2724
def logout(request: HttpRequest) -> None: ...
2825
async def alogout(request: HttpRequest) -> None: ...
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: ...
26+
def get_user_model() -> type[_User]: ...
27+
def get_user(request: HttpRequest | Client) -> _AnyUser: ...
28+
async def aget_user(request: HttpRequest | Client) -> _AnyUser: ...
3229
def get_permission_codename(action: str, opts: Options) -> str: ...
33-
def update_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...
34-
async def aupdate_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...
30+
def update_session_auth_hash(request: HttpRequest, user: _User) -> None: ...
31+
async def aupdate_session_auth_hash(request: HttpRequest, user: _User) -> None: ...
Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
from typing import Any, TypeVar
1+
from typing import Any
22

3-
from django.contrib.auth.base_user import AbstractBaseUser, _UserModel
4-
from django.contrib.auth.models import AnonymousUser, Permission
3+
from django.contrib.auth.models import Permission, _AnyUser, _User
54
from django.db.models import QuerySet
65
from django.db.models.base import Model
76
from django.http.request import HttpRequest
87
from typing_extensions import TypeAlias
98

10-
UserModel: TypeAlias = type[_UserModel]
11-
_AnyUser: TypeAlias = _UserModel | AnonymousUser
9+
UserModel: TypeAlias = type[_User]
1210

1311
class BaseBackend:
14-
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ...
15-
def get_user(self, user_id: Any) -> _UserModel | None: ...
12+
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _User | None: ...
13+
def get_user(self, user_id: Any) -> _User | None: ...
1614
def get_user_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
1715
def get_group_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
1816
def get_all_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
@@ -21,7 +19,7 @@ class BaseBackend:
2119
class ModelBackend(BaseBackend):
2220
def authenticate(
2321
self, request: HttpRequest | None, username: str | None = ..., password: str | None = ..., **kwargs: Any
24-
) -> _UserModel | None: ...
22+
) -> _User | None: ...
2523
def has_module_perms(self, user_obj: _AnyUser, app_label: str) -> bool: ...
2624
def user_can_authenticate(self, user: _AnyUser | None) -> bool: ...
2725
def with_perm(
@@ -30,15 +28,13 @@ class ModelBackend(BaseBackend):
3028
is_active: bool = ...,
3129
include_superusers: bool = ...,
3230
obj: Model | None = ...,
33-
) -> QuerySet[_UserModel]: ...
31+
) -> QuerySet[_User]: ...
3432

3533
class AllowAllUsersModelBackend(ModelBackend): ...
3634

37-
_U = TypeVar("_U", bound=AbstractBaseUser)
38-
3935
class RemoteUserBackend(ModelBackend):
4036
create_unknown_user: bool
4137
def clean_username(self, username: str) -> str: ...
42-
def configure_user(self, request: HttpRequest, user: _U, created: bool = ...) -> _U: ...
38+
def configure_user(self, request: HttpRequest, user: _User, created: bool = ...) -> _User: ...
4339

4440
class AllowAllUsersRemoteUserBackend(RemoteUserBackend): ...

django-stubs/contrib/auth/base_user.pyi

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ 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
87

98
_T = TypeVar("_T", bound=Model)
109

@@ -42,8 +41,3 @@ class AbstractBaseUser(models.Model):
4241
@classmethod
4342
@overload
4443
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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from collections.abc import Callable, Iterable
22
from typing import TypeVar, overload
33

4-
from django.contrib.auth.base_user import _UserModel
5-
from django.contrib.auth.models import AnonymousUser
4+
from django.contrib.auth.models import _AnyUser
65
from django.http.response import HttpResponseBase
76

87
_VIEW = TypeVar("_VIEW", bound=Callable[..., HttpResponseBase])
98

109
def user_passes_test(
11-
test_func: Callable[[_UserModel | AnonymousUser], bool],
10+
test_func: Callable[[_AnyUser], bool],
1211
login_url: str | None = ...,
1312
redirect_field_name: str | None = ...,
1413
) -> Callable[[_VIEW], _VIEW]: ...

django-stubs/contrib/auth/forms.pyi

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from collections.abc import Iterable
22
from logging import Logger
3-
from typing import Any, TypeVar
3+
from typing import Any, Generic
44

55
from django import forms
6-
from django.contrib.auth.base_user import AbstractBaseUser, _UserModel
6+
from django.contrib.auth.models import _User, _UserType
77
from django.contrib.auth.tokens import PasswordResetTokenGenerator
88
from django.core.exceptions import ValidationError
99
from django.db import models
@@ -15,8 +15,7 @@ from typing_extensions import TypeAlias
1515

1616
logger: Logger
1717

18-
UserModel: TypeAlias = type[_UserModel]
19-
_User = TypeVar("_User", bound=AbstractBaseUser)
18+
UserModel: TypeAlias = type[_User]
2019

2120
class ReadOnlyPasswordHashWidget(forms.Widget):
2221
template_name: str
@@ -31,18 +30,19 @@ class UsernameField(forms.CharField):
3130
def to_python(self, value: Any | None) -> Any | None: ...
3231
def widget_attrs(self, widget: Widget) -> dict[str, Any]: ...
3332

34-
class BaseUserCreationForm(forms.ModelForm[_User]):
33+
# mypy is incorrectly interpreting this as not generic, so explicitly say it is
34+
class BaseUserCreationForm(Generic[_UserType], forms.ModelForm[_UserType]):
3535
error_messages: _ErrorMessagesDict
3636
password1: forms.Field
3737
password2: forms.Field
3838
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
3939
def clean_password2(self) -> str: ...
40-
def save(self, commit: bool = ...) -> _User: ...
40+
def save(self, commit: bool = ...) -> _UserType: ...
4141

42-
class UserCreationForm(BaseUserCreationForm[_User]):
42+
class UserCreationForm(BaseUserCreationForm[_UserType]):
4343
def clean_username(self) -> str: ...
4444

45-
class UserChangeForm(forms.ModelForm[_User]):
45+
class UserChangeForm(forms.ModelForm[_UserType]):
4646
password: forms.Field
4747
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
4848

@@ -51,11 +51,11 @@ class AuthenticationForm(forms.Form):
5151
password: forms.Field
5252
error_messages: _ErrorMessagesDict
5353
request: HttpRequest | None
54-
user_cache: _UserModel | None
54+
user_cache: _User | None
5555
username_field: models.Field
5656
def __init__(self, request: HttpRequest | None = ..., *args: Any, **kwargs: Any) -> None: ...
57-
def confirm_login_allowed(self, user: AbstractBaseUser) -> None: ...
58-
def get_user(self) -> _UserModel: ...
57+
def confirm_login_allowed(self, user: _User) -> None: ...
58+
def get_user(self) -> _User: ...
5959
def get_invalid_login_error(self) -> ValidationError: ...
6060
def clean(self) -> dict[str, Any]: ...
6161

@@ -70,7 +70,7 @@ class PasswordResetForm(forms.Form):
7070
to_email: str,
7171
html_email_template_name: str | None = ...,
7272
) -> None: ...
73-
def get_users(self, email: str) -> Iterable[_UserModel]: ...
73+
def get_users(self, email: str) -> Iterable[_User]: ...
7474
def save(
7575
self,
7676
domain_override: str | None = ...,
@@ -84,28 +84,28 @@ class PasswordResetForm(forms.Form):
8484
extra_email_context: dict[str, str] | None = ...,
8585
) -> None: ...
8686

87-
class SetPasswordForm(forms.Form):
87+
class SetPasswordForm(Generic[_UserType], forms.Form):
8888
error_messages: _ErrorMessagesDict
8989
new_password1: forms.Field
9090
new_password2: forms.Field
91-
user: AbstractBaseUser
92-
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
91+
user: _UserType
92+
def __init__(self, user: _UserType, *args: Any, **kwargs: Any) -> None: ...
9393
def clean_new_password2(self) -> str: ...
94-
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
94+
def save(self, commit: bool = ...) -> _UserType: ...
9595

9696
class PasswordChangeForm(SetPasswordForm):
9797
error_messages: _ErrorMessagesDict
9898
old_password: forms.Field
9999
def clean_old_password(self) -> str: ...
100100

101-
class AdminPasswordChangeForm(forms.Form):
101+
class AdminPasswordChangeForm(Generic[_UserType], forms.Form):
102102
error_messages: _ErrorMessagesDict
103103
required_css_class: str
104104
password1: forms.Field
105105
password2: forms.Field
106-
user: AbstractBaseUser
107-
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
106+
user: _UserType
107+
def __init__(self, user: _UserType, *args: Any, **kwargs: Any) -> None: ...
108108
def clean_password2(self) -> str: ...
109-
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
109+
def save(self, commit: bool = ...) -> _UserType: ...
110110
@property
111111
def changed_data(self) -> list[str]: ...

django-stubs/contrib/auth/middleware.pyi

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from django.contrib.auth.base_user import _UserModel
2-
from django.contrib.auth.models import AnonymousUser
1+
from django.contrib.auth.models import _AnyUser
32
from django.http.request import HttpRequest
43
from django.utils.deprecation import MiddlewareMixin
54

6-
def get_user(request: HttpRequest) -> AnonymousUser | _UserModel: ...
7-
async def auser(request: HttpRequest) -> AnonymousUser | _UserModel: ...
5+
def get_user(request: HttpRequest) -> _AnyUser: ...
6+
async def auser(request: HttpRequest) -> _AnyUser: ...
87

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

django-stubs/contrib/auth/models.pyi

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,20 @@ from django.db.models.manager import EmptyManager
1212
from django.utils.functional import _StrOrPromise
1313
from typing_extensions import Self, TypeAlias
1414

15-
_AnyUser: TypeAlias = Model | AnonymousUser
15+
# This is our "placeholder" type the mypy plugin refines to configured 'AUTH_USER_MODEL'
16+
# wherever it is used as a type. The most recognised example of this is (probably)
17+
# `HttpRequest.user`
18+
_User: TypeAlias = AbstractBaseUser
1619

17-
def update_last_login(sender: type[AbstractBaseUser], user: AbstractBaseUser, **kwargs: Any) -> None: ...
20+
_AnyUser: TypeAlias = _User | AnonymousUser
21+
22+
# These are only needed for generic classes in order to bind to a specific implementation
23+
_AnyUserType = TypeVar("_AnyUserType", bound=_AnyUser) # noqa: PYI018
24+
25+
# do not use the alias `_User` so the bound remains at `AbstractUser`
26+
_UserType = TypeVar("_UserType", bound=AbstractUser)
27+
28+
def update_last_login(sender: type[_User], user: _User, **kwargs: Any) -> None: ...
1829

1930
class PermissionManager(models.Manager[Permission]):
2031
def get_by_natural_key(self, codename: str, app_label: str, model: str) -> Permission: ...
@@ -38,23 +49,21 @@ class Group(models.Model):
3849
permissions = models.ManyToManyField(Permission)
3950
def natural_key(self) -> tuple[str]: ...
4051

41-
_T = TypeVar("_T", bound=Model)
42-
43-
class UserManager(BaseUserManager[_T]):
52+
class UserManager(BaseUserManager[_UserType]):
4453
def create_user(
4554
self, username: str, email: str | None = ..., password: str | None = ..., **extra_fields: Any
46-
) -> _T: ...
55+
) -> _UserType: ...
4756
def create_superuser(
4857
self, username: str, email: str | None = ..., password: str | None = ..., **extra_fields: Any
49-
) -> _T: ...
58+
) -> _UserType: ...
5059
def with_perm(
5160
self,
5261
perm: str | Permission,
5362
is_active: bool = ...,
5463
include_superusers: bool = ...,
5564
backend: str | None = ...,
5665
obj: Model | None = ...,
57-
) -> QuerySet[_T]: ...
66+
) -> QuerySet[_UserType]: ...
5867

5968
class PermissionsMixin(models.Model):
6069
is_superuser = models.BooleanField()

django-stubs/contrib/auth/password_validation.pyi

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@ 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.contrib.auth.base_user import _UserModel
5+
from django.contrib.auth.models import _User
66

77
@type_check_only
88
class PasswordValidator(Protocol):
9-
def validate(self, password: str, user: _UserModel | None = ..., /) -> None: ...
9+
def validate(self, password: str, user: _User | None = ..., /) -> None: ...
1010
def get_help_text(self) -> str: ...
1111

1212
def get_default_password_validators() -> list[PasswordValidator]: ...
1313
def get_password_validators(validator_config: Sequence[Mapping[str, Any]]) -> list[PasswordValidator]: ...
1414
def validate_password(
15-
password: str, user: _UserModel | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
15+
password: str, user: _User | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
1616
) -> None: ...
1717
def password_changed(
18-
password: str, user: _UserModel | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
18+
password: str, user: _User | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
1919
) -> None: ...
2020
def password_validators_help_texts(password_validators: Sequence[PasswordValidator] | None = ...) -> list[str]: ...
2121

@@ -24,24 +24,24 @@ password_validators_help_text_html: Any
2424
class MinimumLengthValidator:
2525
min_length: int
2626
def __init__(self, min_length: int = ...) -> None: ...
27-
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
27+
def validate(self, password: str, user: _User | None = ...) -> None: ...
2828
def get_help_text(self) -> str: ...
2929

3030
class UserAttributeSimilarityValidator:
3131
DEFAULT_USER_ATTRIBUTES: Sequence[str]
3232
user_attributes: Sequence[str]
3333
max_similarity: float
3434
def __init__(self, user_attributes: Sequence[str] = ..., max_similarity: float = ...) -> None: ...
35-
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
35+
def validate(self, password: str, user: _User | None = ...) -> None: ...
3636
def get_help_text(self) -> str: ...
3737

3838
class CommonPasswordValidator:
3939
DEFAULT_PASSWORD_LIST_PATH: Path
4040
passwords: set[str]
4141
def __init__(self, password_list_path: Path | PosixPath | str = ...) -> None: ...
42-
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
42+
def validate(self, password: str, user: _User | None = ...) -> None: ...
4343
def get_help_text(self) -> str: ...
4444

4545
class NumericPasswordValidator:
46-
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
46+
def validate(self, password: str, user: _User | None = ...) -> None: ...
4747
def get_help_text(self) -> str: ...

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 _UserModel
4+
from django.contrib.auth.models import _User
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: _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: ...
11+
def make_token(self, user: _User) -> str: ...
12+
def check_token(self, user: _User | None, token: str | None) -> bool: ...
13+
def _make_token_with_timestamp(self, user: _User, timestamp: int, secret: str | bytes = ...) -> str: ...
14+
def _make_hash_value(self, user: _User, 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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from typing import Any
22

3-
from django.contrib.auth.base_user import _UserModel
43
from django.contrib.auth.forms import AuthenticationForm
4+
from django.contrib.auth.models import _User
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
99
from typing_extensions import TypeAlias
1010

11-
UserModel: TypeAlias = type[_UserModel]
11+
UserModel: TypeAlias = type[_User]
1212

1313
class RedirectURLMixin:
1414
next_page: str | None
@@ -66,7 +66,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
6666
token_generator: Any
6767
validlink: bool
6868
user: Any
69-
def get_user(self, uidb64: str) -> _UserModel | None: ...
69+
def get_user(self, uidb64: str) -> _User | None: ...
7070

7171
class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
7272
title: Any

0 commit comments

Comments
 (0)