1515 TypeInfo ,
1616 Var ,
1717)
18- from mypy .plugin import AttributeContext , DynamicClassDefContext , SemanticAnalyzerPluginInterface
18+ from mypy .plugin import AttributeContext , DynamicClassDefContext
19+ from mypy .semanal import SemanticAnalyzer
1920from mypy .semanal_shared import has_placeholder
2021from mypy .types import AnyType , CallableType , Instance , ProperType
2122from mypy .types import Type as MypyType
@@ -150,7 +151,6 @@ def get_method_type_from_reverse_manager(
150151
151152
152153def resolve_manager_method_from_instance (instance : Instance , method_name : str , ctx : AttributeContext ) -> MypyType :
153-
154154 api = helpers .get_typechecker_api (ctx )
155155 method_type = get_method_type_from_dynamic_manager (
156156 api , method_name , instance
@@ -164,9 +164,11 @@ def resolve_manager_method(ctx: AttributeContext) -> MypyType:
164164 A 'get_attribute_hook' that is intended to be invoked whenever the TypeChecker encounters
165165 an attribute on a class that has 'django.db.models.BaseManager' as a base.
166166 """
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`
168168 if not isinstance (ctx .default_attr_type , AnyType ):
169169 return ctx .default_attr_type
170+ elif ctx .default_attr_type .type_of_any != TypeOfAny .implementation_artifact :
171+ return ctx .default_attr_type
170172
171173 # (Current state is:) We wouldn't end up here when looking up a method from a custom _manager_.
172174 # 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
197199 return
198200
199201 # 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 ):
202204 # This is just a deferral run where our work is already finished
203205 return
204206
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 )
206208 if new_manager_info is None :
207209 if not ctx .api .final_iteration :
208210 ctx .api .defer ()
@@ -212,8 +214,17 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
212214 helpers .add_new_manager_base (semanal_api , new_manager_info .fullname )
213215
214216
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+
215226def 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
217228) -> Optional [TypeInfo ]:
218229 """
219230 Extract manager and queryset TypeInfo from a from_queryset call.
@@ -247,30 +258,48 @@ def create_manager_info_from_from_queryset_call(
247258 else :
248259 manager_name = f"{ base_manager_info .name } From{ queryset_info .name } "
249260
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+ )
262290
263291 # Add the new manager to the current module
264292 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 )
268296
297+ module .names [new_manager_info .name ] = SymbolTableNode (GDEF , new_manager_info , plugin_generated = True )
269298 return new_manager_info
270299
271300
272301def 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
274303) -> TypeInfo :
275304
276305 base_manager_instance = fill_typevars (base_manager_info )
@@ -280,17 +309,24 @@ def create_manager_class(
280309 if any (has_placeholder (type_var ) for type_var in base_manager_info .defn .type_vars ):
281310 raise helpers .IncompleteDefnException
282311
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+
284321 manager_info .line = line
285322 manager_info .type_vars = base_manager_info .type_vars
286323 manager_info .defn .type_vars = base_manager_info .defn .type_vars
287324 manager_info .defn .line = line
288- manager_info .metaclass_type = manager_info .calculate_metaclass_type ()
289325
290326 return manager_info
291327
292328
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 :
294330 """
295331 Add methods from the QuerySet class to the manager.
296332 """
@@ -318,7 +354,7 @@ def popuplate_manager_from_queryset(manager_info: TypeInfo, queryset_info: TypeI
318354 helpers .add_new_sym_for_info (
319355 manager_info ,
320356 name = name ,
321- sym_type = AnyType (TypeOfAny .special_form ),
357+ sym_type = AnyType (TypeOfAny .implementation_artifact ),
322358 )
323359
324360 # 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
330366 helpers .add_new_sym_for_info (
331367 manager_info ,
332368 name = method_name ,
333- sym_type = AnyType (TypeOfAny .special_form ),
369+ sym_type = AnyType (TypeOfAny .implementation_artifact ),
334370 )
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