2222 TypeInfo ,
2323 Var ,
2424)
25- from mypy .plugin import AnalyzeTypeContext , AttributeContext , CheckerPluginInterface , ClassDefContext
25+ from mypy .plugin import AnalyzeTypeContext , AttributeContext , ClassDefContext
2626from mypy .plugins import common
2727from mypy .semanal import SemanticAnalyzer
2828from mypy .typeanal import TypeAnalyser
29- from mypy .types import AnyType , Instance , ProperType , TypedDictType , TypeOfAny , TypeType , TypeVarType , get_proper_type
29+ from mypy .types import (
30+ AnyType ,
31+ ExtraAttrs ,
32+ Instance ,
33+ ProperType ,
34+ TypedDictType ,
35+ TypeOfAny ,
36+ TypeType ,
37+ TypeVarType ,
38+ get_proper_type ,
39+ )
3040from mypy .types import Type as MypyType
31- from mypy .typevars import fill_typevars
41+ from mypy .typevars import fill_typevars , fill_typevars_with_any
3242
3343from mypy_django_plugin .django .context import DjangoContext
3444from mypy_django_plugin .errorcodes import MANAGER_MISSING
3545from mypy_django_plugin .exceptions import UnregisteredModelError
3646from mypy_django_plugin .lib import fullnames , helpers
37- from mypy_django_plugin .lib .fullnames import ANNOTATIONS_FULLNAME , ANY_ATTR_ALLOWED_CLASS_FULLNAME , MODEL_CLASS_FULLNAME
47+ from mypy_django_plugin .lib .fullnames import ANNOTATIONS_FULLNAME , MODEL_CLASS_FULLNAME
3848from mypy_django_plugin .transformers .fields import FieldDescriptorTypes , get_field_descriptor_types
3949from mypy_django_plugin .transformers .managers import (
4050 MANAGER_METHODS_RETURNING_QUERYSET ,
@@ -200,6 +210,50 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
200210 raise NotImplementedError (f"Implement this in subclass { self .__class__ .__name__ } " )
201211
202212
213+ class AddAnnotateUtilities (ModelClassInitializer ):
214+ """
215+ Creates a model subclass that will be used when the model's manager/queryset calls
216+ 'annotate' to hold on to attributes that Django adds to a model instance.
217+
218+ Example:
219+
220+ class MyModel(models.Model):
221+ ...
222+
223+ class MyModel@AnnotatedWith(MyModel, django_stubs_ext.Annotations[_Annotations]):
224+ ...
225+ """
226+
227+ def run (self ) -> None :
228+ annotations = self .lookup_typeinfo_or_incomplete_defn_error ("django_stubs_ext.Annotations" )
229+ object_does_not_exist = self .lookup_typeinfo_or_incomplete_defn_error (fullnames .OBJECT_DOES_NOT_EXIST )
230+ multiple_objects_returned = self .lookup_typeinfo_or_incomplete_defn_error (fullnames .MULTIPLE_OBJECTS_RETURNED )
231+ annotated_model_name = self .model_classdef .info .name + "@AnnotatedWith"
232+ annotated_model = self .lookup_typeinfo (self .model_classdef .info .module_name + "." + annotated_model_name )
233+ if annotated_model is None :
234+ model_type = fill_typevars_with_any (self .model_classdef .info )
235+ assert isinstance (model_type , Instance )
236+ annotations_type = fill_typevars (annotations )
237+ assert isinstance (annotations_type , Instance )
238+ annotated_model = self .add_new_class_for_current_module (
239+ annotated_model_name , bases = [model_type , annotations_type ]
240+ )
241+ annotated_model .defn .type_vars = annotations .defn .type_vars
242+ annotated_model .add_type_vars ()
243+ helpers .mark_as_annotated_model (annotated_model )
244+ if self .is_model_abstract :
245+ # Below are abstract attributes, and in a stub file mypy requires
246+ # explicit ABCMeta if not all abstract attributes are implemented i.e.
247+ # class is kept abstract. So we add the attributes to get mypy off our
248+ # back
249+ helpers .add_new_sym_for_info (
250+ annotated_model , "DoesNotExist" , TypeType (Instance (object_does_not_exist , []))
251+ )
252+ helpers .add_new_sym_for_info (
253+ annotated_model , "MultipleObjectsReturned" , TypeType (Instance (multiple_objects_returned , []))
254+ )
255+
256+
203257class InjectAnyAsBaseForNestedMeta (ModelClassInitializer ):
204258 """
205259 Replaces
@@ -1034,6 +1088,7 @@ def run(self) -> None:
10341088
10351089def process_model_class (ctx : ClassDefContext , django_context : DjangoContext ) -> None :
10361090 initializers = [
1091+ AddAnnotateUtilities ,
10371092 InjectAnyAsBaseForNestedMeta ,
10381093 AddDefaultPrimaryKey ,
10391094 AddPrimaryKeyAlias ,
@@ -1059,77 +1114,72 @@ def set_auth_user_model_boolean_fields(ctx: AttributeContext, django_context: Dj
10591114 return Instance (boolinfo , [])
10601115
10611116
1062- def handle_annotated_type (ctx : AnalyzeTypeContext , django_context : DjangoContext ) -> MypyType :
1117+ def handle_annotated_type (ctx : AnalyzeTypeContext , fullname : str ) -> MypyType :
1118+ """
1119+ Replaces the 'WithAnnotations' type with a type that can represent an annotated
1120+ model.
1121+ """
1122+ is_with_annotations = fullname == fullnames .WITH_ANNOTATIONS_FULLNAME
10631123 args = ctx .type .args
1124+ if not args :
1125+ return AnyType (TypeOfAny .from_omitted_generics ) if is_with_annotations else ctx .type
10641126 type_arg = ctx .api .analyze_type (args [0 ])
10651127 if not isinstance (type_arg , Instance ) or not type_arg .type .has_base (MODEL_CLASS_FULLNAME ):
10661128 return type_arg
10671129
10681130 fields_dict = None
10691131 if len (args ) > 1 :
10701132 second_arg_type = get_proper_type (ctx .api .analyze_type (args [1 ]))
1071- if isinstance (second_arg_type , TypedDictType ):
1133+ if isinstance (second_arg_type , TypedDictType ) and is_with_annotations :
10721134 fields_dict = second_arg_type
10731135 elif isinstance (second_arg_type , Instance ) and second_arg_type .type .fullname == ANNOTATIONS_FULLNAME :
10741136 annotations_type_arg = get_proper_type (second_arg_type .args [0 ])
10751137 if isinstance (annotations_type_arg , TypedDictType ):
10761138 fields_dict = annotations_type_arg
10771139 elif not isinstance (annotations_type_arg , AnyType ):
10781140 ctx .api .fail ("Only TypedDicts are supported as type arguments to Annotations" , ctx .context )
1141+ elif annotations_type_arg .type_of_any == TypeOfAny .from_omitted_generics :
1142+ ctx .api .fail ("Missing required TypedDict parameter for generic type Annotations" , ctx .context )
1143+
1144+ if fields_dict is None :
1145+ return type_arg
10791146
10801147 assert isinstance (ctx .api , TypeAnalyser )
10811148 assert isinstance (ctx .api .api , SemanticAnalyzer )
1082- return get_or_create_annotated_type (ctx .api .api , type_arg , fields_dict = fields_dict )
1149+ return get_annotated_type (ctx .api .api , type_arg , fields_dict = fields_dict )
10831150
10841151
1085- def get_or_create_annotated_type (
1086- api : Union [SemanticAnalyzer , CheckerPluginInterface ], model_type : Instance , fields_dict : Optional [ TypedDictType ]
1152+ def get_annotated_type (
1153+ api : Union [SemanticAnalyzer , TypeChecker ], model_type : Instance , fields_dict : TypedDictType
10871154) -> ProperType :
10881155 """
1089-
1090- Get or create the type for a model for which you getting/setting any attr is allowed.
1091-
1092- The generated type is an subclass of the model and django._AnyAttrAllowed.
1093- The generated type is placed in the django_stubs_ext module, with the name WithAnnotations[ModelName].
1094- If the user wanted to annotate their code using this type, then this is the annotation they would use.
1095- This is a bit of a hack to make a pretty type for error messages and which would make sense for users.
1156+ Get a model type that can be used to represent an annotated model
10961157 """
1097- model_module_name = "django_stubs_ext"
1098-
1099- if helpers .is_annotated_model_fullname (model_type .type .fullname ):
1100- # If it's already a generated class, we want to use the original model as a base
1101- model_type = model_type .type .bases [0 ]
1102-
1103- if fields_dict is not None :
1104- type_name = f"WithAnnotations[{ model_type .type .fullname .replace ('.' , '__' )} , { fields_dict } ]"
1158+ if model_type .extra_attrs :
1159+ extra_attrs = ExtraAttrs (
1160+ attrs = {** model_type .extra_attrs .attrs , ** (fields_dict .items if fields_dict is not None else {})},
1161+ immutable = model_type .extra_attrs .immutable .copy (),
1162+ mod_name = None ,
1163+ )
11051164 else :
1106- type_name = f"WithAnnotations[{ model_type .type .fullname .replace ('.' , '__' )} ]"
1107-
1108- annotated_typeinfo = helpers .lookup_fully_qualified_typeinfo (
1109- cast (TypeChecker , api ), model_module_name + "." + type_name
1110- )
1111- if annotated_typeinfo is None :
1112- model_module_file = api .modules .get (model_module_name ) # type: ignore[union-attr]
1113- if model_module_file is None :
1114- return AnyType (TypeOfAny .from_error )
1115-
1116- if isinstance (api , SemanticAnalyzer ):
1117- annotated_model_type = api .named_type_or_none (ANY_ATTR_ALLOWED_CLASS_FULLNAME , [])
1118- assert annotated_model_type is not None
1119- else :
1120- annotated_model_type = api .named_generic_type (ANY_ATTR_ALLOWED_CLASS_FULLNAME , [])
1121-
1122- annotated_typeinfo = helpers .add_new_class_for_module (
1123- model_module_file ,
1124- type_name ,
1125- bases = [model_type ] if fields_dict is not None else [model_type , annotated_model_type ],
1126- fields = fields_dict .items if fields_dict is not None else None ,
1127- no_serialize = True ,
1165+ extra_attrs = ExtraAttrs (
1166+ attrs = fields_dict .items if fields_dict is not None else {},
1167+ immutable = None ,
1168+ mod_name = None ,
11281169 )
1129- if fields_dict is not None :
1130- # To allow structural subtyping, make it a Protocol
1131- annotated_typeinfo .is_protocol = True
1132- # Save for later to easily find which field types were annotated
1133- annotated_typeinfo .metadata ["annotated_field_types" ] = fields_dict .items
1134- annotated_type = Instance (annotated_typeinfo , [])
1135- return annotated_type
1170+
1171+ annotated_model : Optional [TypeInfo ]
1172+ if helpers .is_annotated_model (model_type .type ):
1173+ annotated_model = model_type .type
1174+ if model_type .args and isinstance (model_type .args [0 ], TypedDictType ):
1175+ fields_dict = helpers .make_typeddict (
1176+ api ,
1177+ fields = {** model_type .args [0 ].items , ** fields_dict .items },
1178+ required_keys = {* model_type .args [0 ].required_keys , * fields_dict .required_keys },
1179+ )
1180+ else :
1181+ annotated_model = helpers .lookup_fully_qualified_typeinfo (api , model_type .type .fullname + "@AnnotatedWith" )
1182+
1183+ if annotated_model is None :
1184+ return model_type
1185+ return Instance (annotated_model , [fields_dict ], extra_attrs = extra_attrs )
0 commit comments