99import mypy .semanal
1010from mypy .argmap import map_actuals_to_formals
1111from mypy .erasetype import erase_typevars
12+ from mypy .expandtype import expand_type
13+ from mypy .infer import infer_type_arguments
1214from mypy .nodes import (
1315 ARG_POS ,
1416 ARG_STAR2 ,
1517 SYMBOL_FUNCBASE_TYPES ,
1618 ArgKind ,
1719 Argument ,
1820 CallExpr ,
21+ Expression ,
22+ MemberExpr ,
1923 NameExpr ,
2024 Var ,
2125)
2226from mypy .plugins .common import add_method_to_class
2327from mypy .typeops import get_all_type_vars
2428from mypy .types import (
29+ ANY_STRATEGY ,
2530 AnyType ,
31+ BoolTypeQuery ,
2632 CallableType ,
2733 Instance ,
2834 Overloaded ,
2935 ParamSpecFlavor ,
3036 ParamSpecType ,
3137 Type ,
3238 TypeOfAny ,
39+ TypeVarId ,
3340 TypeVarType ,
3441 UnboundType ,
3542 UnionType ,
43+ UnpackType ,
3644 get_proper_type ,
3745)
3846
4149_ORDERING_METHODS : Final = {"__lt__" , "__le__" , "__gt__" , "__ge__" }
4250
4351PARTIAL : Final = "functools.partial"
52+ PLACEHOLDER : Final = "functools.Placeholder"
4453
4554
4655class _MethodInfo :
@@ -134,6 +143,22 @@ def _analyze_class(ctx: mypy.plugin.ClassDefContext) -> dict[str, _MethodInfo |
134143 return comparison_methods
135144
136145
146+ def _is_functools_placeholder (expr : Expression ) -> bool :
147+ return isinstance (expr , (NameExpr , MemberExpr )) and expr .fullname == PLACEHOLDER
148+
149+
150+ class _HasUnpack (BoolTypeQuery ):
151+ def __init__ (self ) -> None :
152+ super ().__init__ (ANY_STRATEGY )
153+
154+ def visit_unpack_type (self , t : UnpackType ) -> bool :
155+ return True
156+
157+
158+ def _has_unpack (typ : Type ) -> bool :
159+ return typ .accept (_HasUnpack ())
160+
161+
137162def partial_new_callback (ctx : mypy .plugin .FunctionContext ) -> Type :
138163 """Infer a more precise return type for functools.partial"""
139164 if not isinstance (ctx .api , mypy .checker .TypeChecker ): # use internals
@@ -184,6 +209,7 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) -
184209 actual_arg_kinds = []
185210 actual_arg_names = []
186211 actual_types = []
212+ placeholder_actuals = []
187213 seen_args = set ()
188214 for i , param in enumerate (ctx .args [1 :], start = 1 ):
189215 for j , a in enumerate (param ):
@@ -198,6 +224,9 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) -
198224 actual_arg_kinds .append (ctx .arg_kinds [i ][j ])
199225 actual_arg_names .append (ctx .arg_names [i ][j ])
200226 actual_types .append (ctx .arg_types [i ][j ])
227+ placeholder_actuals .append (
228+ ctx .arg_kinds [i ][j ].is_positional () and _is_functools_placeholder (a )
229+ )
201230
202231 formal_to_actual = map_actuals_to_formals (
203232 actual_kinds = actual_arg_kinds ,
@@ -215,8 +244,20 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) -
215244 continue
216245 can_infer_ids .update ({tv .id for tv in get_all_type_vars (arg_type )})
217246
247+ defaulted_arg_types = list (fn_type .arg_types )
248+ for i , actuals in enumerate (formal_to_actual ):
249+ if any (placeholder_actuals [j ] for j in actuals ):
250+ # functools.Placeholder is a positional sentinel introduced in Python 3.14.
251+ # It occupies the formal slot but does not bind it, so make the validation
252+ # call accept the sentinel while preserving the original type for the
253+ # resulting partial signature below.
254+ defaulted_arg_types [i ] = actual_types [
255+ next (j for j in actuals if placeholder_actuals [j ])
256+ ]
257+
218258 # special_sig="partial" allows omission of args/kwargs typed with ParamSpec
219259 defaulted = fn_type .copy_modified (
260+ arg_types = defaulted_arg_types ,
220261 arg_kinds = [
221262 (
222263 ArgKind .ARG_OPT
@@ -273,10 +314,30 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) -
273314 partial_kinds = []
274315 partial_types = []
275316 partial_names = []
317+ inferred_type_vars : dict [TypeVarId , Type ] = {}
318+ if any (placeholder_actuals ) and len (bound .arg_types ) == len (fn_type .arg_types ):
319+ for i , actuals in enumerate (formal_to_actual ):
320+ if not actuals or any (placeholder_actuals [j ] for j in actuals ):
321+ continue
322+ if _has_unpack (fn_type .arg_types [i ]) or _has_unpack (bound .arg_types [i ]):
323+ # TypeVarTuple/Unpack constraints are handled by check_call() above. Calling
324+ # infer_type_arguments() directly on an UnpackType trips the constraint builder's
325+ # internal "unpack should be handled at a higher level" guard.
326+ continue
327+ inferred_args = infer_type_arguments (
328+ fn_type .variables , fn_type .arg_types [i ], bound .arg_types [i ]
329+ )
330+ for type_var , inferred_arg in zip (fn_type .variables , inferred_args ):
331+ if inferred_arg is not None and mypy .checker .is_valid_inferred_type (
332+ inferred_arg , ctx .api .options
333+ ):
334+ inferred_type_vars [type_var .id ] = inferred_arg
276335 # We need to fully apply any positional arguments (they cannot be respecified)
277336 # However, keyword arguments can be respecified, so just give them a default
278337 for i , actuals in enumerate (formal_to_actual ):
279- if len (bound .arg_types ) == len (fn_type .arg_types ):
338+ if any (placeholder_actuals [j ] for j in actuals ):
339+ arg_type = expand_type (fn_type .arg_types [i ], inferred_type_vars )
340+ elif len (bound .arg_types ) == len (fn_type .arg_types ):
280341 arg_type = bound .arg_types [i ]
281342 if not mypy .checker .is_valid_inferred_type (arg_type , ctx .api .options ):
282343 arg_type = fn_type .arg_types [i ] # bit of a hack
@@ -285,10 +346,16 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) -
285346 # true when PEP 646 things are happening. See testFunctoolsPartialTypeVarTuple
286347 arg_type = fn_type .arg_types [i ]
287348
288- if not actuals or fn_type .arg_kinds [i ] in (ArgKind .ARG_STAR , ArgKind .ARG_STAR2 ):
349+ if (
350+ not actuals
351+ or fn_type .arg_kinds [i ] in (ArgKind .ARG_STAR , ArgKind .ARG_STAR2 )
352+ or any (placeholder_actuals [j ] for j in actuals )
353+ ):
289354 partial_kinds .append (fn_type .arg_kinds [i ])
290355 partial_types .append (arg_type )
291- partial_names .append (fn_type .arg_names [i ])
356+ partial_names .append (
357+ None if any (placeholder_actuals [j ] for j in actuals ) else fn_type .arg_names [i ]
358+ )
292359 else :
293360 assert actuals
294361 if any (actual_arg_kinds [j ] in (ArgKind .ARG_POS , ArgKind .ARG_STAR ) for j in actuals ):
0 commit comments