Skip to content

Commit

Permalink
update default AUTH_USER_MODEL to be AbstractUser and reuse a…
Browse files Browse the repository at this point in the history
…liases

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.
  • Loading branch information
terencehonles committed Sep 27, 2024
1 parent e14b20c commit 513df00
Show file tree
Hide file tree
Showing 13 changed files with 84 additions and 91 deletions.
23 changes: 10 additions & 13 deletions django-stubs/contrib/auth/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import Any

from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.base_user import _UserModel
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import _AnyUser, _User
from django.db.models.options import Options
from django.http.request import HttpRequest
from django.test.client import Client
Expand All @@ -18,17 +17,15 @@ REDIRECT_FIELD_NAME: str

def load_backend(path: str) -> BaseBackend: ...
def get_backends() -> list[BaseBackend]: ...
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: _UserModel | None, backend: type[BaseBackend] | str | None = ...
) -> None: ...
def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> _User | None: ...
async def aauthenticate(request: HttpRequest | None = ..., **credentials: Any) -> _User | None: ...
def login(request: HttpRequest, user: _User | None, backend: type[BaseBackend] | str | None = ...) -> None: ...
async def alogin(request: HttpRequest, user: _User | None, backend: type[BaseBackend] | str | None = ...) -> None: ...
def logout(request: HttpRequest) -> None: ...
async def alogout(request: HttpRequest) -> None: ...
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_user_model() -> type[_User]: ...
def get_user(request: HttpRequest | Client) -> _AnyUser: ...
async def aget_user(request: HttpRequest | Client) -> _AnyUser: ...
def get_permission_codename(action: str, opts: Options) -> str: ...
def update_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...
async def aupdate_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...
def update_session_auth_hash(request: HttpRequest, user: _User) -> None: ...
async def aupdate_session_auth_hash(request: HttpRequest, user: _User) -> None: ...
20 changes: 8 additions & 12 deletions django-stubs/contrib/auth/backends.pyi
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
from typing import Any, TypeVar
from typing import Any

from django.contrib.auth.base_user import AbstractBaseUser, _UserModel
from django.contrib.auth.models import AnonymousUser, Permission
from django.contrib.auth.models import Permission, _AnyUser, _User
from django.db.models import QuerySet
from django.db.models.base import Model
from django.http.request import HttpRequest
from typing_extensions import TypeAlias

UserModel: TypeAlias = type[_UserModel]
_AnyUser: TypeAlias = _UserModel | AnonymousUser
UserModel: TypeAlias = type[_User]

class BaseBackend:
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ...
def get_user(self, user_id: Any) -> _UserModel | None: ...
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _User | None: ...
def get_user(self, user_id: Any) -> _User | 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]: ...
Expand All @@ -21,7 +19,7 @@ class BaseBackend:
class ModelBackend(BaseBackend):
def authenticate(
self, request: HttpRequest | None, username: str | None = ..., password: str | None = ..., **kwargs: Any
) -> _UserModel | None: ...
) -> _User | 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(
Expand All @@ -30,15 +28,13 @@ class ModelBackend(BaseBackend):
is_active: bool = ...,
include_superusers: bool = ...,
obj: Model | None = ...,
) -> QuerySet[_UserModel]: ...
) -> QuerySet[_User]: ...

class AllowAllUsersModelBackend(ModelBackend): ...

_U = TypeVar("_U", bound=AbstractBaseUser)

class RemoteUserBackend(ModelBackend):
create_unknown_user: bool
def clean_username(self, username: str) -> str: ...
def configure_user(self, request: HttpRequest, user: _U, created: bool = ...) -> _U: ...
def configure_user(self, request: HttpRequest, user: _User, created: bool = ...) -> _User: ...

class AllowAllUsersRemoteUserBackend(RemoteUserBackend): ...
6 changes: 0 additions & 6 deletions django-stubs/contrib/auth/base_user.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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)

Expand Down Expand Up @@ -42,8 +41,3 @@ 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: 2 additions & 3 deletions django-stubs/contrib/auth/decorators.pyi
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from collections.abc import Callable, Iterable
from typing import TypeVar, overload

from django.contrib.auth.base_user import _UserModel
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import _AnyUser
from django.http.response import HttpResponseBase

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

def user_passes_test(
test_func: Callable[[_UserModel | AnonymousUser], bool],
test_func: Callable[[_AnyUser], bool],
login_url: str | None = ...,
redirect_field_name: str | None = ...,
) -> Callable[[_VIEW], _VIEW]: ...
Expand Down
40 changes: 20 additions & 20 deletions django-stubs/contrib/auth/forms.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from collections.abc import Iterable
from logging import Logger
from typing import Any, TypeVar
from typing import Any, Generic

from django import forms
from django.contrib.auth.base_user import AbstractBaseUser, _UserModel
from django.contrib.auth.models import _User, _UserType
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.db import models
Expand All @@ -15,8 +15,7 @@ from typing_extensions import TypeAlias

logger: Logger

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

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

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

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

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

Expand All @@ -51,11 +51,11 @@ class AuthenticationForm(forms.Form):
password: forms.Field
error_messages: _ErrorMessagesDict
request: HttpRequest | None
user_cache: _UserModel | None
user_cache: _User | 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) -> _UserModel: ...
def confirm_login_allowed(self, user: _User) -> None: ...
def get_user(self) -> _User: ...
def get_invalid_login_error(self) -> ValidationError: ...
def clean(self) -> dict[str, Any]: ...

Expand All @@ -70,7 +70,7 @@ class PasswordResetForm(forms.Form):
to_email: str,
html_email_template_name: str | None = ...,
) -> None: ...
def get_users(self, email: str) -> Iterable[_UserModel]: ...
def get_users(self, email: str) -> Iterable[_User]: ...
def save(
self,
domain_override: str | None = ...,
Expand All @@ -84,28 +84,28 @@ class PasswordResetForm(forms.Form):
extra_email_context: dict[str, str] | None = ...,
) -> None: ...

class SetPasswordForm(forms.Form):
class SetPasswordForm(Generic[_UserType], forms.Form):
error_messages: _ErrorMessagesDict
new_password1: forms.Field
new_password2: forms.Field
user: AbstractBaseUser
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
user: _UserType
def __init__(self, user: _UserType, *args: Any, **kwargs: Any) -> None: ...
def clean_new_password2(self) -> str: ...
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
def save(self, commit: bool = ...) -> _UserType: ...

class PasswordChangeForm(SetPasswordForm):
error_messages: _ErrorMessagesDict
old_password: forms.Field
def clean_old_password(self) -> str: ...

class AdminPasswordChangeForm(forms.Form):
class AdminPasswordChangeForm(Generic[_UserType], forms.Form):
error_messages: _ErrorMessagesDict
required_css_class: str
password1: forms.Field
password2: forms.Field
user: AbstractBaseUser
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
user: _UserType
def __init__(self, user: _UserType, *args: Any, **kwargs: Any) -> None: ...
def clean_password2(self) -> str: ...
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
def save(self, commit: bool = ...) -> _UserType: ...
@property
def changed_data(self) -> list[str]: ...
7 changes: 3 additions & 4 deletions django-stubs/contrib/auth/middleware.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from django.contrib.auth.base_user import _UserModel
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import _AnyUser
from django.http.request import HttpRequest
from django.utils.deprecation import MiddlewareMixin

def get_user(request: HttpRequest) -> AnonymousUser | _UserModel: ...
async def auser(request: HttpRequest) -> AnonymousUser | _UserModel: ...
def get_user(request: HttpRequest) -> _AnyUser: ...
async def auser(request: HttpRequest) -> _AnyUser: ...

class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest) -> None: ...
Expand Down
25 changes: 17 additions & 8 deletions django-stubs/contrib/auth/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@ from django.db.models.manager import EmptyManager
from django.utils.functional import _StrOrPromise
from typing_extensions import Self, TypeAlias

_AnyUser: TypeAlias = Model | AnonymousUser
_AnyUser: TypeAlias = _User | AnonymousUser

def update_last_login(sender: type[AbstractBaseUser], user: AbstractBaseUser, **kwargs: Any) -> None: ...
# 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`
_User: TypeAlias = AbstractUser

# These are only needed for generic classes in order to bind to a specific implementation
_AnyUserType = TypeVar("_AnyUserType", bound=_AnyUser) # noqa: PYI018

# do not use the alias `_User` so the bound remains at `AbstractUser`
_UserType = TypeVar("_UserType", bound=AbstractUser)

def update_last_login(sender: type[_User], user: _User, **kwargs: Any) -> None: ...

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

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

class UserManager(BaseUserManager[_T]):
class UserManager(BaseUserManager[_UserType]):
def create_user(
self, username: str, email: str | None = ..., password: str | None = ..., **extra_fields: Any
) -> _T: ...
) -> _UserType: ...
def create_superuser(
self, username: str, email: str | None = ..., password: str | None = ..., **extra_fields: Any
) -> _T: ...
) -> _UserType: ...
def with_perm(
self,
perm: str | Permission,
is_active: bool = ...,
include_superusers: bool = ...,
backend: str | None = ...,
obj: Model | None = ...,
) -> QuerySet[_T]: ...
) -> QuerySet[_UserType]: ...

class PermissionsMixin(models.Model):
is_superuser = models.BooleanField()
Expand Down
16 changes: 8 additions & 8 deletions django-stubs/contrib/auth/password_validation.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ from collections.abc import Mapping, Sequence
from pathlib import Path, PosixPath
from typing import Any, Protocol, type_check_only

from django.contrib.auth.base_user import _UserModel
from django.contrib.auth.models import _User

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

def get_default_password_validators() -> list[PasswordValidator]: ...
def get_password_validators(validator_config: Sequence[Mapping[str, Any]]) -> list[PasswordValidator]: ...
def validate_password(
password: str, user: _UserModel | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
password: str, user: _User | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
) -> None: ...
def password_changed(
password: str, user: _UserModel | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
password: str, user: _User | None = ..., password_validators: Sequence[PasswordValidator] | None = ...
) -> None: ...
def password_validators_help_texts(password_validators: Sequence[PasswordValidator] | None = ...) -> list[str]: ...

Expand All @@ -24,24 +24,24 @@ password_validators_help_text_html: Any
class MinimumLengthValidator:
min_length: int
def __init__(self, min_length: int = ...) -> None: ...
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
def validate(self, password: str, user: _User | None = ...) -> None: ...
def get_help_text(self) -> str: ...

class UserAttributeSimilarityValidator:
DEFAULT_USER_ATTRIBUTES: Sequence[str]
user_attributes: Sequence[str]
max_similarity: float
def __init__(self, user_attributes: Sequence[str] = ..., max_similarity: float = ...) -> None: ...
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
def validate(self, password: str, user: _User | None = ...) -> None: ...
def get_help_text(self) -> str: ...

class CommonPasswordValidator:
DEFAULT_PASSWORD_LIST_PATH: Path
passwords: set[str]
def __init__(self, password_list_path: Path | PosixPath | str = ...) -> None: ...
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
def validate(self, password: str, user: _User | None = ...) -> None: ...
def get_help_text(self) -> str: ...

class NumericPasswordValidator:
def validate(self, password: str, user: _UserModel | None = ...) -> None: ...
def validate(self, password: str, user: _User | None = ...) -> None: ...
def get_help_text(self) -> str: ...
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 _UserModel
from django.contrib.auth.models import _User

class PasswordResetTokenGenerator:
key_salt: str
secret: str | bytes
secret_fallbacks: list[str | bytes]
algorithm: 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 make_token(self, user: _User) -> str: ...
def check_token(self, user: _User | None, token: str | None) -> bool: ...
def _make_token_with_timestamp(self, user: _User, timestamp: int, secret: str | bytes = ...) -> str: ...
def _make_hash_value(self, user: _User, timestamp: int) -> str: ...
def _num_seconds(self, dt: datetime | date) -> int: ...
def _now(self) -> datetime: ...

Expand Down
6 changes: 3 additions & 3 deletions django-stubs/contrib/auth/views.pyi
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Any

from django.contrib.auth.base_user import _UserModel
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import _User
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: TypeAlias = type[_UserModel]
UserModel: TypeAlias = type[_User]

class RedirectURLMixin:
next_page: str | None
Expand Down Expand Up @@ -66,7 +66,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
token_generator: Any
validlink: bool
user: Any
def get_user(self, uidb64: str) -> _UserModel | None: ...
def get_user(self, uidb64: str) -> _User | None: ...

class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
title: Any
Expand Down
Loading

0 comments on commit 513df00

Please sign in to comment.