36
36
from mypy_django_plugin .exceptions import UnregisteredModelError
37
37
from mypy_django_plugin .lib import fullnames , helpers
38
38
from mypy_django_plugin .lib .fullnames import ANNOTATIONS_FULLNAME , ANY_ATTR_ALLOWED_CLASS_FULLNAME , MODEL_CLASS_FULLNAME
39
- from mypy_django_plugin .transformers .fields import get_field_descriptor_types
39
+ from mypy_django_plugin .transformers .fields import FieldDescriptorTypes , get_field_descriptor_types
40
40
from mypy_django_plugin .transformers .managers import (
41
41
MANAGER_METHODS_RETURNING_QUERYSET ,
42
42
create_manager_info_from_from_queryset_call ,
@@ -644,17 +644,6 @@ def run(self) -> None:
644
644
# TODO: Create abstract through models?
645
645
return
646
646
647
- # Start out by prefetching a couple of dependencies needed to be able to declare any
648
- # new, implicit, through model class.
649
- model_base = self .lookup_typeinfo (fullnames .MODEL_CLASS_FULLNAME )
650
- fk_field = self .lookup_typeinfo (fullnames .FOREIGN_KEY_FULLNAME )
651
- manager_info = self .lookup_typeinfo (fullnames .MANAGER_CLASS_FULLNAME )
652
- if model_base is None or fk_field is None or manager_info is None :
653
- raise helpers .IncompleteDefnException ()
654
-
655
- from_pk = self .get_pk_instance (self .model_classdef .info )
656
- fk_set_type , fk_get_type = get_field_descriptor_types (fk_field , is_set_nullable = False , is_get_nullable = False )
657
-
658
647
for statement in self .statements ():
659
648
# Check if this part of the class body is an assignment from a 'ManyToManyField' call
660
649
# <field> = ManyToManyField(...)
@@ -675,90 +664,16 @@ def run(self) -> None:
675
664
continue
676
665
# Resolve argument information of the 'ManyToManyField(...)' call
677
666
args = self .resolve_many_to_many_arguments (statement .rvalue , context = statement )
678
- if (
679
- # Ignore calls without required 'to' argument, mypy will complain
680
- args is None
681
- or not isinstance (args .to .model , Instance )
682
- # Call has explicit 'through=', no need to create any implicit through table
683
- or args .through is not None
684
- ):
667
+ # Ignore calls without required 'to' argument, mypy will complain
668
+ if args is None :
685
669
continue
686
-
687
670
# Get the names of the implicit through model that will be generated
688
671
through_model_name = f"{ self .model_classdef .name } _{ m2m_field_name } "
689
- through_model_fullname = f"{ self .model_classdef .info .module_name } .{ through_model_name } "
690
- # If implicit through model is already declared there's nothing more we should do
691
- through_model = self .lookup_typeinfo (through_model_fullname )
692
- if through_model is not None :
693
- continue
694
- # Declare a new, empty, implicitly generated through model class named: '<Model>_<field_name>'
695
- through_model = self .add_new_class_for_current_module (
696
- through_model_name , bases = [Instance (model_base , [])]
697
- )
698
- # We attempt to be a bit clever here and store the generated through model's fullname in
699
- # the metadata of the class containing the 'ManyToManyField' call expression, where its
700
- # identifier is the field name of the 'ManyToManyField'. This would allow the containing
701
- # model to always find the implicit through model, so that it doesn't get lost.
702
- model_metadata = helpers .get_django_metadata (self .model_classdef .info )
703
- model_metadata .setdefault ("m2m_throughs" , {})
704
- model_metadata ["m2m_throughs" ][m2m_field_name ] = through_model .fullname
705
- # Add a 'pk' symbol to the model class
706
- helpers .add_new_sym_for_info (
707
- through_model , name = "pk" , sym_type = self .default_pk_instance .copy_modified ()
708
- )
709
- # Add an 'id' symbol to the model class
710
- helpers .add_new_sym_for_info (
711
- through_model , name = "id" , sym_type = self .default_pk_instance .copy_modified ()
712
- )
713
- # Add the foreign key to the model containing the 'ManyToManyField' call:
714
- # <containing_model> or from_<model>
715
- from_name = (
716
- f"from_{ self .model_classdef .name .lower ()} " if args .to .self else self .model_classdef .name .lower ()
717
- )
718
- helpers .add_new_sym_for_info (
719
- through_model ,
720
- name = from_name ,
721
- sym_type = Instance (
722
- fk_field ,
723
- [
724
- helpers .convert_any_to_type (fk_set_type , Instance (self .model_classdef .info , [])),
725
- helpers .convert_any_to_type (fk_get_type , Instance (self .model_classdef .info , [])),
726
- ],
727
- ),
728
- )
729
- # Add the foreign key's '_id' field: <containing_model>_id or from_<model>_id
730
- helpers .add_new_sym_for_info (through_model , name = f"{ from_name } _id" , sym_type = from_pk .copy_modified ())
731
- # Add the foreign key to the model on the opposite side of the relation
732
- # i.e. the model given as 'to' argument to the 'ManyToManyField' call:
733
- # <other_model> or to_<model>
734
- to_name = f"to_{ args .to .model .type .name .lower ()} " if args .to .self else args .to .model .type .name .lower ()
735
- helpers .add_new_sym_for_info (
736
- through_model ,
737
- name = to_name ,
738
- sym_type = Instance (
739
- fk_field ,
740
- [
741
- helpers .convert_any_to_type (fk_set_type , args .to .model ),
742
- helpers .convert_any_to_type (fk_get_type , args .to .model ),
743
- ],
744
- ),
745
- )
746
- # Add the foreign key's '_id' field: <other_model>_id or to_<model>_id
747
- other_pk = self .get_pk_instance (args .to .model .type )
748
- helpers .add_new_sym_for_info (through_model , name = f"{ to_name } _id" , sym_type = other_pk .copy_modified ())
749
- # Add a manager named 'objects'
750
- helpers .add_new_sym_for_info (
751
- through_model ,
752
- name = "objects" ,
753
- sym_type = Instance (manager_info , [Instance (through_model , [])]),
754
- is_classvar = True ,
755
- )
756
- # Also add manager as '_default_manager' attribute
757
- helpers .add_new_sym_for_info (
758
- through_model ,
759
- name = "_default_manager" ,
760
- sym_type = Instance (manager_info , [Instance (through_model , [])]),
761
- is_classvar = True ,
672
+ self .create_through_table_class (
673
+ field_name = m2m_field_name ,
674
+ model_name = through_model_name ,
675
+ model_fullname = f"{ self .model_classdef .info .module_name } .{ through_model_name } " ,
676
+ m2m_args = args ,
762
677
)
763
678
764
679
@cached_property
@@ -771,6 +686,35 @@ def default_pk_instance(self) -> Instance:
771
686
list (get_field_descriptor_types (default_pk_field , is_set_nullable = True , is_get_nullable = False )),
772
687
)
773
688
689
+ @cached_property
690
+ def model_pk_instance (self ) -> Instance :
691
+ return self .get_pk_instance (self .model_classdef .info )
692
+
693
+ @cached_property
694
+ def model_base (self ) -> TypeInfo :
695
+ info = self .lookup_typeinfo (fullnames .MODEL_CLASS_FULLNAME )
696
+ if info is None :
697
+ raise helpers .IncompleteDefnException ()
698
+ return info
699
+
700
+ @cached_property
701
+ def fk_field (self ) -> TypeInfo :
702
+ info = self .lookup_typeinfo (fullnames .FOREIGN_KEY_FULLNAME )
703
+ if info is None :
704
+ raise helpers .IncompleteDefnException ()
705
+ return info
706
+
707
+ @cached_property
708
+ def manager_info (self ) -> TypeInfo :
709
+ info = self .lookup_typeinfo (fullnames .MANAGER_CLASS_FULLNAME )
710
+ if info is None :
711
+ raise helpers .IncompleteDefnException ()
712
+ return info
713
+
714
+ @cached_property
715
+ def fk_field_types (self ) -> FieldDescriptorTypes :
716
+ return get_field_descriptor_types (self .fk_field , is_set_nullable = False , is_get_nullable = False )
717
+
774
718
def get_pk_instance (self , model : TypeInfo , / ) -> Instance :
775
719
"""
776
720
Get a primary key instance of provided model's type info. If primary key can't be resolved,
@@ -783,6 +727,86 @@ def get_pk_instance(self, model: TypeInfo, /) -> Instance:
783
727
return pk .type
784
728
return self .default_pk_instance
785
729
730
+ def create_through_table_class (
731
+ self , field_name : str , model_name : str , model_fullname : str , m2m_args : M2MArguments
732
+ ) -> None :
733
+ if (
734
+ not isinstance (m2m_args .to .model , Instance )
735
+ # Call has explicit 'through=', no need to create any implicit through table
736
+ or m2m_args .through is not None
737
+ ):
738
+ return
739
+
740
+ # If through model is already declared there's nothing more we should do
741
+ through_model = self .lookup_typeinfo (model_fullname )
742
+ if through_model is not None :
743
+ return
744
+ # Declare a new, empty, implicitly generated through model class named: '<Model>_<field_name>'
745
+ through_model = self .add_new_class_for_current_module (model_name , bases = [Instance (self .model_base , [])])
746
+ # We attempt to be a bit clever here and store the generated through model's fullname in
747
+ # the metadata of the class containing the 'ManyToManyField' call expression, where its
748
+ # identifier is the field name of the 'ManyToManyField'. This would allow the containing
749
+ # model to always find the implicit through model, so that it doesn't get lost.
750
+ model_metadata = helpers .get_django_metadata (self .model_classdef .info )
751
+ model_metadata .setdefault ("m2m_throughs" , {})
752
+ model_metadata ["m2m_throughs" ][field_name ] = through_model .fullname
753
+ # Add a 'pk' symbol to the model class
754
+ helpers .add_new_sym_for_info (through_model , name = "pk" , sym_type = self .default_pk_instance .copy_modified ())
755
+ # Add an 'id' symbol to the model class
756
+ helpers .add_new_sym_for_info (through_model , name = "id" , sym_type = self .default_pk_instance .copy_modified ())
757
+ # Add the foreign key to the model containing the 'ManyToManyField' call:
758
+ # <containing_model> or from_<model>
759
+ from_name = f"from_{ self .model_classdef .name .lower ()} " if m2m_args .to .self else self .model_classdef .name .lower ()
760
+ helpers .add_new_sym_for_info (
761
+ through_model ,
762
+ name = from_name ,
763
+ sym_type = Instance (
764
+ self .fk_field ,
765
+ [
766
+ helpers .convert_any_to_type (self .fk_field_types .set , Instance (self .model_classdef .info , [])),
767
+ helpers .convert_any_to_type (self .fk_field_types .get , Instance (self .model_classdef .info , [])),
768
+ ],
769
+ ),
770
+ )
771
+ # Add the foreign key's '_id' field: <containing_model>_id or from_<model>_id
772
+ helpers .add_new_sym_for_info (
773
+ through_model , name = f"{ from_name } _id" , sym_type = self .model_pk_instance .copy_modified ()
774
+ )
775
+ # Add the foreign key to the model on the opposite side of the relation
776
+ # i.e. the model given as 'to' argument to the 'ManyToManyField' call:
777
+ # <other_model> or to_<model>
778
+ to_name = (
779
+ f"to_{ m2m_args .to .model .type .name .lower ()} " if m2m_args .to .self else m2m_args .to .model .type .name .lower ()
780
+ )
781
+ helpers .add_new_sym_for_info (
782
+ through_model ,
783
+ name = to_name ,
784
+ sym_type = Instance (
785
+ self .fk_field ,
786
+ [
787
+ helpers .convert_any_to_type (self .fk_field_types .set , m2m_args .to .model ),
788
+ helpers .convert_any_to_type (self .fk_field_types .get , m2m_args .to .model ),
789
+ ],
790
+ ),
791
+ )
792
+ # Add the foreign key's '_id' field: <other_model>_id or to_<model>_id
793
+ other_pk = self .get_pk_instance (m2m_args .to .model .type )
794
+ helpers .add_new_sym_for_info (through_model , name = f"{ to_name } _id" , sym_type = other_pk .copy_modified ())
795
+ # Add a manager named 'objects'
796
+ helpers .add_new_sym_for_info (
797
+ through_model ,
798
+ name = "objects" ,
799
+ sym_type = Instance (self .manager_info , [Instance (through_model , [])]),
800
+ is_classvar = True ,
801
+ )
802
+ # Also add manager as '_default_manager' attribute
803
+ helpers .add_new_sym_for_info (
804
+ through_model ,
805
+ name = "_default_manager" ,
806
+ sym_type = Instance (self .manager_info , [Instance (through_model , [])]),
807
+ is_classvar = True ,
808
+ )
809
+
786
810
def resolve_many_to_many_arguments (self , call : CallExpr , / , context : Context ) -> Optional [M2MArguments ]:
787
811
"""
788
812
Inspect a 'ManyToManyField(...)' call to collect argument data on any 'to' and
0 commit comments