8
8
FuncBase ,
9
9
FuncDef ,
10
10
MemberExpr ,
11
- NameExpr ,
12
11
OverloadedFuncDef ,
13
12
RefExpr ,
14
13
StrExpr ,
15
14
SymbolTableNode ,
16
15
TypeInfo ,
17
16
Var ,
18
17
)
19
- from mypy .plugin import AttributeContext , ClassDefContext , DynamicClassDefContext , MethodContext
18
+ from mypy .plugin import AttributeContext , DynamicClassDefContext , SemanticAnalyzerPluginInterface
20
19
from mypy .types import AnyType , CallableType , Instance , ProperType
21
20
from mypy .types import Type as MypyType
22
21
from mypy .types import TypeOfAny
23
22
from typing_extensions import Final
24
23
25
- from mypy_django_plugin import errorcodes
26
24
from mypy_django_plugin .lib import fullnames , helpers
27
25
28
26
MANAGER_METHODS_RETURNING_QUERYSET : Final = frozenset (
@@ -182,81 +180,110 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
182
180
"""
183
181
semanal_api = helpers .get_semanal_api (ctx )
184
182
183
+ # TODO: Emit an error when called in a class scope
184
+ if semanal_api .is_class_scope ():
185
+ return
186
+
185
187
# Don't redeclare the manager class if we've already defined it.
186
188
manager_node = semanal_api .lookup_current_scope (ctx .name )
187
189
if manager_node and isinstance (manager_node .node , TypeInfo ):
188
190
# This is just a deferral run where our work is already finished
189
191
return
190
192
191
- callee = ctx .call .callee
192
- assert isinstance (callee , MemberExpr )
193
- assert isinstance (callee .expr , RefExpr )
194
-
195
- base_manager_info = callee .expr .node
196
- if base_manager_info is None :
197
- if not semanal_api .final_iteration :
198
- semanal_api .defer ()
193
+ new_manager_info = create_manager_info_from_from_queryset_call (ctx .api , ctx .call , ctx .name )
194
+ if new_manager_info is None :
195
+ if not ctx .api .final_iteration :
196
+ ctx .api .defer ()
199
197
return
200
198
201
- assert isinstance (base_manager_info , TypeInfo )
199
+ # So that the plugin will reparameterize the manager when it is constructed inside of a Model definition
200
+ helpers .add_new_manager_base (semanal_api , new_manager_info .fullname )
202
201
203
- passed_queryset = ctx .call .args [0 ]
204
- assert isinstance (passed_queryset , NameExpr )
205
202
206
- derived_queryset_fullname = passed_queryset .fullname
207
- if derived_queryset_fullname is None :
208
- # In some cases, due to the way the semantic analyzer works, only passed_queryset.name is available.
209
- # But it should be analyzed again, so this isn't a problem.
210
- return
203
+ def create_manager_info_from_from_queryset_call (
204
+ api : SemanticAnalyzerPluginInterface , call_expr : CallExpr , name : Optional [str ] = None
205
+ ) -> Optional [TypeInfo ]:
206
+ """
207
+ Extract manager and queryset TypeInfo from a from_queryset call.
208
+ """
211
209
212
- base_manager_instance = fill_typevars (base_manager_info )
213
- assert isinstance (base_manager_instance , Instance )
214
- new_manager_info = semanal_api .basic_new_typeinfo (
215
- ctx .name , basetype_or_fallback = base_manager_instance , line = ctx .call .line
216
- )
210
+ if (
211
+ # Check that this is a from_queryset call on a manager subclass
212
+ not isinstance (call_expr .callee , MemberExpr )
213
+ or not isinstance (call_expr .callee .expr , RefExpr )
214
+ or not isinstance (call_expr .callee .expr .node , TypeInfo )
215
+ or not call_expr .callee .expr .node .has_base (fullnames .BASE_MANAGER_CLASS_FULLNAME )
216
+ or not call_expr .callee .name == "from_queryset"
217
+ # Check that the call has one or two arguments and that the first is a
218
+ # QuerySet subclass
219
+ or not 1 <= len (call_expr .args ) <= 2
220
+ or not isinstance (call_expr .args [0 ], RefExpr )
221
+ or not isinstance (call_expr .args [0 ].node , TypeInfo )
222
+ or not call_expr .args [0 ].node .has_base (fullnames .QUERYSET_CLASS_FULLNAME )
223
+ ):
224
+ return None
217
225
218
- sym = semanal_api .lookup_fully_qualified_or_none (derived_queryset_fullname )
219
- assert sym is not None
220
- if sym .node is None :
221
- if not semanal_api .final_iteration :
222
- semanal_api .defer ()
223
- else :
224
- # inherit from Any to prevent false-positives, if queryset class cannot be resolved
225
- new_manager_info .fallback_to_any = True
226
- return
226
+ base_manager_info , queryset_info = call_expr .callee .expr .node , call_expr .args [0 ].node
227
+ if queryset_info .fullname is None :
228
+ # In some cases, due to the way the semantic analyzer works, only
229
+ # passed_queryset.name is available. But it should be analyzed again,
230
+ # so this isn't a problem.
231
+ return None
227
232
228
- derived_queryset_info = sym .node
229
- assert isinstance (derived_queryset_info , TypeInfo )
230
-
231
- new_manager_info .line = ctx .call .line
232
- new_manager_info .type_vars = base_manager_info .type_vars
233
- new_manager_info .defn .type_vars = base_manager_info .defn .type_vars
234
- new_manager_info .defn .line = ctx .call .line
235
- new_manager_info .metaclass_type = new_manager_info .calculate_metaclass_type ()
236
- # Stash the queryset fullname which was passed to .from_queryset
237
- # So that our 'resolve_manager_method' attribute hook can fetch the method from that QuerySet class
238
- new_manager_info .metadata ["django" ] = {"from_queryset_manager" : derived_queryset_fullname }
239
-
240
- if len (ctx .call .args ) > 1 :
241
- expr = ctx .call .args [1 ]
242
- assert isinstance (expr , StrExpr )
243
- custom_manager_generated_name = expr .value
233
+ if len (call_expr .args ) == 2 and isinstance (call_expr .args [1 ], StrExpr ):
234
+ manager_name = call_expr .args [1 ].value
244
235
else :
245
- custom_manager_generated_name = base_manager_info .name + " From" + derived_queryset_info .name
236
+ manager_name = f" { base_manager_info .name } From{ queryset_info .name } "
246
237
247
- custom_manager_generated_fullname = "." .join (["django.db.models.manager" , custom_manager_generated_name ])
238
+ new_manager_info = create_manager_class (api , base_manager_info , name or manager_name , call_expr .line )
239
+
240
+ popuplate_manager_from_queryset (new_manager_info , queryset_info )
241
+
242
+ manager_fullname = "." .join (["django.db.models.manager" , manager_name ])
243
+
244
+ base_manager_info = new_manager_info .mro [1 ]
248
245
base_manager_info .metadata .setdefault ("from_queryset_managers" , {})
249
- base_manager_info .metadata ["from_queryset_managers" ][custom_manager_generated_fullname ] = new_manager_info .fullname
246
+ base_manager_info .metadata ["from_queryset_managers" ][manager_fullname ] = new_manager_info .fullname
247
+
248
+ # Add the new manager to the current module
249
+ module = api .modules [api .cur_mod_id ]
250
+ module .names [name or manager_name ] = SymbolTableNode (
251
+ GDEF , new_manager_info , plugin_generated = True , no_serialize = False
252
+ )
253
+
254
+ return new_manager_info
250
255
251
- # So that the plugin will reparameterize the manager when it is constructed inside of a Model definition
252
- helpers .add_new_manager_base (semanal_api , new_manager_info .fullname )
253
256
254
- class_def_context = ClassDefContext (cls = new_manager_info .defn , reason = ctx .call , api = semanal_api )
255
- self_type = fill_typevars (new_manager_info )
256
- assert isinstance (self_type , Instance )
257
+ def create_manager_class (
258
+ api : SemanticAnalyzerPluginInterface , base_manager_info : TypeInfo , name : str , line : int
259
+ ) -> TypeInfo :
260
+
261
+ base_manager_instance = fill_typevars (base_manager_info )
262
+ assert isinstance (base_manager_instance , Instance )
263
+
264
+ manager_info = helpers .create_type_info (name , api .cur_mod_id , bases = [base_manager_instance ])
265
+ manager_info .line = line
266
+ manager_info .type_vars = base_manager_info .type_vars
267
+ manager_info .defn .type_vars = base_manager_info .defn .type_vars
268
+ manager_info .defn .line = line
269
+ manager_info .metaclass_type = manager_info .calculate_metaclass_type ()
270
+
271
+ return manager_info
272
+
273
+
274
+ def popuplate_manager_from_queryset (manager_info : TypeInfo , queryset_info : TypeInfo ) -> None :
275
+ """
276
+ Add methods from the QuerySet class to the manager.
277
+ """
278
+
279
+ # Stash the queryset fullname which was passed to .from_queryset So that
280
+ # our 'resolve_manager_method' attribute hook can fetch the method from
281
+ # that QuerySet class
282
+ django_metadata = helpers .get_django_metadata (manager_info )
283
+ django_metadata ["from_queryset_manager" ] = queryset_info .fullname
257
284
258
285
# We collect and mark up all methods before django.db.models.query.QuerySet as class members
259
- for class_mro_info in derived_queryset_info .mro :
286
+ for class_mro_info in queryset_info .mro :
260
287
if class_mro_info .fullname == fullnames .QUERYSET_CLASS_FULLNAME :
261
288
break
262
289
for name , sym in class_mro_info .names .items ():
@@ -270,39 +297,19 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
270
297
# queryset_method: Any = ...
271
298
#
272
299
helpers .add_new_sym_for_info (
273
- new_manager_info ,
300
+ manager_info ,
274
301
name = name ,
275
302
sym_type = AnyType (TypeOfAny .special_form ),
276
303
)
277
304
278
- # For methods on BaseManager that return a queryset we need to update the
279
- # return type to be the actual queryset subclass used. This is done by
280
- # adding the methods as attributes with type Any to the manager class,
281
- # similar to how custom queryset methods are handled above. The actual type
282
- # of these methods are resolved in resolve_manager_method.
283
- for name in MANAGER_METHODS_RETURNING_QUERYSET :
305
+ # For methods on BaseManager that return a queryset we need to update
306
+ # the return type to be the actual queryset subclass used. This is done
307
+ # by adding the methods as attributes with type Any to the manager
308
+ # class. The actual type of these methods are resolved in
309
+ # resolve_manager_method.
310
+ for method_name in MANAGER_METHODS_RETURNING_QUERYSET :
284
311
helpers .add_new_sym_for_info (
285
- new_manager_info ,
286
- name = name ,
312
+ manager_info ,
313
+ name = method_name ,
287
314
sym_type = AnyType (TypeOfAny .special_form ),
288
315
)
289
-
290
- # Insert the new manager (dynamic) class
291
- assert semanal_api .add_symbol_table_node (ctx .name , SymbolTableNode (GDEF , new_manager_info , plugin_generated = True ))
292
-
293
-
294
- def fail_if_manager_type_created_in_model_body (ctx : MethodContext ) -> MypyType :
295
- """
296
- Method hook that checks if method `<Manager>.from_queryset` is called inside a model class body.
297
-
298
- Doing so won't, for instance, trigger the dynamic class hook(`create_new_manager_class_from_from_queryset_method`)
299
- for managers.
300
- """
301
- api = helpers .get_typechecker_api (ctx )
302
- outer_model_info = api .scope .active_class ()
303
- if not outer_model_info or not outer_model_info .has_base (fullnames .MODEL_CLASS_FULLNAME ):
304
- # Not inside a model class definition
305
- return ctx .default_return_type
306
-
307
- api .fail ("`.from_queryset` called from inside model class body" , ctx .context , code = errorcodes .MANAGER_UNTYPED )
308
- return ctx .default_return_type
0 commit comments