15
15
TypeInfo ,
16
16
Var ,
17
17
)
18
- from mypy .plugin import AttributeContext , DynamicClassDefContext , SemanticAnalyzerPluginInterface
18
+ from mypy .plugin import AttributeContext , DynamicClassDefContext
19
+ from mypy .semanal import SemanticAnalyzer
19
20
from mypy .semanal_shared import has_placeholder
20
21
from mypy .types import AnyType , CallableType , Instance , ProperType
21
22
from mypy .types import Type as MypyType
@@ -150,7 +151,6 @@ def get_method_type_from_reverse_manager(
150
151
151
152
152
153
def resolve_manager_method_from_instance (instance : Instance , method_name : str , ctx : AttributeContext ) -> MypyType :
153
-
154
154
api = helpers .get_typechecker_api (ctx )
155
155
method_type = get_method_type_from_dynamic_manager (
156
156
api , method_name , instance
@@ -164,9 +164,11 @@ def resolve_manager_method(ctx: AttributeContext) -> MypyType:
164
164
A 'get_attribute_hook' that is intended to be invoked whenever the TypeChecker encounters
165
165
an attribute on a class that has 'django.db.models.BaseManager' as a base.
166
166
"""
167
- # Skip (method) type that is currently something other than Any
167
+ # Skip (method) type that is currently something other than Any of type `implementation_artifact`
168
168
if not isinstance (ctx .default_attr_type , AnyType ):
169
169
return ctx .default_attr_type
170
+ elif ctx .default_attr_type .type_of_any != TypeOfAny .implementation_artifact :
171
+ return ctx .default_attr_type
170
172
171
173
# (Current state is:) We wouldn't end up here when looking up a method from a custom _manager_.
172
174
# That's why we only attempt to lookup the method for either a dynamically added or reverse manager.
@@ -197,12 +199,12 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
197
199
return
198
200
199
201
# Don't redeclare the manager class if we've already defined it.
200
- manager_node = semanal_api .lookup_current_scope (ctx .name )
201
- if manager_node and isinstance (manager_node .node , TypeInfo ):
202
+ manager_sym = semanal_api .lookup_current_scope (ctx .name )
203
+ if manager_sym and isinstance (manager_sym .node , TypeInfo ):
202
204
# This is just a deferral run where our work is already finished
203
205
return
204
206
205
- new_manager_info = create_manager_info_from_from_queryset_call (ctx . api , ctx .call , ctx .name )
207
+ new_manager_info = create_manager_info_from_from_queryset_call (semanal_api , ctx .call , ctx .name )
206
208
if new_manager_info is None :
207
209
if not ctx .api .final_iteration :
208
210
ctx .api .defer ()
@@ -212,8 +214,17 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
212
214
helpers .add_new_manager_base (semanal_api , new_manager_info .fullname )
213
215
214
216
217
+ def register_dynamically_created_manager (fullname : str , manager_name : str , manager_base : TypeInfo ) -> None :
218
+ manager_base .metadata .setdefault ("from_queryset_managers" , {})
219
+ # The `__module__` value of the manager type created by Django's
220
+ # `.from_queryset` is `django.db.models.manager`. But we put new type(s) in the
221
+ # module currently being processed, so we'll map those together through metadata.
222
+ runtime_fullname = "." .join (["django.db.models.manager" , manager_name ])
223
+ manager_base .metadata ["from_queryset_managers" ][runtime_fullname ] = fullname
224
+
225
+
215
226
def create_manager_info_from_from_queryset_call (
216
- api : SemanticAnalyzerPluginInterface , call_expr : CallExpr , name : Optional [str ] = None
227
+ api : SemanticAnalyzer , call_expr : CallExpr , name : Optional [str ] = None
217
228
) -> Optional [TypeInfo ]:
218
229
"""
219
230
Extract manager and queryset TypeInfo from a from_queryset call.
@@ -247,30 +258,48 @@ def create_manager_info_from_from_queryset_call(
247
258
else :
248
259
manager_name = f"{ base_manager_info .name } From{ queryset_info .name } "
249
260
250
- try :
251
- new_manager_info = create_manager_class (api , base_manager_info , name or manager_name , call_expr .line )
252
- except helpers .IncompleteDefnException :
253
- return None
254
-
255
- popuplate_manager_from_queryset (new_manager_info , queryset_info )
256
-
257
- manager_fullname = "." .join (["django.db.models.manager" , manager_name ])
258
-
259
- base_manager_info = new_manager_info .mro [1 ]
260
- base_manager_info .metadata .setdefault ("from_queryset_managers" , {})
261
- base_manager_info .metadata ["from_queryset_managers" ][manager_fullname ] = new_manager_info .fullname
261
+ # Always look in global scope, as that's where we'll declare dynamic manager classes
262
+ manager_sym = api .globals .get (manager_name )
263
+ if (
264
+ manager_sym is not None
265
+ and isinstance (manager_sym .node , TypeInfo )
266
+ and manager_sym .node .has_base (base_manager_info .fullname )
267
+ and manager_sym .node .metadata .get ("django" , {}).get ("from_queryset_manager" ) == queryset_info .fullname
268
+ ):
269
+ # Reuse an identical, already generated, manager
270
+ new_manager_info = manager_sym .node
271
+ else :
272
+ # Create a new `TypeInfo` instance for the manager type
273
+ try :
274
+ new_manager_info = create_manager_class (
275
+ api = api ,
276
+ base_manager_info = base_manager_info ,
277
+ name = manager_name ,
278
+ line = call_expr .line ,
279
+ with_unique_name = name is not None and name != manager_name ,
280
+ )
281
+ except helpers .IncompleteDefnException :
282
+ return None
283
+
284
+ populate_manager_from_queryset (new_manager_info , queryset_info )
285
+ register_dynamically_created_manager (
286
+ fullname = new_manager_info .fullname ,
287
+ manager_name = manager_name ,
288
+ manager_base = base_manager_info ,
289
+ )
262
290
263
291
# Add the new manager to the current module
264
292
module = api .modules [api .cur_mod_id ]
265
- module . names [ name or manager_name ] = SymbolTableNode (
266
- GDEF , new_manager_info , plugin_generated = True , no_serialize = False
267
- )
293
+ if name is not None and name != new_manager_info . name :
294
+ # Unless names are equal, there's 2 symbol names that needs the manager info
295
+ module . names [ name ] = SymbolTableNode ( GDEF , new_manager_info , plugin_generated = True )
268
296
297
+ module .names [new_manager_info .name ] = SymbolTableNode (GDEF , new_manager_info , plugin_generated = True )
269
298
return new_manager_info
270
299
271
300
272
301
def create_manager_class (
273
- api : SemanticAnalyzerPluginInterface , base_manager_info : TypeInfo , name : str , line : int
302
+ api : SemanticAnalyzer , base_manager_info : TypeInfo , name : str , line : int , with_unique_name : bool
274
303
) -> TypeInfo :
275
304
276
305
base_manager_instance = fill_typevars (base_manager_info )
@@ -280,17 +309,24 @@ def create_manager_class(
280
309
if any (has_placeholder (type_var ) for type_var in base_manager_info .defn .type_vars ):
281
310
raise helpers .IncompleteDefnException
282
311
283
- manager_info = helpers .create_type_info (name , api .cur_mod_id , bases = [base_manager_instance ])
312
+ if with_unique_name :
313
+ manager_info = helpers .add_new_class_for_module (
314
+ module = api .modules [api .cur_mod_id ],
315
+ name = name ,
316
+ bases = [base_manager_instance ],
317
+ )
318
+ else :
319
+ manager_info = helpers .create_type_info (name , api .cur_mod_id , bases = [base_manager_instance ])
320
+
284
321
manager_info .line = line
285
322
manager_info .type_vars = base_manager_info .type_vars
286
323
manager_info .defn .type_vars = base_manager_info .defn .type_vars
287
324
manager_info .defn .line = line
288
- manager_info .metaclass_type = manager_info .calculate_metaclass_type ()
289
325
290
326
return manager_info
291
327
292
328
293
- def popuplate_manager_from_queryset (manager_info : TypeInfo , queryset_info : TypeInfo ) -> None :
329
+ def populate_manager_from_queryset (manager_info : TypeInfo , queryset_info : TypeInfo ) -> None :
294
330
"""
295
331
Add methods from the QuerySet class to the manager.
296
332
"""
@@ -318,7 +354,7 @@ def popuplate_manager_from_queryset(manager_info: TypeInfo, queryset_info: TypeI
318
354
helpers .add_new_sym_for_info (
319
355
manager_info ,
320
356
name = name ,
321
- sym_type = AnyType (TypeOfAny .special_form ),
357
+ sym_type = AnyType (TypeOfAny .implementation_artifact ),
322
358
)
323
359
324
360
# For methods on BaseManager that return a queryset we need to update
@@ -330,5 +366,103 @@ def popuplate_manager_from_queryset(manager_info: TypeInfo, queryset_info: TypeI
330
366
helpers .add_new_sym_for_info (
331
367
manager_info ,
332
368
name = method_name ,
333
- sym_type = AnyType (TypeOfAny .special_form ),
369
+ sym_type = AnyType (TypeOfAny .implementation_artifact ),
334
370
)
371
+
372
+
373
+ def create_new_manager_class_from_as_manager_method (ctx : DynamicClassDefContext ) -> None :
374
+ """
375
+ Insert a new manager class node for a
376
+
377
+ ```
378
+ <manager name> = <QuerySet>.as_manager()
379
+ ```
380
+ """
381
+ semanal_api = helpers .get_semanal_api (ctx )
382
+ # Don't redeclare the manager class if we've already defined it.
383
+ manager_node = semanal_api .lookup_current_scope (ctx .name )
384
+ if manager_node and manager_node .type is not None :
385
+ # This is just a deferral run where our work is already finished
386
+ return
387
+
388
+ manager_sym = semanal_api .lookup_fully_qualified_or_none (fullnames .MANAGER_CLASS_FULLNAME )
389
+ assert manager_sym is not None
390
+ manager_base = manager_sym .node
391
+ if manager_base is None :
392
+ if not semanal_api .final_iteration :
393
+ semanal_api .defer ()
394
+ return
395
+
396
+ assert isinstance (manager_base , TypeInfo )
397
+
398
+ callee = ctx .call .callee
399
+ assert isinstance (callee , MemberExpr )
400
+ assert isinstance (callee .expr , RefExpr )
401
+
402
+ queryset_info = callee .expr .node
403
+ if queryset_info is None :
404
+ if not semanal_api .final_iteration :
405
+ semanal_api .defer ()
406
+ return
407
+
408
+ assert isinstance (queryset_info , TypeInfo )
409
+
410
+ manager_class_name = manager_base .name + "From" + queryset_info .name
411
+ current_module = semanal_api .modules [semanal_api .cur_mod_id ]
412
+ existing_sym = current_module .names .get (manager_class_name )
413
+ if (
414
+ existing_sym is not None
415
+ and isinstance (existing_sym .node , TypeInfo )
416
+ and existing_sym .node .has_base (fullnames .MANAGER_CLASS_FULLNAME )
417
+ and existing_sym .node .metadata .get ("django" , {}).get ("from_queryset_manager" ) == queryset_info .fullname
418
+ ):
419
+ # Reuse an identical, already generated, manager
420
+ new_manager_info = existing_sym .node
421
+ else :
422
+ # Create a new `TypeInfo` instance for the manager type
423
+ try :
424
+ new_manager_info = create_manager_class (
425
+ api = semanal_api ,
426
+ base_manager_info = manager_base ,
427
+ name = manager_class_name ,
428
+ line = ctx .call .line ,
429
+ with_unique_name = True ,
430
+ )
431
+ except helpers .IncompleteDefnException :
432
+ if not semanal_api .final_iteration :
433
+ semanal_api .defer ()
434
+ return
435
+
436
+ populate_manager_from_queryset (new_manager_info , queryset_info )
437
+ register_dynamically_created_manager (
438
+ fullname = new_manager_info .fullname ,
439
+ manager_name = manager_class_name ,
440
+ manager_base = manager_base ,
441
+ )
442
+
443
+ # So that the plugin will reparameterize the manager when it is constructed inside of a Model definition
444
+ helpers .add_new_manager_base (semanal_api , new_manager_info .fullname )
445
+
446
+ # Whenever `<QuerySet>.as_manager()` isn't called at class level, we want to ensure
447
+ # that the variable is an instance of our generated manager. Instead of the return
448
+ # value of `.as_manager()`. Though model argument is populated as `Any`.
449
+ # `transformers.models.AddManagers` will populate a model's manager(s), when it
450
+ # finds it on class level.
451
+ var = Var (name = ctx .name , type = Instance (new_manager_info , [AnyType (TypeOfAny .from_omitted_generics )]))
452
+ var .info = new_manager_info
453
+ var ._fullname = f"{ current_module .fullname } .{ ctx .name } "
454
+ var .is_inferred = True
455
+ # Note: Order of `add_symbol_table_node` calls matters. Depending on what level
456
+ # we've found the `.as_manager()` call. Point here being that we want to replace the
457
+ # `.as_manager` return value with our newly created manager.
458
+ assert semanal_api .add_symbol_table_node (
459
+ ctx .name , SymbolTableNode (semanal_api .current_symbol_kind (), var , plugin_generated = True )
460
+ )
461
+ # Add the new manager to the current module
462
+ assert semanal_api .add_symbol_table_node (
463
+ # We'll use `new_manager_info.name` instead of `manager_class_name` here
464
+ # to handle possible name collisions, as it's unique.
465
+ new_manager_info .name ,
466
+ # Note that the generated manager type is always inserted at module level
467
+ SymbolTableNode (GDEF , new_manager_info , plugin_generated = True ),
468
+ )
0 commit comments