Don't change type of HttpRequest.user if type has been changed by subclassing (#415)

* Don't change type of HttpRequest.user if type has been changed by subclassing

* Asserts for typing

* Add tests

* Add description of HttpRequest subclassing to README

* Dummy to rebuild travis
This commit is contained in:
Alexander Viklund
2020-07-07 11:52:21 +02:00
committed by GitHub
parent b1d619edb2
commit 3704d0ab98
3 changed files with 50 additions and 12 deletions

View File

@@ -90,19 +90,24 @@ You can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel]'`, thi
Currently we [are working](https://github.com/django/django/pull/12405) on providing `__class_getitem__` to the classes where we need them. Currently we [are working](https://github.com/django/django/pull/12405) on providing `__class_getitem__` to the classes where we need them.
### How can I use HttpRequest with custom user model? ### How can I create a HttpRequest that's guaranteed to have an authenticated user?
You can subclass standard request like so: Django's built in `HttpRequest` has the attribute `user` that resolves to the type
```python
Union[User, AnonymousUser]
```
where `User` is the user model specified by the `AUTH_USER_MODEL` setting.
If you want a `HttpRequest` that you can type-annotate with where you know that the user is authenticated you can subclass the normal `HttpRequest` class like so:
```python ```python
from django.http import HttpRequest from django.http import HttpRequest
from my_user_app.models import MyUser from my_user_app.models import MyUser
class MyRequest(HttpRequest): class AuthenticatedHttpRequest(HttpRequest):
user: MyUser user: MyUser
``` ```
And then use `MyRequest` instead of standard `HttpRequest` inside your project. And then use `AuthenticatedHttpRequest` instead of the standard `HttpRequest` for when you know that the user is authenticated. For example in views using the `@login_required` decorator.
## Related projects ## Related projects

View File

@@ -8,6 +8,22 @@ from mypy_django_plugin.lib import helpers
def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType: def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType:
# Imported here because django isn't properly loaded yet when module is loaded
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import AnonymousUser
abstract_base_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AbstractBaseUser)
anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser)
# This shouldn't be able to happen, as we managed to import the models above.
assert abstract_base_user_info is not None
assert anonymous_user_info is not None
if ctx.default_attr_type != UnionType([Instance(abstract_base_user_info, []), Instance(anonymous_user_info, [])]):
# Type has been changed from the default in django-stubs.
# I.e. HttpRequest has been subclassed and user-type overridden, so let's leave it as is.
return ctx.default_attr_type
auth_user_model = django_context.settings.AUTH_USER_MODEL auth_user_model = django_context.settings.AUTH_USER_MODEL
user_cls = django_context.apps_registry.get_model(auth_user_model) user_cls = django_context.apps_registry.get_model(auth_user_model)
user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), user_cls) user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), user_cls)
@@ -15,12 +31,4 @@ def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_c
if user_info is None: if user_info is None:
return ctx.default_attr_type return ctx.default_attr_type
# 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, [])]) return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])])

View File

@@ -27,3 +27,28 @@
reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User' reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User'
custom_settings: | custom_settings: |
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth') INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')
- case: subclass_request_not_changed_user_type
disable_cache: true
main: |
from django.http.request import HttpRequest
class MyRequest(HttpRequest):
foo: int # Just do something
request = MyRequest()
reveal_type(request.user) # N: Revealed type is 'Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]'
custom_settings: |
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')
- case: subclass_request_changed_user_type
disable_cache: true
main: |
from django.http.request import HttpRequest
from django.contrib.auth.models import User
class MyRequest(HttpRequest):
user: User # Override the type of user
request = MyRequest()
reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User'
custom_settings: |
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')