diff --git a/django-stubs/contrib/auth/base_user.pyi b/django-stubs/contrib/auth/base_user.pyi index c673938..1cbcac1 100644 --- a/django-stubs/contrib/auth/base_user.pyi +++ b/django-stubs/contrib/auth/base_user.pyi @@ -1,9 +1,15 @@ +import sys from typing import Any, Optional, Tuple, List, overload, TypeVar from django.db.models.base import Model from django.db import models +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + _T = TypeVar("_T", bound=Model) class BaseUserManager(models.Manager[_T]): @@ -20,9 +26,9 @@ class AbstractBaseUser(models.Model): def get_username(self) -> str: ... def natural_key(self) -> Tuple[str]: ... @property - def is_anonymous(self) -> bool: ... + def is_anonymous(self) -> Literal[False]: ... @property - def is_authenticated(self) -> bool: ... + def is_authenticated(self) -> Literal[True]: ... def set_password(self, raw_password: Optional[str]) -> None: ... def check_password(self, raw_password: str) -> bool: ... def set_unusable_password(self) -> None: ... diff --git a/django-stubs/contrib/auth/models.pyi b/django-stubs/contrib/auth/models.pyi index 2068c8a..921841e 100644 --- a/django-stubs/contrib/auth/models.pyi +++ b/django-stubs/contrib/auth/models.pyi @@ -1,3 +1,4 @@ +import sys from typing import Any, Collection, Optional, Set, Tuple, Type, TypeVar, Union from django.contrib.auth.backends import ModelBackend @@ -9,6 +10,11 @@ from django.db.models.manager import EmptyManager from django.db import models +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + _AnyUser = Union[Model, "AnonymousUser"] def update_last_login(sender: Type[AbstractBaseUser], user: AbstractBaseUser, **kwargs: Any) -> None: ... @@ -105,7 +111,7 @@ class AnonymousUser: def has_perms(self, perm_list: Collection[str], obj: Optional[_AnyUser] = ...) -> bool: ... def has_module_perms(self, module: str) -> bool: ... @property - def is_anonymous(self) -> bool: ... + def is_anonymous(self) -> Literal[True]: ... @property - def is_authenticated(self) -> bool: ... + def is_authenticated(self) -> Literal[False]: ... def get_username(self) -> str: ... diff --git a/django-stubs/core/handlers/wsgi.pyi b/django-stubs/core/handlers/wsgi.pyi index c0c0a69..4ea28d9 100644 --- a/django-stubs/core/handlers/wsgi.pyi +++ b/django-stubs/core/handlers/wsgi.pyi @@ -1,12 +1,10 @@ from io import BytesIO from typing import Any, Callable, Dict, Optional, Union -from django.contrib.auth.models import AbstractUser from django.contrib.sessions.backends.base import SessionBase -from django.http.response import HttpResponse - from django.core.handlers import base from django.http import HttpRequest +from django.http.response import HttpResponse _Stream = Union[BytesIO, str] _WSGIEnviron = Dict[str, Any] @@ -22,7 +20,6 @@ class LimitedStream: class WSGIRequest(HttpRequest): environ: _WSGIEnviron = ... - user: AbstractUser session: SessionBase encoding: Any = ... def __init__(self, environ: _WSGIEnviron) -> None: ... diff --git a/django-stubs/http/request.pyi b/django-stubs/http/request.pyi index c2c00bf..821e690 100644 --- a/django-stubs/http/request.pyi +++ b/django-stubs/http/request.pyi @@ -17,6 +17,7 @@ from typing import ( ) from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.models import AnonymousUser from django.contrib.sessions.backends.base import SessionBase from django.contrib.sites.models import Site from django.utils.datastructures import CaseInsensitiveMapping, ImmutableList, MultiValueDict @@ -51,7 +52,7 @@ class HttpRequest(BytesIO): resolver_match: ResolverMatch = ... content_type: Optional[str] = ... content_params: Optional[Dict[str, str]] = ... - user: AbstractBaseUser + user: Union[AbstractBaseUser, AnonymousUser] site: Site session: SessionBase encoding: Optional[str] = ... diff --git a/flake8-pyi.ini b/flake8-pyi.ini index 568f91c..ba413f2 100644 --- a/flake8-pyi.ini +++ b/flake8-pyi.ini @@ -10,3 +10,5 @@ select = F401, Y max_line_length = 120 per-file-ignores = *__init__.pyi: F401 + base_user.pyi: Y003 + models.pyi: Y003 diff --git a/mypy_django_plugin/transformers/request.py b/mypy_django_plugin/transformers/request.py index be584ab..45b212d 100644 --- a/mypy_django_plugin/transformers/request.py +++ b/mypy_django_plugin/transformers/request.py @@ -1,6 +1,7 @@ from mypy.plugin import AttributeContext from mypy.types import Instance from mypy.types import Type as MypyType +from mypy.types import UnionType from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.lib import helpers @@ -8,9 +9,18 @@ from mypy_django_plugin.lib import helpers def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, 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_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), model_cls) - if model_info is None: + 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 Instance(model_info, []) + # Imported here because django isn't properly loaded yet when module is loaded + from django.contrib.auth.models import AnonymousUser + + anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser) + if anonymous_user_info is None: + # This shouldn't be able to happen, as we managed to import the model above... + return Instance(user_info, []) + + return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])]) diff --git a/test-data/typecheck/models/test_contrib_models.yml b/test-data/typecheck/models/test_contrib_models.yml index 6e36ebf..946d2de 100644 --- a/test-data/typecheck/models/test_contrib_models.yml +++ b/test-data/typecheck/models/test_contrib_models.yml @@ -10,6 +10,12 @@ reveal_type(User().is_active) # N: Revealed type is 'builtins.bool*' reveal_type(User().date_joined) # N: Revealed type is 'datetime.datetime*' reveal_type(User().last_login) # N: Revealed type is 'Union[datetime.datetime, None]' + reveal_type(User().is_authenticated) # N: Revealed type is 'Literal[True]' + reveal_type(User().is_anonymous) # N: Revealed type is 'Literal[False]' + + from django.contrib.auth.models import AnonymousUser + reveal_type(AnonymousUser().is_authenticated) # N: Revealed type is 'Literal[False]' + reveal_type(AnonymousUser().is_anonymous) # N: Revealed type is 'Literal[True]' from django.contrib.auth.models import Permission reveal_type(Permission().name) # N: Revealed type is 'builtins.str*' diff --git a/test-data/typecheck/test_request.yml b/test-data/typecheck/test_request.yml index 1c0850a..4a72402 100644 --- a/test-data/typecheck/test_request.yml +++ b/test-data/typecheck/test_request.yml @@ -2,11 +2,11 @@ disable_cache: true main: | from django.http.request import HttpRequest - reveal_type(HttpRequest().user) # N: Revealed type is 'myapp.models.MyUser' + reveal_type(HttpRequest().user) # N: Revealed type is 'Union[myapp.models.MyUser, django.contrib.auth.models.AnonymousUser]' # check that other fields work ok reveal_type(HttpRequest().method) # N: Revealed type is 'Union[builtins.str, None]' custom_settings: | - INSTALLED_APPS = ('django.contrib.contenttypes', 'myapp') + INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth', 'myapp') AUTH_USER_MODEL='myapp.MyUser' files: - path: myapp/__init__.py @@ -14,4 +14,16 @@ content: | from django.db import models class MyUser(models.Model): - pass \ No newline at end of file + pass +- case: request_object_user_can_be_descriminated + disable_cache: true + main: | + from django.http.request import HttpRequest + request = HttpRequest() + reveal_type(request.user) # N: Revealed type is 'Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]' + if not request.user.is_anonymous: + reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User' + if request.user.is_authenticated: + reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User' + custom_settings: | + INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')