Skip to content

Treat get_user_model as a dynamic class factory #1730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions django-stubs/contrib/auth/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ BACKEND_SESSION_KEY: str
HASH_SESSION_KEY: str
REDIRECT_FIELD_NAME: str

def get_user_model() -> type[AbstractBaseUser]: ...

_UserModel = get_user_model()

def load_backend(path: str) -> ModelBackend: ...
def get_backends() -> list[ModelBackend]: ...
def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> AbstractBaseUser | None: ...
def login(
request: HttpRequest, user: AbstractBaseUser | None, backend: type[ModelBackend] | str | None = ...
) -> None: ...
def authenticate(request: HttpRequest | None = ..., **credentials: Any) -> _UserModel | None: ...
def login(request: HttpRequest, user: _UserModel | None, backend: type[ModelBackend] | str | None = ...) -> None: ...
def logout(request: HttpRequest) -> None: ...
def get_user_model() -> type[AbstractBaseUser]: ...
def get_user(request: HttpRequest | Client) -> AbstractBaseUser | AnonymousUser: ...
def get_user(request: HttpRequest | Client) -> _UserModel | AnonymousUser: ...
def get_permission_codename(action: str, opts: Options) -> str: ...
def update_session_auth_hash(request: HttpRequest, user: AbstractBaseUser) -> None: ...
def update_session_auth_hash(request: HttpRequest, user: _UserModel) -> None: ...

default_app_config: str
17 changes: 8 additions & 9 deletions django-stubs/contrib/auth/backends.pyi
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
from typing import Any, TypeVar

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, Permission
from django.db.models import QuerySet
from django.db.models.base import Model
from django.http.request import HttpRequest
from typing_extensions import TypeAlias

_AnyUser: TypeAlias = AbstractBaseUser | AnonymousUser

UserModel: Any
_UserModel = get_user_model()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we declare _UserModel in 1 file and import it in the others? Or does _UserModel exist runtime?

Then we should probably keep it in contrib.auth next to definition of get_user_model

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this:

from django.contrib.auth import _UserModel as _UserModel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_UserModel does not exist at runtime, but in some modules UserModel does exist, for example in django/contrib/auth/backens.py. The subtest complains about these though, saying the typing doesn't match runtime for some attributes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also think we should import from one place it and not define for each module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave this approach a go now, but for some reason it ends up causing an endless defer look, which crashes mypy. Can we leave it like this for now and maybe revisit? It does kinda seem like a bug in mypy, cause we never get a call with final_iteration=True before it gives up, but I'm not entirely sure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't run the whole suite just yet, but I tried this diff:

         # for `get_user_model()`
-        if self.django_context.settings and any(
-            i.id == "django.contrib.auth" and any(name == "get_user_model" for name, _ in i.names)
-            for i in file.imports
-            if isinstance(i, ImportFrom)
-        ):
+        if file.fullname == "django.contrib.auth" and self.django_context.settings:
             auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
             ...

Together with these changes:

-from django.contrib.auth import get_user_model
+from django.contrib.auth import _UserModel
...

-_UserModel = get_user_model()

And an additional test case that depends on django.contrib.auth to declare a custom user, while also testing that the custom user is resolved via get_user_model by itself.

-   case: test_user_model_resolved_via_get_user_model
    main: |
        from typing import Union
        from django.contrib.auth import get_user_model
        from django.contrib.auth.models import AnonymousUser
        from django.http import HttpRequest, HttpResponse
        from django.contrib.auth.decorators import user_passes_test
        User = get_user_model()
        reveal_type(User)
        def check(user: Union[User, AnonymousUser]) -> bool: return True
        @user_passes_test(check)
        def view(request: HttpRequest) -> HttpResponse:
            reveal_type(request.user)
            return HttpResponse()
    out: |
        main:7: note: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyUser"
        main:11: note: Revealed type is "Union[myapp.models.MyUser, django.contrib.auth.models.AnonymousUser]"
    custom_settings: |
        INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "myapp")
        AUTH_USER_MODEL = "myapp.MyUser"
    files:
        - path: myapp/__init__.py
        - path: myapp/models.py
          content: |
              from django.contrib.auth.models import AbstractUser, PermissionsMixin

              class MyUser(AbstractUser, PermissionsMixin):
                  ...

I get both your test and the one above passing with these changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen things crash a bit randomly, but I'll try without the requests one. I though mypy was supposed to handle cyclic references though, but maybe not at this level?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try this test:

pytest tests/typecheck/test_shortcuts.yml::get_user_model_returns_proper_class

It's one of the ones triggering the cyclic dependencies problem here. If fails even if I remove all references to auth in requests.pyi

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I don't hit any errors when trying it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, so my issue is that I'm checking if django.contrib.auth is installed, and if it's not I don't add the dependencies. Guess I'll have to properly figure out what do when that isn't installed. I believe it'll raise an error at runtime 🤔

_AnyUser: TypeAlias = _UserModel | AnonymousUser

class BaseBackend:
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> AbstractBaseUser | None: ...
def get_user(self, user_id: int) -> AbstractBaseUser | None: ...
def authenticate(self, request: HttpRequest | None, **kwargs: Any) -> _UserModel | None: ...
def get_user(self, user_id: int) -> _UserModel | None: ...
def get_user_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
def get_group_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
def get_all_permissions(self, user_obj: _AnyUser, obj: Model | None = ...) -> set[str]: ...
Expand All @@ -22,7 +21,7 @@ class BaseBackend:
class ModelBackend(BaseBackend):
def authenticate(
self, request: HttpRequest | None, username: str | None = ..., password: str | None = ..., **kwargs: Any
) -> AbstractBaseUser | None: ...
) -> _UserModel | None: ...
def has_module_perms(self, user_obj: _AnyUser, app_label: str) -> bool: ...
def user_can_authenticate(self, user: _AnyUser | None) -> bool: ...
def with_perm(
Expand All @@ -31,11 +30,11 @@ class ModelBackend(BaseBackend):
is_active: bool = ...,
include_superusers: bool = ...,
obj: Model | None = ...,
) -> QuerySet[AbstractBaseUser]: ...
) -> QuerySet[_UserModel]: ...

class AllowAllUsersModelBackend(ModelBackend): ...

_U = TypeVar("_U", bound=AbstractBaseUser)
_U = TypeVar("_U", bound=_UserModel)

class RemoteUserBackend(ModelBackend):
create_unknown_user: bool
Expand Down
7 changes: 5 additions & 2 deletions django-stubs/contrib/auth/decorators.pyi
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from collections.abc import Callable, Iterable
from typing import TypeVar, overload

from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.http.response import HttpResponseBase

_VIEW = TypeVar("_VIEW", bound=Callable[..., HttpResponseBase])

_UserModel = get_user_model()

def user_passes_test(
test_func: Callable[[AbstractBaseUser | AnonymousUser], bool],
test_func: Callable[[_UserModel | AnonymousUser], bool],
login_url: str | None = ...,
redirect_field_name: str | None = ...,
) -> Callable[[_VIEW], _VIEW]: ...
Expand Down
6 changes: 4 additions & 2 deletions django-stubs/contrib/auth/middleware.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.http.request import HttpRequest
from django.utils.deprecation import MiddlewareMixin

def get_user(request: HttpRequest) -> AnonymousUser | AbstractBaseUser: ...
_UserModel = get_user_model()

def get_user(request: HttpRequest) -> AnonymousUser | _UserModel: ...

class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest) -> None: ...
Expand Down
5 changes: 2 additions & 3 deletions django-stubs/contrib/auth/password_validation.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ from collections.abc import Mapping, Sequence
from pathlib import Path, PosixPath
from typing import Any, Protocol

from django.db.models.base import Model
from typing_extensions import TypeAlias
from django.contrib.auth import get_user_model

_UserModel: TypeAlias = Model
_UserModel = get_user_model()

class PasswordValidator(Protocol):
def validate(self, __password: str, __user: _UserModel | None = ...) -> None: ...
Expand Down
12 changes: 7 additions & 5 deletions django-stubs/contrib/auth/tokens.pyi
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from datetime import date, datetime
from typing import Any

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth import get_user_model

_UserModel = get_user_model()

class PasswordResetTokenGenerator:
key_salt: str
secret: str | bytes
secret_fallbacks: list[str | bytes]
algorithm: str
def make_token(self, user: AbstractBaseUser) -> str: ...
def check_token(self, user: AbstractBaseUser | None, token: str | None) -> bool: ...
def _make_token_with_timestamp(self, user: AbstractBaseUser, timestamp: int, secret: str | bytes = ...) -> str: ...
def _make_hash_value(self, user: AbstractBaseUser, timestamp: int) -> str: ...
def make_token(self, user: _UserModel) -> str: ...
def check_token(self, user: _UserModel | None, token: str | None) -> bool: ...
def _make_token_with_timestamp(self, user: _UserModel, timestamp: int, secret: str | bytes = ...) -> str: ...
def _make_hash_value(self, user: _UserModel, timestamp: int) -> str: ...
def _num_seconds(self, dt: datetime | date) -> int: ...
def _now(self) -> datetime: ...

Expand Down
6 changes: 3 additions & 3 deletions django-stubs/contrib/auth/views.pyi
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Any

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm
from django.http.request import HttpRequest
from django.http.response import HttpResponse, HttpResponseRedirect
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView

UserModel: Any
_UserModel = get_user_model()

class RedirectURLMixin:
next_page: str | None
Expand Down Expand Up @@ -65,7 +65,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
token_generator: Any
validlink: bool
user: Any
def get_user(self, uidb64: str) -> AbstractBaseUser | None: ...
def get_user(self, uidb64: str) -> _UserModel | None: ...

class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
title: Any
Expand Down
6 changes: 4 additions & 2 deletions django-stubs/http/request.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ from io import BytesIO
from re import Pattern
from typing import Any, BinaryIO, Literal, NoReturn, TypeVar, overload

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sites.models import Site
Expand All @@ -12,6 +12,8 @@ from django.urls import ResolverMatch
from django.utils.datastructures import CaseInsensitiveMapping, ImmutableList, MultiValueDict
from typing_extensions import Self, TypeAlias

_UserModel = get_user_model()

RAISE_ERROR: object
host_validation_re: Pattern[str]

Expand Down Expand Up @@ -45,7 +47,7 @@ class HttpRequest(BytesIO):
# django.contrib.admin views:
current_app: str
# django.contrib.auth.middleware.AuthenticationMiddleware:
user: AbstractBaseUser | AnonymousUser
user: _UserModel | AnonymousUser
# django.middleware.locale.LocaleMiddleware:
LANGUAGE_CODE: str
# django.contrib.sites.middleware.CurrentSiteMiddleware
Expand Down
6 changes: 4 additions & 2 deletions django-stubs/test/client.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ from re import Pattern
from types import TracebackType
from typing import Any, Generic, NoReturn, TypeVar

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth import get_user_model
from django.contrib.sessions.backends.base import SessionBase
from django.core.handlers.asgi import ASGIRequest
from django.core.handlers.base import BaseHandler
Expand All @@ -23,6 +23,8 @@ MULTIPART_CONTENT: str
CONTENT_TYPE_RE: Pattern
JSON_CONTENT_TYPE_RE: Pattern

_UserModel = get_user_model()

class RedirectCycleError(Exception):
last_response: HttpResponseBase
redirect_chain: list[tuple[str, int]]
Expand Down Expand Up @@ -180,7 +182,7 @@ class ClientMixin:
@property
def session(self) -> SessionBase: ...
def login(self, **credentials: Any) -> bool: ...
def force_login(self, user: AbstractBaseUser, backend: str | None = ...) -> None: ...
def force_login(self, user: _UserModel, backend: str | None = ...) -> None: ...
def logout(self) -> None: ...

class Client(ClientMixin, _RequestFactory[_MonkeyPatchedWSGIResponse]):
Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/lib/fullnames.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
GET_USER_MODEL_FULLNAME = "django.contrib.auth.get_user_model"
ABSTRACT_USER_MODEL_FULLNAME = "django.contrib.auth.models.AbstractUser"
ABSTRACT_BASE_USER_MODEL_FULLNAME = "django.contrib.auth.base_user.AbstractBaseUser"
PERMISSION_MIXIN_CLASS_FULLNAME = "django.contrib.auth.models.PermissionsMixin"
MODEL_METACLASS_FULLNAME = "django.db.models.base.ModelBase"
MODEL_CLASS_FULLNAME = "django.db.models.base.Model"
Expand Down
32 changes: 17 additions & 15 deletions mypy_django_plugin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Any, Callable, Dict, List, Optional, Tuple, Type

from mypy.modulefinder import mypy_path
from mypy.nodes import MypyFile, TypeInfo
from mypy.nodes import ImportFrom, MypyFile, TypeInfo
from mypy.options import Options
from mypy.plugin import (
AnalyzeTypeContext,
Expand All @@ -23,7 +23,7 @@
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.exceptions import UnregisteredModelError
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 import fields, forms, init_create, meta, querysets, settings
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
from mypy_django_plugin.transformers.managers import (
create_new_manager_class_from_as_manager_method,
Expand Down Expand Up @@ -111,15 +111,18 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
return [self._new_dependency("typing"), self._new_dependency("django_stubs_ext")]

# for `get_user_model()`
if self.django_context.settings:
if file.fullname == "django.contrib.auth" or file.fullname in {"django.http", "django.http.request"}:
auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
try:
auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__
except LookupError:
# get_user_model() model app is not installed
return []
return [self._new_dependency(auth_user_module), self._new_dependency("django_stubs_ext")]
if self.django_context.settings and any(
i.id == "django.contrib.auth" and any(name == "get_user_model" for name, _ in i.names)
for i in file.imports
if isinstance(i, ImportFrom)
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a regular import dependency, on django.contrib.auth? Which mypy should establish by itself

At least I was thinking this hook is for "invisible" dependencies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a dependency on the configured user model, which might not be in django.contrib.auth. I guess one option we could do is to add the user model as a dependency of django.contrib.auth, that might be cleaner?

Copy link
Member

@flaeppe flaeppe Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] I guess one option we could do is to add the user model as a dependency of django.contrib.auth, that might be cleaner?

Yes, that reads exactly how I imagine this works

Copy link
Contributor Author

@ljodal ljodal Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, doing it like that causes the same issue as above where mypy gets stuck in an infinite loop

Edit: I see now that my issue is that django.contrib.auth is not registered as installed in the tests for some reason, so the dependency hook doesn't run

auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
try:
auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__
except LookupError:
# get_user_model() model app is not installed
return []
return [self._new_dependency(auth_user_module), self._new_dependency("django_stubs_ext")]

# ensure that all mentioned to='someapp.SomeModel' are loaded with corresponding related Fields
defined_model_classes = self.django_context.model_modules.get(file.fullname)
Expand Down Expand Up @@ -273,10 +276,6 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte
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)

# Lookup of the 'request.user' attribute
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)

# Lookup of the 'user.is_staff' or 'user.is_active' attribute
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)
Expand Down Expand Up @@ -306,6 +305,9 @@ def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeType
return None

def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
if fullname == fullnames.GET_USER_MODEL_FULLNAME:
return partial(settings.transform_get_user_model_hook, django_context=self.django_context)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add branching on a helper that checks if django.contrib.auth is installed?

Suggested change
if fullname == fullnames.GET_USER_MODEL_FULLNAME:
return partial(settings.transform_get_user_model_hook, django_context=self.django_context)
if fullname == fullnames.GET_USER_MODEL_FULLNAME and self.django_context.is_contrib_auth_installed:
return partial(settings.transform_get_user_model_hook, django_context=self.django_context)

That would help in the dependency hook as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was intending to fix this in a separate PR. I think it's better to change the return type to NoReturn when auth is not configured. That's give a somewhat sensible error message, rather than Variable "User" is not valid as a type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, that's totally fair, we can definitely do it later on

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Doing this would also assist in resolving #1380. We could include a test to resolve, looking like below, which currently crashes the plugin:

-   case: test_xyz
    main: |
        from django.contrib.auth import get_user_model
        User = get_user_model()
    installed_apps:
        -   myapp
    files:
        -   path: myapp/__init__.py
        -   path: myapp/models.py


# Create a new manager class definition when a manager's '.from_queryset' classmethod is called
class_name, _, method_name = fullname.rpartition(".")
if method_name == "from_queryset":
Expand Down
35 changes: 2 additions & 33 deletions mypy_django_plugin/transformers/request.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,8 @@
from mypy.plugin import AttributeContext, MethodContext
from mypy.types import Instance, UninhabitedType, UnionType
from mypy.plugin import MethodContext
from mypy.types import Type as MypyType
from mypy.types import UninhabitedType

from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import helpers


def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType:
if not django_context.apps_registry.is_installed("django.contrib.auth"):
return ctx.default_attr_type

# 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)

if user_info is None:
return ctx.default_attr_type

return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])])


def check_querydict_is_mutable(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
Expand Down
39 changes: 34 additions & 5 deletions mypy_django_plugin/transformers/settings.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
from mypy.nodes import MemberExpr
from mypy.plugin import AttributeContext, FunctionContext
from typing import Union

from mypy.nodes import GDEF, MemberExpr, PlaceholderNode, SymbolTableNode, TypeInfo
from mypy.plugin import AttributeContext, DynamicClassDefContext, FunctionContext
from mypy.types import AnyType, Instance, TypeOfAny, TypeType
from mypy.types import Type as MypyType

from mypy_django_plugin.config import DjangoPluginConfig
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import helpers
from mypy_django_plugin.lib import fullnames, helpers


def get_user_model_hook(ctx: FunctionContext, 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_cls_fullname = helpers.get_class_fullname(model_cls)

model_info = None
try:
model_cls = django_context.apps_registry.get_model(auth_user_model)
model_cls_fullname = helpers.get_class_fullname(model_cls)
except LookupError:
model_cls_fullname = fullnames.ABSTRACT_BASE_USER_MODEL_FULLNAME

model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), model_cls_fullname)
if model_info is None:
Expand All @@ -20,6 +27,28 @@ def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) ->
return TypeType(Instance(model_info, []))


def transform_get_user_model_hook(ctx: DynamicClassDefContext, django_context: DjangoContext) -> None:
auth_user_model = django_context.settings.AUTH_USER_MODEL
try:
model_cls = django_context.apps_registry.get_model(auth_user_model)
model_cls_fullname = helpers.get_class_fullname(model_cls)
except LookupError:
model_cls_fullname = fullnames.ABSTRACT_BASE_USER_MODEL_FULLNAME
Comment on lines +32 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have my lazy reference resolver in #1719 we wouldn't need to use the runtime model here:

Suggested change
auth_user_model = django_context.settings.AUTH_USER_MODEL
try:
model_cls = django_context.apps_registry.get_model(auth_user_model)
model_cls_fullname = helpers.get_class_fullname(model_cls)
except LookupError:
model_cls_fullname = fullnames.ABSTRACT_BASE_USER_MODEL_FULLNAME
auth_user_model = django_context.settings.AUTH_USER_MODEL
model_cls_fullname = helpers.resolve_lazy_reference(auth_user_model, ...)
if model_cls_fullname is None:
model_cls_fullname = fullnames.ABSTRACT_BASE_USER_MODEL_FULLNAME

We might want to use the fallback only on last iteration then


model_info: Union[TypeInfo, PlaceholderNode, None]
model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_semanal_api(ctx), model_cls_fullname)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
model_info: Union[TypeInfo, PlaceholderNode, None]
model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_semanal_api(ctx), model_cls_fullname)
model_info: Union[TypeInfo, PlaceholderNode, None] = helpers.lookup_fully_qualified_typeinfo(helpers.get_semanal_api(ctx), model_cls_fullname)

if model_info is None:
if not ctx.api.final_iteration:
ctx.api.defer()
# Temporarily replace the node with a PlaceholderNode. This suppresses
# 'Variable "..." is not valid as a type' errors from mypy
model_info = PlaceholderNode(model_cls_fullname, ctx.call, ctx.call.line)
else:
return

ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, model_info))


def get_type_of_settings_attribute(
ctx: AttributeContext, django_context: DjangoContext, plugin_config: DjangoPluginConfig
) -> MypyType:
Expand Down
Loading