1
+ from functools import cached_property
1
2
from typing import Any , Dict , List , Optional , Type , Union , cast
2
3
3
4
from django .db .models import Manager , Model
4
5
from django .db .models .fields import DateField , DateTimeField , Field
5
6
from django .db .models .fields .reverse_related import ForeignObjectRel , OneToOneRel
6
7
from mypy .checker import TypeChecker
7
- from mypy .nodes import ARG_STAR2 , Argument , AssignmentStmt , CallExpr , Context , NameExpr , TypeInfo , Var
8
+ from mypy .nodes import (
9
+ ARG_STAR2 ,
10
+ MDEF ,
11
+ Argument ,
12
+ AssignmentStmt ,
13
+ CallExpr ,
14
+ Context ,
15
+ NameExpr ,
16
+ SymbolTableNode ,
17
+ TypeInfo ,
18
+ Var ,
19
+ )
8
20
from mypy .plugin import AnalyzeTypeContext , AttributeContext , CheckerPluginInterface , ClassDefContext
9
21
from mypy .plugins import common
10
22
from mypy .semanal import SemanticAnalyzer
11
- from mypy .types import AnyType , Instance , TypedDictType , TypeOfAny , get_proper_type
23
+ from mypy .types import AnyType , Instance , LiteralType , TypedDictType , TypeOfAny , TypeType , get_proper_type
12
24
from mypy .types import Type as MypyType
13
25
from mypy .typevars import fill_typevars
14
26
@@ -168,7 +180,7 @@ def get_or_create_queryset_with_any_fallback(self) -> TypeInfo:
168
180
return queryset_info
169
181
170
182
def run_with_model_cls (self , model_cls : Type [Model ]) -> None :
171
- raise NotImplementedError ("Implement this in subclasses " )
183
+ raise NotImplementedError (f "Implement this in subclass { self . __class__ . __name__ } " )
172
184
173
185
174
186
class InjectAnyAsBaseForNestedMeta (ModelClassInitializer ):
@@ -587,6 +599,128 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
587
599
self .add_new_node_to_model_class ("_meta" , Instance (options_info , [Instance (self .model_classdef .info , [])]))
588
600
589
601
602
+ class MetaclassAdjustments (ModelClassInitializer ):
603
+ @classmethod
604
+ def adjust_model_class (cls , ctx : ClassDefContext ) -> None :
605
+ """
606
+ For the sake of type checkers other than mypy, some attributes that are
607
+ dynamically added by Django's model metaclass has been annotated on
608
+ `django.db.models.base.Model`. We remove those attributes and will handle them
609
+ through the plugin.
610
+ """
611
+ if ctx .cls .fullname != fullnames .MODEL_CLASS_FULLNAME :
612
+ return
613
+
614
+ does_not_exist = ctx .cls .info .names .get ("DoesNotExist" )
615
+ if does_not_exist is not None and isinstance (does_not_exist .node , Var ) and not does_not_exist .plugin_generated :
616
+ del ctx .cls .info .names ["DoesNotExist" ]
617
+
618
+ multiple_objects_returned = ctx .cls .info .names .get ("MultipleObjectsReturned" )
619
+ if (
620
+ multiple_objects_returned is not None
621
+ and isinstance (multiple_objects_returned .node , Var )
622
+ and not multiple_objects_returned .plugin_generated
623
+ ):
624
+ del ctx .cls .info .names ["MultipleObjectsReturned" ]
625
+
626
+ return
627
+
628
+ def get_exception_bases (self , name : str ) -> List [Instance ]:
629
+ bases = []
630
+ for model_base in self .model_classdef .info .direct_base_classes ():
631
+ exception_base_sym = model_base .names .get (name )
632
+ if (
633
+ # Base class is a Model
634
+ model_base .metaclass_type is not None
635
+ and model_base .metaclass_type .type .fullname == fullnames .MODEL_METACLASS_FULLNAME
636
+ # But base class is not 'models.Model'
637
+ and model_base .fullname != fullnames .MODEL_CLASS_FULLNAME
638
+ # Base class also has a generated exception base e.g. 'DoesNotExist'
639
+ and exception_base_sym is not None
640
+ and exception_base_sym .plugin_generated
641
+ and isinstance (exception_base_sym .node , TypeInfo )
642
+ ):
643
+ bases .append (Instance (exception_base_sym .node , []))
644
+
645
+ return bases
646
+
647
+ @cached_property
648
+ def is_model_abstract (self ) -> bool :
649
+ meta = self .model_classdef .info .names .get ("Meta" )
650
+ # Check if 'abstract' is declared in this model's 'class Meta' as
651
+ # 'abstract = True' won't be inherited from a parent model.
652
+ if meta is not None and isinstance (meta .node , TypeInfo ) and "abstract" in meta .node .names :
653
+ for stmt in meta .node .defn .defs .body :
654
+ if (
655
+ # abstract =
656
+ isinstance (stmt , AssignmentStmt )
657
+ and len (stmt .lvalues ) == 1
658
+ and isinstance (stmt .lvalues [0 ], NameExpr )
659
+ and stmt .lvalues [0 ].name == "abstract"
660
+ ):
661
+ # abstract = True (builtins.bool)
662
+ rhs_is_true = (
663
+ isinstance (stmt .rvalue , NameExpr )
664
+ and stmt .rvalue .name == "True"
665
+ and isinstance (stmt .rvalue .node , Var )
666
+ and isinstance (stmt .rvalue .node .type , Instance )
667
+ and stmt .rvalue .node .type .type .fullname == "builtins.bool"
668
+ )
669
+ # abstract: Literal[True]
670
+ is_literal_true = isinstance (stmt .type , LiteralType ) and stmt .type .value is True
671
+ return rhs_is_true or is_literal_true
672
+ return False
673
+
674
+ def add_exception_classes (self ) -> None :
675
+ """
676
+ Adds exception classes 'DoesNotExist' and 'MultipleObjectsReturned' to a model
677
+ type, aligned with how the model metaclass does it runtime.
678
+
679
+ If the model is abstract, exceptions will be added as abstract attributes.
680
+ """
681
+ if "DoesNotExist" not in self .model_classdef .info .names :
682
+ object_does_not_exist = self .lookup_typeinfo_or_incomplete_defn_error (fullnames .OBJECT_DOES_NOT_EXIST )
683
+ does_not_exist : Union [Var , TypeInfo ]
684
+ if self .is_model_abstract :
685
+ does_not_exist = self .create_new_var ("DoesNotExist" , TypeType (Instance (object_does_not_exist , [])))
686
+ does_not_exist .is_abstract_var = True
687
+ else :
688
+ does_not_exist = helpers .create_type_info (
689
+ "DoesNotExist" ,
690
+ self .model_classdef .info .fullname ,
691
+ self .get_exception_bases ("DoesNotExist" ) or [Instance (object_does_not_exist , [])],
692
+ )
693
+ self .model_classdef .info .names [does_not_exist .name ] = SymbolTableNode (
694
+ MDEF , does_not_exist , plugin_generated = True
695
+ )
696
+
697
+ if "MultipleObjectsReturned" not in self .model_classdef .info .names :
698
+ django_multiple_objects_returned = self .lookup_typeinfo_or_incomplete_defn_error (
699
+ fullnames .MULTIPLE_OBJECTS_RETURNED
700
+ )
701
+ multiple_objects_returned : Union [Var , TypeInfo ]
702
+ if self .is_model_abstract :
703
+ multiple_objects_returned = self .create_new_var (
704
+ "MultipleObjectsReturned" , TypeType (Instance (django_multiple_objects_returned , []))
705
+ )
706
+ multiple_objects_returned .is_abstract_var = True
707
+ else :
708
+ multiple_objects_returned = helpers .create_type_info (
709
+ "MultipleObjectsReturned" ,
710
+ self .model_classdef .info .fullname ,
711
+ (
712
+ self .get_exception_bases ("MultipleObjectsReturned" )
713
+ or [Instance (django_multiple_objects_returned , [])]
714
+ ),
715
+ )
716
+ self .model_classdef .info .names [multiple_objects_returned .name ] = SymbolTableNode (
717
+ MDEF , multiple_objects_returned , plugin_generated = True
718
+ )
719
+
720
+ def run (self ) -> None :
721
+ self .add_exception_classes ()
722
+
723
+
590
724
def process_model_class (ctx : ClassDefContext , django_context : DjangoContext ) -> None :
591
725
initializers = [
592
726
InjectAnyAsBaseForNestedMeta ,
@@ -598,6 +732,7 @@ def process_model_class(ctx: ClassDefContext, django_context: DjangoContext) ->
598
732
AddRelatedManagers ,
599
733
AddExtraFieldMethods ,
600
734
AddMetaOptionsAttribute ,
735
+ MetaclassAdjustments ,
601
736
]
602
737
for initializer_cls in initializers :
603
738
try :
0 commit comments