Skip to content
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

Resolve configured AUTH_USER_MODEL with a get_type_analyze_hook #2335

Merged
merged 8 commits into from
Aug 12, 2024
3 changes: 2 additions & 1 deletion mypy_django_plugin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,10 @@ def parse_ini_file(self, filepath: Path) -> None:
except ValueError:
exit_with_error(INVALID_BOOL_SETTING.format(key="strict_settings"))

def to_json(self) -> Dict[str, Any]:
def to_json(self, extra_data: Dict[str, Any]) -> Dict[str, Any]:
"""We use this method to reset mypy cache via `report_config_data` hook."""
return {
"django_settings_module": self.django_settings_module,
"strict_settings": self.strict_settings,
**dict(sorted(extra_data.items())),
}
4 changes: 4 additions & 0 deletions mypy_django_plugin/django/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,7 @@ def resolve_lookup_expected_type(

def resolve_f_expression_type(self, f_expression_type: Instance) -> ProperType:
return AnyType(TypeOfAny.explicit)

@cached_property
def is_contrib_auth_installed(self) -> bool:
return "django.contrib.auth" in self.settings.INSTALLED_APPS
39 changes: 27 additions & 12 deletions mypy_django_plugin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
request,
settings,
)
from mypy_django_plugin.transformers.auth import update_authenticate_hook
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
from mypy_django_plugin.transformers.managers import (
add_as_manager_to_queryset_class,
Expand Down Expand Up @@ -110,15 +111,14 @@ 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 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")]

# 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 @@ -148,9 +148,21 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
self._new_dependency("django.db.models.query"),
]

@cached_property
def contrib_auth_hooks(self) -> Dict[str, Callable[[FunctionContext], MypyType]]:
authenticate_hook = partial(update_authenticate_hook, django_context=self.django_context)
return {
"django.contrib.auth.get_user_model": partial(
settings.get_user_model_hook, django_context=self.django_context
),
"django.contrib.auth.authenticate": authenticate_hook,
"django.contrib.auth.aauthenticate": authenticate_hook,
}

def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], MypyType]]:
if fullname == "django.contrib.auth.get_user_model":
return partial(settings.get_user_model_hook, django_context=self.django_context)
auth_hook = self.contrib_auth_hooks.get(fullname)
if auth_hook is not None:
return auth_hook

info = self._get_typeinfo_or_none(fullname)
if info:
Expand Down Expand Up @@ -313,7 +325,10 @@ def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicCla

def report_config_data(self, ctx: ReportConfigContext) -> Dict[str, Any]:
# Cache would be cleared if any settings do change.
return self.plugin_config.to_json()
extra_data = {}
if ctx.id == "django.contrib.auth":
extra_data["AUTH_USER_MODEL"] = self.django_context.settings.AUTH_USER_MODEL
return self.plugin_config.to_json(extra_data)
Copy link
Member Author

Choose a reason for hiding this comment

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

Now we get "implicit" cache tainting on django.contrib.auth, and thus authenticate and friends if AUTH_USER_MODEL changes.

There might be other modules we want to add some extra config data to. Not sure though



def plugin(version: str) -> Type[NewSemanalDjangoPlugin]:
Expand Down
30 changes: 30 additions & 0 deletions mypy_django_plugin/transformers/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from mypy.plugin import FunctionContext
from mypy.types import Instance, NoneType, UnionType, get_proper_type
from mypy.types import Type as MypyType
from mypy.typevars import fill_typevars_with_any

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


def update_authenticate_hook(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
if not django_context.is_contrib_auth_installed:
return ctx.default_return_type

auth_user_model = django_context.settings.AUTH_USER_MODEL
api = helpers.get_typechecker_api(ctx)
model_info = helpers.resolve_lazy_reference(
auth_user_model, api=api, django_context=django_context, ctx=ctx.context
)
if model_info is None:
return ctx.default_return_type

optional_model = UnionType([fill_typevars_with_any(model_info), NoneType()], ctx.context.line, ctx.context.column)
default_return_type = get_proper_type(ctx.default_return_type)
if isinstance(default_return_type, Instance) and default_return_type.type.fullname == "typing.Coroutine":
if len(default_return_type.args) == 3:
return default_return_type.copy_modified(
args=[default_return_type.args[0], default_return_type.args[1], optional_model]
)
return ctx.default_return_type
return optional_model
51 changes: 51 additions & 0 deletions tests/typecheck/contrib/auth/test_auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
- case: test_authenticate_returns_configured_auth_user_model
main: |
from django.contrib.auth import authenticate, aauthenticate
reveal_type(authenticate()) # N: Revealed type is "Union[myapp.models.MyUser, None]"
async def f() -> None:
reveal_type(await aauthenticate()) # N: Revealed type is "Union[myapp.models.MyUser, None]"
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.db import models

class MyUser(models.Model):
...

- case: test_authenticate_returns_stubs_types_when_contrib_auth_is_not_installed
main: |
from django.contrib.auth import authenticate, aauthenticate
reveal_type(authenticate()) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, None]"
async def f() -> None:
reveal_type(await aauthenticate()) # N: Revealed type is "Union[django.contrib.auth.base_user.AbstractBaseUser, None]"
custom_settings: |
INSTALLED_APPS = ("django.contrib.contenttypes", "myapp")
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models

class MyUser(models.Model):
...

- case: test_authenticate_returns_builtin_auth_user_per_default
main: |
from django.contrib.auth import authenticate, aauthenticate
reveal_type(authenticate()) # N: Revealed type is "Union[django.contrib.auth.models.User, None]"
async def f() -> None:
reveal_type(await aauthenticate()) # N: Revealed type is "Union[django.contrib.auth.models.User, None]"
custom_settings: |
INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "myapp")
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models

class MyUser(models.Model):
...
Loading