1- from typing import Optional , Union
1+ from typing import Optional , Tuple , Union
22
3+ from django .db .models import base , manager
34from mypy .checker import TypeChecker , fill_typevars
45from mypy .nodes import (
56 GDEF ,
1617 TypeInfo ,
1718 Var ,
1819)
19- from mypy .plugin import AttributeContext , ClassDefContext , DynamicClassDefContext , MethodContext
20+ from mypy .plugin import AttributeContext , ClassDefContext , DynamicClassDefContext , SemanticAnalyzerPluginInterface
2021from mypy .types import AnyType , CallableType , Instance , ProperType
2122from mypy .types import Type as MypyType
2223from mypy .types import TypeOfAny
2324from typing_extensions import Final
2425
25- from mypy_django_plugin import errorcodes
2626from mypy_django_plugin .lib import fullnames , helpers
2727
2828MANAGER_METHODS_RETURNING_QUERYSET : Final = frozenset (
@@ -182,81 +182,111 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
182182 """
183183 semanal_api = helpers .get_semanal_api (ctx )
184184
185+ # TODO: Emit an error when called in a class scope
186+ if semanal_api .is_class_scope ():
187+ return
188+
185189 # Don't redeclare the manager class if we've already defined it.
186190 manager_node = semanal_api .lookup_current_scope (ctx .name )
187191 if manager_node and isinstance (manager_node .node , TypeInfo ):
188192 # This is just a deferral run where our work is already finished
189193 return
190194
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 ()
195+ new_manager_info , generated_name = create_manager_info_from_from_queryset_call (ctx .api , ctx .call , ctx .name )
196+ if new_manager_info is None :
197+ if not ctx .api .final_iteration :
198+ ctx .api .defer ()
199199 return
200200
201- assert isinstance (base_manager_info , TypeInfo )
201+ assert generated_name
202+ manager_fullname = "." .join (["django.db.models.manager" , generated_name ])
202203
203- passed_queryset = ctx .call .args [0 ]
204- assert isinstance (passed_queryset , NameExpr )
204+ base_manager_info = new_manager_info .mro [1 ]
205+ base_manager_info .metadata .setdefault ("from_queryset_managers" , {})
206+ base_manager_info .metadata ["from_queryset_managers" ][manager_fullname ] = new_manager_info .fullname
205207
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
208+ # So that the plugin will reparameterize the manager when it is constructed inside of a Model definition
209+ helpers .add_new_manager_base (semanal_api , new_manager_info .fullname )
211210
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
211+ # Insert the new manager (dynamic) class
212+ assert semanal_api . add_symbol_table_node (
213+ ctx . name ,
214+ SymbolTableNode ( GDEF , new_manager_info , plugin_generated = True ),
216215 )
217216
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
227217
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
218+ def create_manager_info_from_from_queryset_call (
219+ api : SemanticAnalyzerPluginInterface , call_expr : CallExpr , name : Optional [str ] = None
220+ ) -> Tuple [Optional [TypeInfo ], Optional [str ]]:
221+ """
222+ Extract manager and queryset TypeInfo from a from_queryset call.
223+ """
224+
225+ if (
226+ # Check that this is a from_queryset call on a manager subclass
227+ not isinstance (call_expr .callee , MemberExpr )
228+ or not isinstance (call_expr .callee .expr , RefExpr )
229+ or not isinstance (call_expr .callee .expr .node , TypeInfo )
230+ or not call_expr .callee .expr .node .has_base (fullnames .BASE_MANAGER_CLASS_FULLNAME )
231+ or not call_expr .callee .name == "from_queryset"
232+ # Check that the call has one or two arguments and that the first is a
233+ # QuerySet subclass
234+ or not 1 <= len (call_expr .args ) <= 2
235+ or not isinstance (call_expr .args [0 ], RefExpr )
236+ or not isinstance (call_expr .args [0 ].node , TypeInfo )
237+ or not call_expr .args [0 ].node .has_base (fullnames .QUERYSET_CLASS_FULLNAME )
238+ ):
239+ return None , None
240+
241+ base_manager_info , queryset_info = call_expr .callee .expr .node , call_expr .args [0 ].node
242+ if queryset_info .fullname is None :
243+ # In some cases, due to the way the semantic analyzer works, only
244+ # passed_queryset.name is available. But it should be analyzed again,
245+ # so this isn't a problem.
246+ return None , None
247+
248+ if len (call_expr .args ) == 2 and isinstance (call_expr .args [1 ], StrExpr ):
249+ manager_name = call_expr .args [1 ].value
244250 else :
245- custom_manager_generated_name = base_manager_info .name + " From" + derived_queryset_info .name
251+ manager_name = f" { base_manager_info .name } From{ queryset_info .name } "
246252
247- custom_manager_generated_fullname = "." .join (["django.db.models.manager" , custom_manager_generated_name ])
248- base_manager_info .metadata .setdefault ("from_queryset_managers" , {})
249- base_manager_info .metadata ["from_queryset_managers" ][custom_manager_generated_fullname ] = new_manager_info .fullname
253+ new_manager_info = create_manager_class (api , base_manager_info , name or manager_name , call_expr .line )
250254
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 )
255+ popuplate_manager_from_queryset (new_manager_info , queryset_info )
253256
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+ return new_manager_info , manager_name
258+
259+
260+ def create_manager_class (
261+ api : SemanticAnalyzerPluginInterface , base_manager_info : TypeInfo , name : str , line : int
262+ ) -> TypeInfo :
263+
264+ base_manager_instance = fill_typevars (base_manager_info )
265+ assert isinstance (base_manager_instance , Instance )
266+
267+ manager_info = api .basic_new_typeinfo (name , basetype_or_fallback = base_manager_instance , line = line )
268+ manager_info .line = line
269+ manager_info .type_vars = base_manager_info .type_vars
270+ manager_info .defn .type_vars = base_manager_info .defn .type_vars
271+ manager_info .defn .line = line
272+ manager_info .metaclass_type = manager_info .calculate_metaclass_type ()
273+
274+ return manager_info
275+
276+
277+ def popuplate_manager_from_queryset (manager_info : TypeInfo , queryset_info : TypeInfo ) -> None :
278+ """
279+ Add methods from the QuerySet class to the manager.
280+ """
281+
282+ # Stash the queryset fullname which was passed to .from_queryset So that
283+ # our 'resolve_manager_method' attribute hook can fetch the method from
284+ # that QuerySet class
285+ django_metadata = helpers .get_django_metadata (manager_info )
286+ django_metadata ["from_queryset_manager" ] = queryset_info .fullname
257287
258288 # 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 :
289+ for class_mro_info in queryset_info .mro :
260290 if class_mro_info .fullname == fullnames .QUERYSET_CLASS_FULLNAME :
261291 break
262292 for name , sym in class_mro_info .names .items ():
@@ -270,39 +300,19 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
270300 # queryset_method: Any = ...
271301 #
272302 helpers .add_new_sym_for_info (
273- new_manager_info ,
303+ manager_info ,
274304 name = name ,
275305 sym_type = AnyType (TypeOfAny .special_form ),
276306 )
277307
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 :
308+ # For methods on BaseManager that return a queryset we need to update
309+ # the return type to be the actual queryset subclass used. This is done
310+ # by adding the methods as attributes with type Any to the manager
311+ # class. The actual type of these methods are resolved in
312+ # resolve_manager_method.
313+ for method_name in MANAGER_METHODS_RETURNING_QUERYSET :
284314 helpers .add_new_sym_for_info (
285- new_manager_info ,
286- name = name ,
315+ manager_info ,
316+ name = method_name ,
287317 sym_type = AnyType (TypeOfAny .special_form ),
288318 )
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