Skip to content

Commit 8eeed9b

Browse files
authored
convert as_manager hooks to base class hook (#2282)
1 parent 59ebe6f commit 8eeed9b

File tree

2 files changed

+43
-71
lines changed

2 files changed

+43
-71
lines changed

mypy_django_plugin/main.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@
3737
)
3838
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
3939
from mypy_django_plugin.transformers.managers import (
40-
construct_as_manager_instance,
41-
create_new_manager_class_from_as_manager_method,
40+
add_as_manager_to_queryset_class,
4241
create_new_manager_class_from_from_queryset_method,
4342
reparametrize_any_manager_hook,
4443
resolve_manager_method,
@@ -209,10 +208,6 @@ def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], M
209208
fullnames.REVERSE_MANY_TO_ONE_DESCRIPTOR: manytoone.refine_many_to_one_related_manager,
210209
}
211210
return hooks.get(class_fullname)
212-
elif method_name == "as_manager":
213-
info = self._get_typeinfo_or_none(class_fullname)
214-
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
215-
return partial(construct_as_manager_instance, info=info)
216211

217212
if method_name in self.manager_and_queryset_method_hooks:
218213
info = self._get_typeinfo_or_none(class_fullname)
@@ -250,6 +245,10 @@ def get_base_class_hook(self, fullname: str) -> Optional[Callable[[ClassDefConte
250245
# Base class is a Form class definition
251246
if fullname in self._get_current_form_bases():
252247
return transform_form_class
248+
249+
# Base class is a QuerySet class definition
250+
if sym is not None and isinstance(sym.node, TypeInfo) and sym.node.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
251+
return add_as_manager_to_queryset_class
253252
return None
254253

255254
def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeContext], MypyType]]:
@@ -308,10 +307,6 @@ def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicCla
308307
info = self._get_typeinfo_or_none(class_name)
309308
if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME):
310309
return create_new_manager_class_from_from_queryset_method
311-
elif method_name == "as_manager":
312-
info = self._get_typeinfo_or_none(class_name)
313-
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
314-
return create_new_manager_class_from_as_manager_method
315310
return None
316311

317312
def report_config_data(self, ctx: ReportConfigContext) -> Dict[str, Any]:

mypy_django_plugin/transformers/managers.py

Lines changed: 38 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
SymbolTableNode,
1717
TypeInfo,
1818
)
19-
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext, MethodContext
19+
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext
20+
from mypy.plugins.common import add_method_to_class
2021
from mypy.semanal import SemanticAnalyzer
2122
from mypy.semanal_shared import has_placeholder
2223
from mypy.subtypes import find_member
@@ -482,44 +483,37 @@ def populate_manager_from_queryset(manager_info: TypeInfo, queryset_info: TypeIn
482483
)
483484

484485

485-
def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext) -> None:
486-
"""
487-
Insert a new manager class node for a
488-
489-
```
490-
<manager name> = <QuerySet>.as_manager()
491-
```
492-
"""
486+
def add_as_manager_to_queryset_class(ctx: ClassDefContext) -> None:
493487
semanal_api = helpers.get_semanal_api(ctx)
494-
# Don't redeclare the manager class if we've already defined it.
495-
manager_node = semanal_api.lookup_current_scope(ctx.name)
496-
if manager_node and manager_node.type is not None:
497-
# This is just a deferral run where our work is already finished
498-
return
499488

500-
manager_sym = semanal_api.lookup_fully_qualified_or_none(fullnames.MANAGER_CLASS_FULLNAME)
501-
assert manager_sym is not None
502-
manager_base = manager_sym.node
503-
if manager_base is None:
489+
def _defer() -> None:
504490
if not semanal_api.final_iteration:
505491
semanal_api.defer()
506-
return
507492

508-
assert isinstance(manager_base, TypeInfo)
493+
queryset_info = semanal_api.type
494+
if queryset_info is None:
495+
return _defer()
509496

510-
callee = ctx.call.callee
511-
assert isinstance(callee, MemberExpr)
512-
assert isinstance(callee.expr, RefExpr)
497+
# either a manual `as_manager` definition or this is a deferral pass
498+
if "as_manager" in queryset_info.names:
499+
return
513500

514-
queryset_info = callee.expr.node
515-
if queryset_info is None:
516-
if not semanal_api.final_iteration:
517-
semanal_api.defer()
501+
base_as_manager = queryset_info.get("as_manager")
502+
if (
503+
base_as_manager is None
504+
or not isinstance(base_as_manager.type, CallableType)
505+
or not isinstance(base_as_manager.type.ret_type, Instance)
506+
):
518507
return
519508

520-
assert isinstance(queryset_info, TypeInfo)
509+
base_ret_type = base_as_manager.type.ret_type.type
510+
511+
manager_sym = semanal_api.lookup_fully_qualified_or_none(fullnames.MANAGER_CLASS_FULLNAME)
512+
if manager_sym is None or not isinstance(manager_sym.node, TypeInfo):
513+
return _defer()
521514

522-
manager_class_name = manager_base.name + "From" + queryset_info.name
515+
manager_base = manager_sym.node
516+
manager_class_name = f"{manager_base.name}From{queryset_info.name}"
523517
current_module = semanal_api.modules[semanal_api.cur_mod_id]
524518
existing_sym = current_module.names.get(manager_class_name)
525519
if (
@@ -535,54 +529,37 @@ def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext)
535529
try:
536530
new_manager_info = create_manager_class(
537531
api=semanal_api,
538-
base_manager_info=manager_base,
532+
base_manager_info=base_ret_type,
539533
name=manager_class_name,
540-
line=ctx.call.line,
534+
line=queryset_info.line,
541535
with_unique_name=True,
542536
)
543537
except helpers.IncompleteDefnException:
544-
if not semanal_api.final_iteration:
545-
semanal_api.defer()
546-
return
538+
return _defer()
547539

548540
populate_manager_from_queryset(new_manager_info, queryset_info)
549541
register_dynamically_created_manager(
550542
fullname=new_manager_info.fullname,
551543
manager_name=manager_class_name,
552544
manager_base=manager_base,
553545
)
554-
queryset_info.metadata.setdefault("django_as_manager_names", {})
555-
queryset_info.metadata["django_as_manager_names"][semanal_api.cur_mod_id] = new_manager_info.name
556546

557547
# Add the new manager to the current module
558-
added = semanal_api.add_symbol_table_node(
559-
# We'll use `new_manager_info.name` instead of `manager_class_name` here
560-
# to handle possible name collisions, as it's unique.
561-
new_manager_info.name,
548+
# We'll use `new_manager_info.name` instead of `manager_class_name` here
549+
# to handle possible name collisions, as it's unique.
550+
current_module.names[new_manager_info.name] = (
562551
# Note that the generated manager type is always inserted at module level
563-
SymbolTableNode(GDEF, new_manager_info, plugin_generated=True),
552+
SymbolTableNode(GDEF, new_manager_info, plugin_generated=True)
564553
)
565-
assert added
566-
567554

568-
def construct_as_manager_instance(ctx: MethodContext, *, info: TypeInfo) -> MypyType:
569-
api = helpers.get_typechecker_api(ctx)
570-
module = helpers.get_current_module(api)
571-
try:
572-
manager_name = info.metadata["django_as_manager_names"][module.fullname]
573-
except KeyError:
574-
return ctx.default_return_type
575-
576-
manager_node = api.lookup(manager_name)
577-
if not isinstance(manager_node.node, TypeInfo):
578-
return ctx.default_return_type
579-
580-
# Whenever `<QuerySet>.as_manager()` isn't called at class level, we want to ensure
581-
# that the variable is an instance of our generated manager. Instead of the return
582-
# value of `.as_manager()`. Though model argument is populated as `Any`.
583-
# `transformers.models.AddManagers` will populate a model's manager(s), when it
584-
# finds it on class level.
585-
return Instance(manager_node.node, [AnyType(TypeOfAny.from_omitted_generics)])
555+
add_method_to_class(
556+
semanal_api,
557+
ctx.cls,
558+
"as_manager",
559+
args=[],
560+
return_type=Instance(new_manager_info, [AnyType(TypeOfAny.from_omitted_generics)]),
561+
is_classmethod=True,
562+
)
586563

587564

588565
def reparametrize_any_manager_hook(ctx: ClassDefContext) -> None:

0 commit comments

Comments
 (0)