Skip to content

create and re-use TypeAliases and TypeVars for "user" and "any user" #2384

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

Merged
merged 2 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, _UserModel
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() -> _UserModel: ...
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: ...
26 changes: 11 additions & 15 deletions django-stubs/contrib/auth/backends.pyi
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
from typing import Any, TypeAlias, 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, _UserModel, _UserType
from django.db.models import QuerySet
from django.db.models.base import Model
from django.http.request import HttpRequest

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

class BaseBackend:
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ...
async def aauthenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ...
def get_user(self, user_id: Any) -> _UserModel | None: ...
async def aget_user(self, user_id: Any) -> _UserModel | None: ...
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _User | None: ...
async def aauthenticate(self, request: HttpRequest | None, **kwargs: Any) -> _User | None: ...
def get_user(self, user_id: Any) -> _User | None: ...
async def aget_user(self, user_id: Any) -> _User | None: ...
def get_user_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
async def aget_user_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
def get_group_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
Expand All @@ -26,10 +24,10 @@ class BaseBackend:
class ModelBackend(BaseBackend):
def authenticate(
self, request: HttpRequest | None, username: str | None = ..., password: str | None = ..., **kwargs: Any
) -> _UserModel | None: ...
) -> _User | None: ...
async def aauthenticate(
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: ...
async def ahas_module_perms(self, user_obj: _AnyUser, app_label: str) -> bool: ...
def user_can_authenticate(self, user: _AnyUser | None) -> bool: ...
Expand All @@ -39,15 +37,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: _UserType, created: bool = ...) -> _UserType: ...

class AllowAllUsersRemoteUserBackend(RemoteUserBackend): ...
7 changes: 1 addition & 6 deletions django-stubs/contrib/auth/base_user.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from typing import Any, ClassVar, Literal, TypeAlias, TypeVar, overload
from typing import Any, ClassVar, Literal, TypeVar, overload

from django.db import models
from django.db.models.base import Model
Expand Down Expand Up @@ -41,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 Awaitable, 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 | Awaitable[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
54 changes: 25 additions & 29 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, TypeAlias, 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, _UserModel, _UserType
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.db import models
Expand All @@ -14,9 +14,7 @@ from django.http.request import HttpRequest
from django.utils.functional import _StrOrPromise

logger: Logger

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

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

class SetPasswordMixin:
class SetPasswordMixin(Generic[_UserType]):
error_messages: _ErrorMessagesDict

@staticmethod
Expand All @@ -43,12 +41,12 @@ class SetPasswordMixin:
password1_field_name: str = ...,
password2_field_name: str = ...,
) -> None: ...
def validate_password_for_user(self, user: AbstractBaseUser, password_field_name: str = "password2") -> None: ...
def validate_password_for_user(self, user: _UserType, password_field_name: str = "password2") -> None: ...
def set_password_and_save(
self, user: AbstractBaseUser, password_field_name: str = "password1", commit: bool = True
) -> AbstractBaseUser: ...
self, user: _UserType, password_field_name: str = "password1", commit: bool = True
) -> _UserType: ...

class SetUnusablePasswordMixin:
class SetUnusablePasswordMixin(Generic[_UserType]):
usable_password_help_text: _StrOrPromise

@staticmethod
Expand All @@ -59,20 +57,20 @@ class SetUnusablePasswordMixin:
password2_field_name: str = ...,
usable_password_field_name: str = ...,
) -> None: ...
def validate_password_for_user(self, user: AbstractBaseUser, **kwargs: Any) -> None: ...
def validate_password_for_user(self, user: _UserType, **kwargs: Any) -> None: ...
def set_password_and_save(self, user: _User, commit: bool = True, **kwargs: Any) -> _User: ...

class BaseUserCreationForm(forms.ModelForm[_User]):
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 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 @@ -81,11 +79,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 @@ -100,7 +98,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 @@ -114,28 +112,26 @@ class PasswordResetForm(forms.Form):
extra_email_context: dict[str, str] | None = ...,
) -> None: ...

class SetPasswordForm(forms.Form):
error_messages: _ErrorMessagesDict
class SetPasswordForm(Generic[_UserType], SetPasswordMixin, forms.Form):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not generic at runtime, which prevents this from being subclassed.

class MySetPasswordForm(SetPasswordForm):
    pass

mypy complains: error: Missing type parameters for generic type "SetPasswordForm" [type-arg]

class MySetPasswordForm(SetPasswordForm[User]):
    pass

Fails at runtime: TypeError: 'DeclarativeFieldsMetaclass' object is not subscriptable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andersk can you please send a PR to ext/? So we can monkeypatch this object in runtime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add a small test to ensure that ? Something parsing pyi file for ``generic` calls and ensuring they match the one declared in the ext ? I can try something like that

Copy link
Member

@sobolevn sobolevn May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, this would be a great addition. I have no idea on how to actually do that :)
Maybe a stubtest extra check? 🤔

I can open an issue in mypy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if stubtest can catch that indeed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new_password1: forms.Field
new_password2: forms.Field
user: AbstractBaseUser
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
user: _UserType
def __init__(self, user: _UserType, *args: Any, **kwargs: Any) -> None: ...
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
usable_password_help_text: str
password1: forms.Field
password2: forms.Field
user: AbstractBaseUser
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
user: _UserType
def __init__(self, user: _UserType, *args: Any, **kwargs: Any) -> None: ...
def save(self, commit: bool = ...) -> _UserType: ...
@property
def changed_data(self) -> list[str]: ...

Expand Down
7 changes: 3 additions & 4 deletions django-stubs/contrib/auth/middleware.pyi
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from collections.abc import Callable
from typing import Any, ClassVar

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 import HttpRequest, HttpResponseBase, HttpResponseRedirect
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
27 changes: 19 additions & 8 deletions django-stubs/contrib/auth/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,22 @@ from django.db.models.manager import EmptyManager
from django.utils.functional import _StrOrPromise
from typing_extensions import Self

_AnyUser: TypeAlias = Model | AnonymousUser
# 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 = AbstractBaseUser

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

_AnyUser: TypeAlias = _User | AnonymousUser

# 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)
Comment on lines +27 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this bound seems incorrect -- or at least it breaks the usage in sentry

the docs for django seem to indicate AbstractBaseUser as the bound here ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this probably is more correct. thanks for noticing!

btw, can we test compat with sentry somehow? like running mypy-primer but for django-related projects only? 🤔

it should be possible if projects that you are talking about are opensource.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep yep -- the repo in question is fully open source https://github.com/getsentry/sentry

there's a few problems that might be showstoppers though:

so I guess as long as it doesn't have to typecheck completely cleanly it should be fine?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created #2645 for the fix


def update_last_login(sender: _UserModel, 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 +51,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: ...
Loading
Loading