diff --git a/README.md b/README.md index 0b8f7c9..5162556 100644 --- a/README.md +++ b/README.md @@ -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. -### 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 from django.http import HttpRequest from my_user_app.models import MyUser -class MyRequest(HttpRequest): +class AuthenticatedHttpRequest(HttpRequest): 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 diff --git a/mypy_django_plugin/transformers/request.py b/mypy_django_plugin/transformers/request.py index 45b212d..83899ce 100644 --- a/mypy_django_plugin/transformers/request.py +++ b/mypy_django_plugin/transformers/request.py @@ -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: + # 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 user_cls = django_context.apps_registry.get_model(auth_user_model) 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: 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, [])]) diff --git a/test-data/typecheck/test_request.yml b/test-data/typecheck/test_request.yml index 4a72402..48a4ec7 100644 --- a/test-data/typecheck/test_request.yml +++ b/test-data/typecheck/test_request.yml @@ -27,3 +27,28 @@ reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User' custom_settings: | 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') +