Repository: typeddjango/django-stubs

Issue:
user.is_staff complex type
Hello, as I am not sure if this is an issue related to django-stubs, or djangorestframework-stubs, or just my code, I am posting here as well. I created this issue https://github.com/typeddjango/djangorestframework-stubs/issues/109
Seems `user.is_staff` does not agree with the `bool` return type annotation I gave to my method. Is this an issue for fixing in django-stubs or djangorestframework-stubs? If it is an issue of my code, please suggest what I could do.


Discussion:
Can you please post what `reveal_type(user)` and `reveal_type(user.is_stuff)` produces? 
user: Revealed type is 'Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]'
user.is_staff: Revealed type is 'Union[django.db.models.fields.BooleanField[Union[builtins.bool, django.db.models.expressions.Combinable], builtins.bool], builtins.bool]'
could always just use `bool(user.is_active)`. calling `user.clean_fields()` will ensure the instance has a bool there
> could always just use `bool(user.is_active)`. calling `user.clean_fields()` will ensure the instance has a bool there

Yep, I was hoping to solve it without adding more visual clutter to the code.
I've read your comments and referred issues/PRs and I'm still not sure what is the solution other than wrapping it in `bool()`. I think that assuming bool should be totally okay, but I don't understand how it works in django/drf in general. I can use a model's let's say `FloatField` as a `float`. Here it breaks because of the union, and this (failing) testcase shows it.

```patch
diff --git a/tests/typecheck/contrib/auth/test_misc.yml b/tests/typecheck/contrib/auth/test_misc.yml
index b9207ce..169f9fb 100644
--- a/tests/typecheck/contrib/auth/test_misc.yml
+++ b/tests/typecheck/contrib/auth/test_misc.yml
@@ -13,3 +13,21 @@
         from django.test import Client
 
         get_user(Client())
+-   case: test_user_fields
+    main: |
+        from typing import Union
+        from django.contrib.auth.models import User
+        from django.contrib.auth.models import AnonymousUser
+
+        anonymous: AnonymousUser
+        is_staff1: bool = anonymous.is_staff
+        is_active1: bool = anonymous.is_active
+
+        user: User
+        is_staff2: bool = user.is_staff
+        is_active2: bool = user.is_active
+
+        union: Union[User, AnonymousUser]
+        is_staff3: bool = union.is_staff
+        is_active3: bool = union.is_active
+        is_authenticated: bool = union.is_authenticated
```
Output:
```plain
tests/typecheck/contrib/auth/test_misc.yml::test_user_fields FAILED

=========================================================== FAILURES ===========================================================
_______________________________________________________ test_user_fields _______________________________________________________
/.../django-stubs/tests/typecheck/contrib/auth/test_misc.yml:17:
E   pytest_mypy_plugins.utils.TypecheckAssertionError: Output is not expected:
E   Expected:
E   Actual:
E     main:14: error: Incompatible types in assignment (expression has type "Union[BooleanField[Union[bool, Combinable], bool], bool]", variable has type "bool") (diff)
E     main:15: error: Incompatible types in assignment (expression has type "Union[BooleanField[Union[bool, Combinable], bool], bool]", variable has type "bool") (diff)
=================================================== short test summary info ====================================================
FAILED tests/typecheck/contrib/auth/test_misc.yml::test_user_fields -
====================================================== 1 failed in 3.33s =======================================================

```
@MarcinWieczorek can you please test other fields of `AnonymousUser`? Are they revealed correctly? 
Yes, but what would be a fix? Maybe defining an user class inheriting both `User` and `AnonymousUser` and typing their common fields to bool? That would add clutter and probably break existing types.
If other fields of `AnonymousUser` are revealed incorrectly, then the fix would be to change the type-inference logic for it.
It seems that only is_authenticated is working correctly.

```
E   pytest_mypy_plugins.utils.TypecheckAssertionError: Output is not expected:
E   Expected:
E   Actual:
E     main:17: error: Incompatible types in assignment (expression has type "Union[BooleanField[Union[bool, Combinable], bool], bool]", variable has type "bool") (diff)
E     main:18: error: Incompatible types in assignment (expression has type "Union[BooleanField[Union[bool, Combinable], bool], bool]", variable has type "bool") (diff)
E     main:19: error: Incompatible types in assignment (expression has type "Union[BooleanField[Union[bool, Combinable], bool], bool]", variable has type "bool") (diff)
```

```patch
diff --git a/tests/typecheck/contrib/auth/test_misc.yml b/tests/typecheck/contrib/auth/test_misc.yml
index b9207ce..39fe44b 100644
--- a/tests/typecheck/contrib/auth/test_misc.yml
+++ b/tests/typecheck/contrib/auth/test_misc.yml
@@ -13,3 +13,25 @@
         from django.test import Client
 
         get_user(Client())
+-   case: test_user_fields
+    main: |
+        from typing import Union
+        from django.contrib.auth.models import AnonymousUser, User
+
+        anonymous: AnonymousUser
+        anonymous_is_staff: bool = anonymous.is_staff
+        anonymous_is_active: bool = anonymous.is_active
+        anonymous_is_superuser: bool = anonymous.is_superuser
+        anonymous_is_authenticated: bool = anonymous.is_authenticated
+
+        user: User
+        user_is_staff: bool = user.is_staff
+        user_is_active: bool = user.is_active
+        user_is_superuser: bool = user.is_superuser
+        user_is_authenticated: bool = user.is_authenticated
+
+        union: Union[User, AnonymousUser]
+        union_is_staff: bool = union.is_staff
+        union_is_active: bool = union.is_active
+        union_is_superuser: bool = union.is_superuser
+        union_is_authenticated: bool = union.is_authenticated
```
@MarcinWieczorek and I know why this happens 🙂 

Because, `AnonymousUser` is not a `models.Model` subclass: https://github.com/django/django/blob/5fcfe5361e5b8c9738b1ee4c1e9a6f293a7dda40/django/contrib/auth/models.py#L400

However, this should be a special case I think.

Here's when we work with it in our plugin: https://github.com/typeddjango/django-stubs/blob/d9c851abcedf6a2a434da9a9dbda7aa536edce52/mypy_django_plugin/transformers/request.py#L19

Currently we only test these fields: https://github.com/typeddjango/django-stubs/blob/44151c485d167238889f2afd0b90c1b5f63200bb/tests/typecheck/models/test_contrib_models.yml#L16-L18

Any PRs are more than welcome. I won't have time to work on this (by writting code) for the next ~3/4 months.
i'll look into this in the next few hours, see what i can do
@void-witch thanks a lot! 

offtop: oh, you've changed your profile pic and username! It took me sometime 🙂 
@sobolevn oh yes! apologies, i didn't mean to switch them both so quickly, forgot that would be disorienting haha
i dug through a lot of the plugin code and got really lost. it would help if i had an easy way to debug the plugin to find where the issue is, but that line you linked isn't even being run by any of the minimal examples i try, and digging through the callstack is painful


Have you seen `mypy --pbd`? https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-pdb
i have, it doesn't drop into a pdb shell. i've added breakpoints all over the `set_auth_user_model_as_type_for_request_user` function, no dice (maybe the `partial` is doing something that breaks pdb?). `find`+`grep` says it's only called here:

https://github.com/typeddjango/django-stubs/blob/d9c851abcedf6a2a434da9a9dbda7aa536edce52/mypy_django_plugin/main.py#L274

and setting breakpoints after this line works alright, but it's a really complex call stack and i couldn't get through it

https://github.com/typeddjango/django-stubs/blob/d9c851abcedf6a2a434da9a9dbda7aa536edce52/mypy_django_plugin/main.py#L272

i'll try again now that i'm somewhat more awake, but i think i'm gonna need some help on this one haha
aha, i got it working. the following example no longer fails on `is_staff`, but on `is_active`

```
from typing import Union
from django.contrib.auth.models import User
from django.contrib.auth.models import AnonymousUser

union: Union[User, AnonymousUser]
is_staff3: bool = union.is_staff
is_active3: bool = union.is_active
is_authenticated: bool = union.is_authenticated
```

is this fix something we want to apply to *all* BooleanFields on a class inheriting from AbstractUser, or should this be conservative and only apply to these two attributes?
I think those 3 that failed in my test
i've run into a weird off-by-one-character issue in a pre-existing test, asking the mypy gitter about it now, but it should be done soon. in the meantime, if anyone knows whats the difference between `builtins.bool` and `builtins.bool*` in the output of `reveal_type` is, please let me know
I don't understand it fully but here's something that might help
https://stackoverflow.com/questions/50498575/what-does-the-asterisk-in-the-output-of-reveal-type-mean
ah thank you, i wasn't finding anything helpful from google
pr should be ready within the next hour!
Btw, I would love to accept PRs with `--ignore-inffered-char` into `pytest-mypy-plugins` 

This drives me mad sometimes! Example: https://github.com/dry-python/returns/commit/ca5b8f613fc255b2b6cae05354a5bd048ad73fec

Resolving patch:
diff --git a/mypy_django_plugin/lib/fullnames.py b/mypy_django_plugin/lib/fullnames.py
--- a/mypy_django_plugin/lib/fullnames.py
+++ b/mypy_django_plugin/lib/fullnames.py
@@ -1,3 +1,5 @@
+ABSTRACT_USER_MODEL_FULLNAME = "django.contrib.auth.models.AbstractUser"
+PERMISSION_MIXIN_CLASS_FULLNAME = "django.contrib.auth.models.PermissionsMixin"
 MODEL_CLASS_FULLNAME = "django.db.models.base.Model"
 FIELD_FULLNAME = "django.db.models.fields.Field"
 CHAR_FIELD_FULLNAME = "django.db.models.fields.CharField"
diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py
--- a/mypy_django_plugin/main.py
+++ b/mypy_django_plugin/main.py
@@ -22,7 +22,7 @@
 from mypy_django_plugin.lib import fullnames, helpers
 from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
 from mypy_django_plugin.transformers.managers import create_new_manager_class_from_from_queryset_method
-from mypy_django_plugin.transformers.models import process_model_class
+from mypy_django_plugin.transformers.models import process_model_class, set_auth_user_model_boolean_fields
 
 
 def transform_model_class(ctx: ClassDefContext, django_context: DjangoContext) -> None:
@@ -270,8 +270,12 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte
             return partial(settings.get_type_of_settings_attribute, django_context=self.django_context)
 
         info = self._get_typeinfo_or_none(class_name)
+        if info and info.has_base(fullnames.PERMISSION_MIXIN_CLASS_FULLNAME) and attr_name == "is_superuser":
+            return partial(set_auth_user_model_boolean_fields, django_context=self.django_context)
         if info and info.has_base(fullnames.HTTPREQUEST_CLASS_FULLNAME) and attr_name == "user":
             return partial(request.set_auth_user_model_as_type_for_request_user, django_context=self.django_context)
+        if info and info.has_base(fullnames.ABSTRACT_USER_MODEL_FULLNAME) and attr_name in ("is_staff", "is_active"):
+            return partial(set_auth_user_model_boolean_fields, django_context=self.django_context)
         return None
 
     def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py
--- a/mypy_django_plugin/transformers/models.py
+++ b/mypy_django_plugin/transformers/models.py
@@ -5,7 +5,7 @@
 from django.db.models.fields.related import ForeignKey
 from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
 from mypy.nodes import ARG_STAR2, Argument, Context, FuncDef, TypeInfo, Var
-from mypy.plugin import ClassDefContext
+from mypy.plugin import AttributeContext, ClassDefContext
 from mypy.plugins import common
 from mypy.semanal import SemanticAnalyzer
 from mypy.types import AnyType, Instance
@@ -355,3 +355,9 @@ def process_model_class(ctx: ClassDefContext, django_context: DjangoContext) ->
         except helpers.IncompleteDefnException:
             if not ctx.api.final_iteration:
                 ctx.api.defer()
+
+
+def set_auth_user_model_boolean_fields(ctx: AttributeContext, django_context: DjangoContext) -> MypyType:
+    boolinfo = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), bool)
+    assert boolinfo is not None
+    return Instance(boolinfo, [])


Test patch:
diff --git a/tests/typecheck/contrib/auth/test_misc.yml b/tests/typecheck/contrib/auth/test_misc.yml
--- a/tests/typecheck/contrib/auth/test_misc.yml
+++ b/tests/typecheck/contrib/auth/test_misc.yml
@@ -13,3 +13,25 @@
         from django.test import Client
 
         get_user(Client())
+-   case: test_user_fields
+    main: |
+        from typing import Union
+        from django.contrib.auth.models import AnonymousUser, User
+
+        anonymous: AnonymousUser
+        anonymous_is_staff: bool = anonymous.is_staff
+        anonymous_is_active: bool = anonymous.is_active
+        anonymous_is_superuser: bool = anonymous.is_superuser
+        anonymous_is_authenticated: bool = anonymous.is_authenticated
+
+        user: User
+        user_is_staff: bool = user.is_staff
+        user_is_active: bool = user.is_active
+        user_is_superuser: bool = user.is_superuser
+        user_is_authenticated: bool = user.is_authenticated
+
+        union: Union[User, AnonymousUser]
+        union_is_staff: bool = union.is_staff
+        union_is_active: bool = union.is_active
+        union_is_superuser: bool = union.is_superuser
+        union_is_authenticated: bool = union.is_authenticated
diff --git a/tests/typecheck/models/test_contrib_models.yml b/tests/typecheck/models/test_contrib_models.yml
--- a/tests/typecheck/models/test_contrib_models.yml
+++ b/tests/typecheck/models/test_contrib_models.yml
@@ -6,8 +6,8 @@
         reveal_type(User().first_name)  # N: Revealed type is 'builtins.str*'
         reveal_type(User().last_name)  # N: Revealed type is 'builtins.str*'
         reveal_type(User().email)  # N: Revealed type is 'builtins.str*'
-        reveal_type(User().is_staff)  # N: Revealed type is 'builtins.bool*'
-        reveal_type(User().is_active)  # N: Revealed type is 'builtins.bool*'
+        reveal_type(User().is_staff)  # N: Revealed type is 'builtins.bool'
+        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]'
@@ -22,7 +22,7 @@
         reveal_type(Permission().codename)  # N: Revealed type is 'builtins.str*'
 
         from django.contrib.auth.models import PermissionsMixin
-        reveal_type(PermissionsMixin().is_superuser)  # N: Revealed type is 'builtins.bool*'
+        reveal_type(PermissionsMixin().is_superuser)  # N: Revealed type is 'builtins.bool'
 
         from django.contrib.auth.models import Group
         reveal_type(Group().name)  # N: Revealed type is 'builtins.str*'