|
27 | 27 | from mypy.plugins import common |
28 | 28 | from mypy.semanal import SemanticAnalyzer |
29 | 29 | 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 |
31 | 31 | from mypy.types import Type as MypyType |
32 | 32 | from mypy.typevars import fill_typevars |
33 | 33 |
|
@@ -608,9 +608,15 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: |
608 | 608 |
|
609 | 609 | class ProcessManyToManyFields(ModelClassInitializer): |
610 | 610 | """ |
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 |
614 | 620 | """ |
615 | 621 |
|
616 | 622 | def statements(self) -> Iterable[Statement]: |
@@ -675,6 +681,11 @@ def run(self) -> None: |
675 | 681 | model_fullname=f"{self.model_classdef.info.module_name}.{through_model_name}", |
676 | 682 | m2m_args=args, |
677 | 683 | ) |
| 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) |
678 | 689 |
|
679 | 690 | @cached_property |
680 | 691 | def default_pk_instance(self) -> Instance: |
@@ -715,6 +726,10 @@ def manager_info(self) -> TypeInfo: |
715 | 726 | def fk_field_types(self) -> FieldDescriptorTypes: |
716 | 727 | return get_field_descriptor_types(self.fk_field, is_set_nullable=False, is_get_nullable=False) |
717 | 728 |
|
| 729 | + @cached_property |
| 730 | + def many_related_manager(self) -> TypeInfo: |
| 731 | + return self.lookup_typeinfo_or_incomplete_defn_error(fullnames.MANY_RELATED_MANAGER) |
| 732 | + |
718 | 733 | def get_pk_instance(self, model: TypeInfo, /) -> Instance: |
719 | 734 | """ |
720 | 735 | 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) -> |
852 | 867 |
|
853 | 868 | return M2MArguments(to=to, through=through) |
854 | 869 |
|
| 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 | + |
855 | 911 |
|
856 | 912 | class MetaclassAdjustments(ModelClassInitializer): |
857 | 913 | @classmethod |
|
0 commit comments