From 658de866ccb246d0cdac364d5ba226ca1115fd87 Mon Sep 17 00:00:00 2001 From: Anthony Sottile <asottile@umich.edu> Date: Fri, 26 Jul 2024 13:26:18 -0400 Subject: [PATCH] construct as_manager instance at checker time --- mypy_django_plugin/main.py | 5 +++ mypy_django_plugin/transformers/managers.py | 41 +++++++++++-------- .../managers/querysets/test_as_manager.yml | 6 +-- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index e9f549b70..554550378 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -37,6 +37,7 @@ ) from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute from mypy_django_plugin.transformers.managers import ( + construct_as_manager_instance, create_new_manager_class_from_as_manager_method, create_new_manager_class_from_from_queryset_method, reparametrize_any_manager_hook, @@ -208,6 +209,10 @@ def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], M fullnames.REVERSE_MANY_TO_ONE_DESCRIPTOR: manytoone.refine_many_to_one_related_manager, } return hooks.get(class_fullname) + elif method_name == "as_manager": + info = self._get_typeinfo_or_none(class_fullname) + if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME): + return partial(construct_as_manager_instance, info=info) if method_name in self.manager_and_queryset_method_hooks: info = self._get_typeinfo_or_none(class_fullname) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 79f8c71cb..ec13bf20c 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -15,9 +15,8 @@ StrExpr, SymbolTableNode, TypeInfo, - Var, ) -from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext +from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext, MethodContext from mypy.semanal import SemanticAnalyzer from mypy.semanal_shared import has_placeholder from mypy.subtypes import find_member @@ -552,23 +551,9 @@ def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext) manager_name=manager_class_name, manager_base=manager_base, ) + queryset_info.metadata.setdefault("django_as_manager_names", {}) + queryset_info.metadata["django_as_manager_names"][semanal_api.cur_mod_id] = new_manager_info.name - # Whenever `<QuerySet>.as_manager()` isn't called at class level, we want to ensure - # that the variable is an instance of our generated manager. Instead of the return - # value of `.as_manager()`. Though model argument is populated as `Any`. - # `transformers.models.AddManagers` will populate a model's manager(s), when it - # finds it on class level. - var = Var(name=ctx.name, type=Instance(new_manager_info, [AnyType(TypeOfAny.from_omitted_generics)])) - var.info = new_manager_info - var._fullname = f"{current_module.fullname}.{ctx.name}" - var.is_inferred = True - # Note: Order of `add_symbol_table_node` calls matters. Depending on what level - # we've found the `.as_manager()` call. Point here being that we want to replace the - # `.as_manager` return value with our newly created manager. - added = semanal_api.add_symbol_table_node( - ctx.name, SymbolTableNode(semanal_api.current_symbol_kind(), var, plugin_generated=True) - ) - assert added # Add the new manager to the current module added = semanal_api.add_symbol_table_node( # We'll use `new_manager_info.name` instead of `manager_class_name` here @@ -580,6 +565,26 @@ def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext) assert added +def construct_as_manager_instance(ctx: MethodContext, *, info: TypeInfo) -> MypyType: + api = helpers.get_typechecker_api(ctx) + module = helpers.get_current_module(api) + try: + manager_name = info.metadata["django_as_manager_names"][module.fullname] + except KeyError: + return ctx.default_return_type + + manager_node = api.lookup(manager_name) + if not isinstance(manager_node.node, TypeInfo): + return ctx.default_return_type + + # Whenever `<QuerySet>.as_manager()` isn't called at class level, we want to ensure + # that the variable is an instance of our generated manager. Instead of the return + # value of `.as_manager()`. Though model argument is populated as `Any`. + # `transformers.models.AddManagers` will populate a model's manager(s), when it + # finds it on class level. + return Instance(manager_node.node, [AnyType(TypeOfAny.from_omitted_generics)]) + + def reparametrize_any_manager_hook(ctx: ClassDefContext) -> None: """ Add implicit generics to manager classes that are defined without generic. diff --git a/tests/typecheck/managers/querysets/test_as_manager.yml b/tests/typecheck/managers/querysets/test_as_manager.yml index 9bdfec735..641d4be30 100644 --- a/tests/typecheck/managers/querysets/test_as_manager.yml +++ b/tests/typecheck/managers/querysets/test_as_manager.yml @@ -28,7 +28,7 @@ def just_int(self) -> int: ... class MyModel(models.Model): - objects = MyQuerySet.as_manager() # type: ignore[var-annotated] + objects = MyQuerySet.as_manager() class QuerySetWithoutSelf(models.QuerySet["MyModelWithoutSelf"]): def method(self) -> "QuerySetWithoutSelf": @@ -75,7 +75,7 @@ ... class MyModel(models.Model): - objects = MyQuerySet.as_manager() # type: ignore[var-annotated] + objects = MyQuerySet.as_manager() - case: model_gets_generated_manager_as_default_manager main: | @@ -285,7 +285,7 @@ objects = MyModelQuerySet.as_manager() class MyOtherModel(models.Model): - objects = _MyModelQuerySet2.as_manager() # type: ignore + objects = _MyModelQuerySet2.as_manager() - case: handles_type_vars main: |