Skip to content

Commit f902275

Browse files
authored
Erase stray typevars in functools.partial generic (#18954)
Fixes #18953. Fixes #15215. Refs #17461. When the function passed to `partial` is generic and has generic params in the return type, we must erase them, otherwise they become orphan and cannot be used later. This only applies to `partial[...]` generic param and not to the underlying "exact" callable stored internally as the latter remains generic. The ultimate fix would be to implement #17620 so that we stop caring about `partial[...]` generic param, but this should improve usability (but causes false negatives).
1 parent cb0d5b5 commit f902275

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

mypy/plugins/functools.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import mypy.plugin
99
import mypy.semanal
1010
from mypy.argmap import map_actuals_to_formals
11+
from mypy.erasetype import erase_typevars
1112
from mypy.nodes import (
1213
ARG_POS,
1314
ARG_STAR2,
@@ -312,7 +313,11 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) -
312313
special_sig="partial",
313314
)
314315

315-
ret = ctx.api.named_generic_type(PARTIAL, [ret_type])
316+
# Do not leak typevars from generic functions - they cannot be usable.
317+
# Keep them in the wrapped callable, but avoid `partial[SomeStrayTypeVar]`
318+
erased_ret_type = erase_typevars(ret_type, [tv.id for tv in fn_type.variables])
319+
320+
ret = ctx.api.named_generic_type(PARTIAL, [erased_ret_type])
316321
ret = ret.copy_with_extra_attr("__mypy_partial", partially_applied)
317322
if partially_applied.param_spec():
318323
assert ret.extra_attrs is not None # copy_with_extra_attr above ensures this

test-data/unit/check-functools.test

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,3 +656,73 @@ def f(x: P):
656656
# TODO: but this is incorrect, predating the functools.partial plugin
657657
reveal_type(partial(x, "a")()) # N: Revealed type is "builtins.int"
658658
[builtins fixtures/tuple.pyi]
659+
660+
[case testFunctoolsPartialTypeVarErasure]
661+
from typing import Callable, TypeVar, Union
662+
from typing_extensions import ParamSpec, TypeVarTuple, Unpack
663+
from functools import partial
664+
665+
def use_int_callable(x: Callable[[int], int]) -> None:
666+
pass
667+
def use_func_callable(
668+
x: Callable[
669+
[Callable[[int], None]],
670+
Callable[[int], None],
671+
],
672+
) -> None:
673+
pass
674+
675+
Tc = TypeVar("Tc", int, str)
676+
Tb = TypeVar("Tb", bound=Union[int, str])
677+
P = ParamSpec("P")
678+
Ts = TypeVarTuple("Ts")
679+
680+
def func_b(a: Tb, b: str) -> Tb:
681+
return a
682+
def func_c(a: Tc, b: str) -> Tc:
683+
return a
684+
685+
def func_fn(fn: Callable[P, Tc], b: str) -> Callable[P, Tc]:
686+
return fn
687+
def func_fn_unpack(fn: Callable[[Unpack[Ts]], Tc], b: str) -> Callable[[Unpack[Ts]], Tc]:
688+
return fn
689+
690+
# We should not leak stray typevars that aren't in scope:
691+
reveal_type(partial(func_b, b="")) # N: Revealed type is "functools.partial[Any]"
692+
reveal_type(partial(func_c, b="")) # N: Revealed type is "functools.partial[Any]"
693+
reveal_type(partial(func_fn, b="")) # N: Revealed type is "functools.partial[def (*Any, **Any) -> Any]"
694+
reveal_type(partial(func_fn_unpack, b="")) # N: Revealed type is "functools.partial[def (*Any) -> Any]"
695+
696+
use_int_callable(partial(func_b, b=""))
697+
use_func_callable(partial(func_b, b=""))
698+
use_int_callable(partial(func_c, b=""))
699+
use_func_callable(partial(func_c, b=""))
700+
use_int_callable(partial(func_fn, b="")) # E: Argument 1 to "use_int_callable" has incompatible type "partial[Callable[[VarArg(Any), KwArg(Any)], Any]]"; expected "Callable[[int], int]" \
701+
# N: "partial[Callable[[VarArg(Any), KwArg(Any)], Any]].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], Callable[[VarArg(Any), KwArg(Any)], Any]]"
702+
use_func_callable(partial(func_fn, b=""))
703+
use_int_callable(partial(func_fn_unpack, b="")) # E: Argument 1 to "use_int_callable" has incompatible type "partial[Callable[[VarArg(Any)], Any]]"; expected "Callable[[int], int]" \
704+
# N: "partial[Callable[[VarArg(Any)], Any]].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], Callable[[VarArg(Any)], Any]]"
705+
use_func_callable(partial(func_fn_unpack, b=""))
706+
707+
# But we should not erase typevars that aren't bound by function
708+
# passed to `partial`:
709+
710+
def outer_b(arg: Tb) -> None:
711+
712+
def inner(a: Tb, b: str) -> Tb:
713+
return a
714+
715+
reveal_type(partial(inner, b="")) # N: Revealed type is "functools.partial[Tb`-1]"
716+
use_int_callable(partial(inner, b="")) # E: Argument 1 to "use_int_callable" has incompatible type "partial[Tb]"; expected "Callable[[int], int]" \
717+
# N: "partial[Tb].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], Tb]"
718+
719+
def outer_c(arg: Tc) -> None:
720+
721+
def inner(a: Tc, b: str) -> Tc:
722+
return a
723+
724+
reveal_type(partial(inner, b="")) # N: Revealed type is "functools.partial[builtins.int]" \
725+
# N: Revealed type is "functools.partial[builtins.str]"
726+
use_int_callable(partial(inner, b="")) # E: Argument 1 to "use_int_callable" has incompatible type "partial[str]"; expected "Callable[[int], int]" \
727+
# N: "partial[str].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], str]"
728+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)