22
22
TypeInfo ,
23
23
Var ,
24
24
)
25
- from mypy .plugin import AnalyzeTypeContext , AttributeContext , CheckerPluginInterface , ClassDefContext
25
+ from mypy .plugin import AnalyzeTypeContext , AttributeContext , ClassDefContext
26
26
from mypy .plugins import common
27
27
from mypy .semanal import SemanticAnalyzer
28
28
from 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
+ )
30
40
from mypy .types import Type as MypyType
31
- from mypy .typevars import fill_typevars
41
+ from mypy .typevars import fill_typevars , fill_typevars_with_any
32
42
33
43
from mypy_django_plugin .django .context import DjangoContext
34
44
from mypy_django_plugin .errorcodes import MANAGER_MISSING
35
45
from mypy_django_plugin .exceptions import UnregisteredModelError
36
46
from 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
38
48
from mypy_django_plugin .transformers .fields import FieldDescriptorTypes , get_field_descriptor_types
39
49
from mypy_django_plugin .transformers .managers import (
40
50
MANAGER_METHODS_RETURNING_QUERYSET ,
@@ -200,6 +210,50 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
200
210
raise NotImplementedError (f"Implement this in subclass { self .__class__ .__name__ } " )
201
211
202
212
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
+
203
257
class InjectAnyAsBaseForNestedMeta (ModelClassInitializer ):
204
258
"""
205
259
Replaces
@@ -1034,6 +1088,7 @@ def run(self) -> None:
1034
1088
1035
1089
def process_model_class (ctx : ClassDefContext , django_context : DjangoContext ) -> None :
1036
1090
initializers = [
1091
+ AddAnnotateUtilities ,
1037
1092
InjectAnyAsBaseForNestedMeta ,
1038
1093
AddDefaultPrimaryKey ,
1039
1094
AddPrimaryKeyAlias ,
@@ -1059,77 +1114,72 @@ def set_auth_user_model_boolean_fields(ctx: AttributeContext, django_context: Dj
1059
1114
return Instance (boolinfo , [])
1060
1115
1061
1116
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
1063
1123
args = ctx .type .args
1124
+ if not args :
1125
+ return AnyType (TypeOfAny .from_omitted_generics ) if is_with_annotations else ctx .type
1064
1126
type_arg = ctx .api .analyze_type (args [0 ])
1065
1127
if not isinstance (type_arg , Instance ) or not type_arg .type .has_base (MODEL_CLASS_FULLNAME ):
1066
1128
return type_arg
1067
1129
1068
1130
fields_dict = None
1069
1131
if len (args ) > 1 :
1070
1132
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 :
1072
1134
fields_dict = second_arg_type
1073
1135
elif isinstance (second_arg_type , Instance ) and second_arg_type .type .fullname == ANNOTATIONS_FULLNAME :
1074
1136
annotations_type_arg = get_proper_type (second_arg_type .args [0 ])
1075
1137
if isinstance (annotations_type_arg , TypedDictType ):
1076
1138
fields_dict = annotations_type_arg
1077
1139
elif not isinstance (annotations_type_arg , AnyType ):
1078
1140
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
1079
1146
1080
1147
assert isinstance (ctx .api , TypeAnalyser )
1081
1148
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 )
1083
1150
1084
1151
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
1087
1154
) -> ProperType :
1088
1155
"""
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
1096
1157
"""
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
+ )
1105
1164
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 ,
1128
1169
)
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