Skip to content

Commit 71751d3

Browse files
bullfestKacper Szmigiel
and
Kacper Szmigiel
authored
Issue 309 (#383)
* added tags for user models * type test for HttpRequest.user * test for User and AnonymousUser tags * httrequest test fix * checking python version fix for readibility * Rewrite version check for readability * Annotate is_authenticated/is_anonymous with Literal-type * Add auth in INSTALLED_APPS in test * Fix wrong type assertion in test * Fix misconception of how branch-testing works * Remove user from WSGIRequest * Change HttpRequest-transformer to set user-type to include AnonymousUser * Add check for anonymous_user_info=None to appease mypy * Isort transformers/request * Remove trailing whitespace * Remove unused import Co-authored-by: Kacper Szmigiel <[email protected]>
1 parent 25f92e8 commit 71751d3

File tree

8 files changed

+56
-16
lines changed

8 files changed

+56
-16
lines changed

django-stubs/contrib/auth/base_user.pyi

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import sys
12
from typing import Any, Optional, Tuple, List, overload, TypeVar
23

34
from django.db.models.base import Model
45

56
from django.db import models
67

8+
if sys.version_info < (3, 8):
9+
from typing_extensions import Literal
10+
else:
11+
from typing import Literal
12+
713
_T = TypeVar("_T", bound=Model)
814

915
class BaseUserManager(models.Manager[_T]):
@@ -20,9 +26,9 @@ class AbstractBaseUser(models.Model):
2026
def get_username(self) -> str: ...
2127
def natural_key(self) -> Tuple[str]: ...
2228
@property
23-
def is_anonymous(self) -> bool: ...
29+
def is_anonymous(self) -> Literal[False]: ...
2430
@property
25-
def is_authenticated(self) -> bool: ...
31+
def is_authenticated(self) -> Literal[True]: ...
2632
def set_password(self, raw_password: Optional[str]) -> None: ...
2733
def check_password(self, raw_password: str) -> bool: ...
2834
def set_unusable_password(self) -> None: ...

django-stubs/contrib/auth/models.pyi

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from typing import Any, Collection, Optional, Set, Tuple, Type, TypeVar, Union
23

34
from django.contrib.auth.backends import ModelBackend
@@ -9,6 +10,11 @@ from django.db.models.manager import EmptyManager
910

1011
from django.db import models
1112

13+
if sys.version_info < (3, 8):
14+
from typing_extensions import Literal
15+
else:
16+
from typing import Literal
17+
1218
_AnyUser = Union[Model, "AnonymousUser"]
1319

1420
def update_last_login(sender: Type[AbstractBaseUser], user: AbstractBaseUser, **kwargs: Any) -> None: ...
@@ -105,7 +111,7 @@ class AnonymousUser:
105111
def has_perms(self, perm_list: Collection[str], obj: Optional[_AnyUser] = ...) -> bool: ...
106112
def has_module_perms(self, module: str) -> bool: ...
107113
@property
108-
def is_anonymous(self) -> bool: ...
114+
def is_anonymous(self) -> Literal[True]: ...
109115
@property
110-
def is_authenticated(self) -> bool: ...
116+
def is_authenticated(self) -> Literal[False]: ...
111117
def get_username(self) -> str: ...

django-stubs/core/handlers/wsgi.pyi

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from io import BytesIO
22
from typing import Any, Callable, Dict, Optional, Union
33

4-
from django.contrib.auth.models import AbstractUser
54
from django.contrib.sessions.backends.base import SessionBase
6-
from django.http.response import HttpResponse
7-
85
from django.core.handlers import base
96
from django.http import HttpRequest
7+
from django.http.response import HttpResponse
108

119
_Stream = Union[BytesIO, str]
1210
_WSGIEnviron = Dict[str, Any]
@@ -22,7 +20,6 @@ class LimitedStream:
2220

2321
class WSGIRequest(HttpRequest):
2422
environ: _WSGIEnviron = ...
25-
user: AbstractUser
2623
session: SessionBase
2724
encoding: Any = ...
2825
def __init__(self, environ: _WSGIEnviron) -> None: ...

django-stubs/http/request.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ from typing import (
1717
)
1818

1919
from django.contrib.auth.base_user import AbstractBaseUser
20+
from django.contrib.auth.models import AnonymousUser
2021
from django.contrib.sessions.backends.base import SessionBase
2122
from django.contrib.sites.models import Site
2223
from django.utils.datastructures import CaseInsensitiveMapping, ImmutableList, MultiValueDict
@@ -51,7 +52,7 @@ class HttpRequest(BytesIO):
5152
resolver_match: ResolverMatch = ...
5253
content_type: Optional[str] = ...
5354
content_params: Optional[Dict[str, str]] = ...
54-
user: AbstractBaseUser
55+
user: Union[AbstractBaseUser, AnonymousUser]
5556
site: Site
5657
session: SessionBase
5758
encoding: Optional[str] = ...

flake8-pyi.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ select = F401, Y
1010
max_line_length = 120
1111
per-file-ignores =
1212
*__init__.pyi: F401
13+
base_user.pyi: Y003
14+
models.pyi: Y003
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
from mypy.plugin import AttributeContext
22
from mypy.types import Instance
33
from mypy.types import Type as MypyType
4+
from mypy.types import UnionType
45

56
from mypy_django_plugin.django.context import DjangoContext
67
from mypy_django_plugin.lib import helpers
78

89

910
def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType:
1011
auth_user_model = django_context.settings.AUTH_USER_MODEL
11-
model_cls = django_context.apps_registry.get_model(auth_user_model)
12-
model_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), model_cls)
13-
if model_info is None:
12+
user_cls = django_context.apps_registry.get_model(auth_user_model)
13+
user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), user_cls)
14+
15+
if user_info is None:
1416
return ctx.default_attr_type
1517

16-
return Instance(model_info, [])
18+
# Imported here because django isn't properly loaded yet when module is loaded
19+
from django.contrib.auth.models import AnonymousUser
20+
21+
anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser)
22+
if anonymous_user_info is None:
23+
# This shouldn't be able to happen, as we managed to import the model above...
24+
return Instance(user_info, [])
25+
26+
return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])])

test-data/typecheck/models/test_contrib_models.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
reveal_type(User().is_active) # N: Revealed type is 'builtins.bool*'
1111
reveal_type(User().date_joined) # N: Revealed type is 'datetime.datetime*'
1212
reveal_type(User().last_login) # N: Revealed type is 'Union[datetime.datetime, None]'
13+
reveal_type(User().is_authenticated) # N: Revealed type is 'Literal[True]'
14+
reveal_type(User().is_anonymous) # N: Revealed type is 'Literal[False]'
15+
16+
from django.contrib.auth.models import AnonymousUser
17+
reveal_type(AnonymousUser().is_authenticated) # N: Revealed type is 'Literal[False]'
18+
reveal_type(AnonymousUser().is_anonymous) # N: Revealed type is 'Literal[True]'
1319
1420
from django.contrib.auth.models import Permission
1521
reveal_type(Permission().name) # N: Revealed type is 'builtins.str*'

test-data/typecheck/test_request.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,28 @@
22
disable_cache: true
33
main: |
44
from django.http.request import HttpRequest
5-
reveal_type(HttpRequest().user) # N: Revealed type is 'myapp.models.MyUser'
5+
reveal_type(HttpRequest().user) # N: Revealed type is 'Union[myapp.models.MyUser, django.contrib.auth.models.AnonymousUser]'
66
# check that other fields work ok
77
reveal_type(HttpRequest().method) # N: Revealed type is 'Union[builtins.str, None]'
88
custom_settings: |
9-
INSTALLED_APPS = ('django.contrib.contenttypes', 'myapp')
9+
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth', 'myapp')
1010
AUTH_USER_MODEL='myapp.MyUser'
1111
files:
1212
- path: myapp/__init__.py
1313
- path: myapp/models.py
1414
content: |
1515
from django.db import models
1616
class MyUser(models.Model):
17-
pass
17+
pass
18+
- case: request_object_user_can_be_descriminated
19+
disable_cache: true
20+
main: |
21+
from django.http.request import HttpRequest
22+
request = HttpRequest()
23+
reveal_type(request.user) # N: Revealed type is 'Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]'
24+
if not request.user.is_anonymous:
25+
reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User'
26+
if request.user.is_authenticated:
27+
reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User'
28+
custom_settings: |
29+
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')

0 commit comments

Comments
 (0)