1616from django .db .models .sql .query import Query
1717from mypy .checker import TypeChecker
1818from mypy .errorcodes import NO_REDEF
19- from mypy .nodes import ARG_NAMED , ARG_NAMED_OPT , ARG_STAR , CallExpr , Expression , ListExpr , SetExpr , TupleExpr
19+ from mypy .nodes import ARG_NAMED , ARG_NAMED_OPT , ARG_STAR , CallExpr , Expression , ListExpr , SetExpr , TupleExpr , Var
2020from mypy .plugin import FunctionContext , MethodContext
2121from mypy .types import AnyType , Instance , LiteralType , ProperType , TupleType , TypedDictType , TypeOfAny , get_proper_type
2222from mypy .types import Type as MypyType
@@ -198,12 +198,39 @@ def gather_kwargs(ctx: MethodContext) -> dict[str, MypyType] | None:
198198 return kwargs
199199
200200
201+ def _resolve_output_field_type (expr_type : MypyType ) -> MypyType | None :
202+ """Try to resolve the Python type for an expression's output_field ClassVar.
203+
204+ Returns None if the output_field can't be statically resolved.
205+ """
206+ proper = get_proper_type (expr_type )
207+ if not isinstance (proper , Instance ):
208+ return None
209+
210+ output_field_sym = proper .type .get ("output_field" )
211+ if output_field_sym is None or output_field_sym .node is None :
212+ return None
213+
214+ node = output_field_sym .node
215+ if not isinstance (node , Var ) or node .type is None :
216+ return None
217+
218+ field_type = get_proper_type (node .type )
219+ if not isinstance (field_type , Instance ):
220+ return None
221+
222+ return helpers .get_private_descriptor_type (field_type .type , "_pyi_private_get_type" , is_nullable = False )
223+
224+
201225def gather_expression_types (ctx : MethodContext ) -> dict [str , MypyType ]:
202226 kwargs = gather_kwargs (ctx )
203227 if not kwargs :
204228 return {}
205229
206- # For now, we don't try to resolve the output_field of the field would be, but use Any.
230+ # Try to resolve the output_field type for each expression. For expressions
231+ # with a static ClassVar output_field (e.g. Count → IntegerField → int),
232+ # we can infer the concrete Python type. Otherwise, fall back to Any.
233+ #
207234 # NOTE: It's important that we use 'special_form' for 'Any' as otherwise we can
208235 # get stuck with mypy interpreting an overload ambiguity towards the
209236 # overloaded 'Field.__get__' method when its 'model' argument gets matched. This
@@ -230,7 +257,14 @@ def gather_expression_types(ctx: MethodContext) -> dict[str, MypyType]:
230257 # select due to the 'Any' in 'TypedDict({"foo": Any})'. But if we specify the
231258 # 'Any' as 'TypeOfAny.special_form' mypy doesn't consider the model instance to
232259 # contain 'Any' and the ambiguity goes away.
233- return {name : AnyType (TypeOfAny .special_form ) for name , _ in kwargs .items ()}
260+ result : dict [str , MypyType ] = {}
261+ for name , expr_type in kwargs .items ():
262+ resolved = _resolve_output_field_type (expr_type )
263+ if resolved is not None and not isinstance (get_proper_type (resolved ), AnyType ):
264+ result [name ] = resolved
265+ else :
266+ result [name ] = AnyType (TypeOfAny .special_form )
267+ return result
234268
235269
236270def extract_proper_type_queryset_annotate (ctx : MethodContext , django_context : DjangoContext ) -> MypyType :
0 commit comments