Skip to content

Commit edfebe4

Browse files
authored
Do many related manager creation during semantic analysis (#2231)
When finding a `ManyToManyField` we can directly create 2 `ManyRelatedManager`s, one for each side of the relation.
1 parent ec37d06 commit edfebe4

File tree

3 files changed

+69
-41
lines changed

3 files changed

+69
-41
lines changed

mypy_django_plugin/transformers/manytomany.py

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mypy.nodes import AssignmentStmt, Expression, MemberExpr, NameExpr, Node, RefExpr, StrExpr, TypeInfo
55
from mypy.plugin import FunctionContext, MethodContext
66
from mypy.semanal import SemanticAnalyzer
7-
from mypy.types import Instance, ProperType, TypeVarType, UninhabitedType
7+
from mypy.types import Instance, ProperType, UninhabitedType
88
from mypy.types import Type as MypyType
99

1010
from mypy_django_plugin.django.context import DjangoContext
@@ -198,40 +198,5 @@ def refine_many_to_many_related_manager(ctx: MethodContext) -> MypyType:
198198
checker, to=related_model_instance.type, derived_from="_default_manager"
199199
)
200200
if related_manager_info is None:
201-
default_manager_node = related_model_instance.type.names.get("_default_manager")
202-
if default_manager_node is None or not isinstance(default_manager_node.type, Instance):
203-
return ctx.default_return_type
204-
205-
# Create a reusable generic subclass that is generic over a 'through' model,
206-
# explicitly declared it'd could have looked something like below
207-
#
208-
# class X(models.Model): ...
209-
# _Through = TypeVar("_Through", bound=models.Model)
210-
# class X_ManyRelatedManager(ManyRelatedManager[X, _Through], type(X._default_manager), Generic[_Through]): ...
211-
_through_type_var = many_related_manager.type.defn.type_vars[1]
212-
assert isinstance(_through_type_var, TypeVarType)
213-
generic_to_many_related_manager = many_related_manager.copy_modified(
214-
args=[
215-
# Keep the same '_To' as the (parent) `ManyRelatedManager` instance
216-
many_related_manager.args[0],
217-
# But reset the '_Through' `TypeVar` declared for `ManyRelatedManager`
218-
_through_type_var.copy_modified(),
219-
]
220-
)
221-
related_manager_info = helpers.add_new_class_for_module(
222-
module=checker.modules[related_model_instance.type.module_name],
223-
name=f"{related_model_instance.type.name}_ManyRelatedManager",
224-
bases=[generic_to_many_related_manager, default_manager_node.type],
225-
)
226-
# Reuse the '_Through' `TypeVar` from `ManyRelatedManager` in our subclass
227-
related_manager_info.defn.type_vars = [_through_type_var.copy_modified()]
228-
related_manager_info.add_type_vars()
229-
related_manager_info.metadata["django"] = {"related_manager_to_model": related_model_instance.type.fullname}
230-
# Track the existence of our manager subclass, by tying it to model it operates on
231-
helpers.set_many_to_many_manager_info(
232-
to=related_model_instance.type,
233-
derived_from="_default_manager",
234-
manager_info=related_manager_info,
235-
)
236-
201+
return ctx.default_return_type
237202
return Instance(related_manager_info, [through_model_instance])

mypy_django_plugin/transformers/models.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from mypy.plugins import common
2828
from mypy.semanal import SemanticAnalyzer
2929
from mypy.typeanal import TypeAnalyser
30-
from mypy.types import AnyType, Instance, ProperType, TypedDictType, TypeOfAny, TypeType, get_proper_type
30+
from mypy.types import AnyType, Instance, ProperType, TypedDictType, TypeOfAny, TypeType, TypeVarType, get_proper_type
3131
from mypy.types import Type as MypyType
3232
from mypy.typevars import fill_typevars
3333

@@ -608,9 +608,15 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
608608

609609
class ProcessManyToManyFields(ModelClassInitializer):
610610
"""
611-
Processes 'ManyToManyField()' fields and generates any implicit through tables that
612-
Django also generates. It won't do anything if the model is abstract or for fields
613-
where an explicit 'through' argument has been passed.
611+
Processes 'ManyToManyField()' fields and;
612+
613+
- Generates any implicit through tables that Django also generates. It won't do
614+
anything if the model is abstract or for fields where an explicit 'through'
615+
argument has been passed.
616+
- Creates related managers for both ends of the many to many relationship
617+
618+
TODO: Move the 'related_name' contribution from 'AddReverseLookups' to here. As it
619+
makes sense to add it when processing ManyToManyField
614620
"""
615621

616622
def statements(self) -> Iterable[Statement]:
@@ -675,6 +681,11 @@ def run(self) -> None:
675681
model_fullname=f"{self.model_classdef.info.module_name}.{through_model_name}",
676682
m2m_args=args,
677683
)
684+
# Create a 'ManyRelatedManager' class for the processed model
685+
self.create_many_related_manager(Instance(self.model_classdef.info, []))
686+
if isinstance(args.to.model, Instance):
687+
# Create a 'ManyRelatedManager' class for the related model
688+
self.create_many_related_manager(args.to.model)
678689

679690
@cached_property
680691
def default_pk_instance(self) -> Instance:
@@ -715,6 +726,10 @@ def manager_info(self) -> TypeInfo:
715726
def fk_field_types(self) -> FieldDescriptorTypes:
716727
return get_field_descriptor_types(self.fk_field, is_set_nullable=False, is_get_nullable=False)
717728

729+
@cached_property
730+
def many_related_manager(self) -> TypeInfo:
731+
return self.lookup_typeinfo_or_incomplete_defn_error(fullnames.MANY_RELATED_MANAGER)
732+
718733
def get_pk_instance(self, model: TypeInfo, /) -> Instance:
719734
"""
720735
Get a primary key instance of provided model's type info. If primary key can't be resolved,
@@ -852,6 +867,47 @@ def resolve_many_to_many_arguments(self, call: CallExpr, /, context: Context) ->
852867

853868
return M2MArguments(to=to, through=through)
854869

870+
def create_many_related_manager(self, model: Instance) -> None:
871+
"""
872+
Creates a generic manager that subclasses both 'ManyRelatedManager' and the
873+
default manager of the given model. These are normally used on both models
874+
involved in a ManyToManyField.
875+
876+
The manager classes are generic over a '_Through' model, meaning that they can
877+
be reused for multiple many to many relations.
878+
"""
879+
if helpers.get_many_to_many_manager_info(self.api, to=model.type, derived_from="_default_manager") is not None:
880+
return
881+
882+
default_manager_node = model.type.names.get("_default_manager")
883+
if default_manager_node is None:
884+
raise helpers.IncompleteDefnException()
885+
elif not isinstance(default_manager_node.type, Instance):
886+
return
887+
888+
# Create a reusable generic subclass that is generic over a 'through' model,
889+
# explicitly declared it'd could have looked something like below
890+
#
891+
# class X(models.Model): ...
892+
# _Through = TypeVar("_Through", bound=models.Model)
893+
# class X_ManyRelatedManager(ManyRelatedManager[X, _Through], type(X._default_manager), Generic[_Through]): ...
894+
through_type_var = self.many_related_manager.defn.type_vars[1]
895+
assert isinstance(through_type_var, TypeVarType)
896+
generic_to_many_related_manager = Instance(self.many_related_manager, [model, through_type_var.copy_modified()])
897+
related_manager_info = helpers.add_new_class_for_module(
898+
module=self.api.modules[model.type.module_name],
899+
name=f"{model.type.name}_ManyRelatedManager",
900+
bases=[generic_to_many_related_manager, default_manager_node.type],
901+
)
902+
# Reuse the '_Through' `TypeVar` from `ManyRelatedManager` in our subclass
903+
related_manager_info.defn.type_vars = [through_type_var.copy_modified()]
904+
related_manager_info.add_type_vars()
905+
# Track the existence of our manager subclass, by tying it to the model it
906+
# operates on
907+
helpers.set_many_to_many_manager_info(
908+
to=model.type, derived_from="_default_manager", manager_info=related_manager_info
909+
)
910+
855911

856912
class MetaclassAdjustments(ModelClassInitializer):
857913
@classmethod

scripts/stubtest/allowlist.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ django.core.files.storage.default_storage
2727
django.contrib.admin.models.LogEntry_RelatedManager
2828
django.contrib.auth.models.Permission_RelatedManager
2929

30+
# '<Model>_ManyRelatedManager' entries are plugin generated and these subclasses only exist
31+
# _locally/dynamically_ runtime -- Created via
32+
# 'django.db.models.fields.related_descriptors.create_forward_many_to_many_manager'
33+
django.contrib.auth.models.Group_ManyRelatedManager
34+
django.contrib.auth.models.Permission_ManyRelatedManager
35+
django.contrib.auth.models.User_ManyRelatedManager
36+
3037
# BaseArchive abstract methods that take no argument, but typed with arguments to match the Archive and TarArchive Implementations
3138
django.utils.archive.BaseArchive.list
3239
django.utils.archive.BaseArchive.extract

0 commit comments

Comments
 (0)