diff --git a/mypy/plugins/functools.py b/mypy/plugins/functools.py index 25a8c83007ba..c8b370f15e6d 100644 --- a/mypy/plugins/functools.py +++ b/mypy/plugins/functools.py @@ -8,6 +8,7 @@ import mypy.plugin import mypy.semanal from mypy.argmap import map_actuals_to_formals +from mypy.erasetype import erase_typevars from mypy.nodes import ( ARG_POS, ARG_STAR2, @@ -312,7 +313,11 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) - special_sig="partial", ) - ret = ctx.api.named_generic_type(PARTIAL, [ret_type]) + # Do not leak typevars from generic functions - they cannot be usable. + # Keep them in the wrapped callable, but avoid `partial[SomeStrayTypeVar]` + erased_ret_type = erase_typevars(ret_type, [tv.id for tv in fn_type.variables]) + + ret = ctx.api.named_generic_type(PARTIAL, [erased_ret_type]) ret = ret.copy_with_extra_attr("__mypy_partial", partially_applied) if partially_applied.param_spec(): assert ret.extra_attrs is not None # copy_with_extra_attr above ensures this diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 53ddc96cbe19..1f2046999af9 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -658,3 +658,73 @@ def f(x: P): # TODO: but this is incorrect, predating the functools.partial plugin reveal_type(partial(x, "a")()) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] + +[case testFunctoolsPartialTypeVarErasure] +from typing import Callable, TypeVar, Union +from typing_extensions import ParamSpec, TypeVarTuple, Unpack +from functools import partial + +def use_int_callable(x: Callable[[int], int]) -> None: + pass +def use_func_callable( + x: Callable[ + [Callable[[int], None]], + Callable[[int], None], + ], +) -> None: + pass + +Tc = TypeVar("Tc", int, str) +Tb = TypeVar("Tb", bound=Union[int, str]) +P = ParamSpec("P") +Ts = TypeVarTuple("Ts") + +def func_b(a: Tb, b: str) -> Tb: + return a +def func_c(a: Tc, b: str) -> Tc: + return a + +def func_fn(fn: Callable[P, Tc], b: str) -> Callable[P, Tc]: + return fn +def func_fn_unpack(fn: Callable[[Unpack[Ts]], Tc], b: str) -> Callable[[Unpack[Ts]], Tc]: + return fn + +# We should not leak stray typevars that aren't in scope: +reveal_type(partial(func_b, b="")) # N: Revealed type is "functools.partial[Any]" +reveal_type(partial(func_c, b="")) # N: Revealed type is "functools.partial[Any]" +reveal_type(partial(func_fn, b="")) # N: Revealed type is "functools.partial[def (*Any, **Any) -> Any]" +reveal_type(partial(func_fn_unpack, b="")) # N: Revealed type is "functools.partial[def (*Any) -> Any]" + +use_int_callable(partial(func_b, b="")) +use_func_callable(partial(func_b, b="")) +use_int_callable(partial(func_c, b="")) +use_func_callable(partial(func_c, b="")) +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]" \ + # N: "partial[Callable[[VarArg(Any), KwArg(Any)], Any]].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], Callable[[VarArg(Any), KwArg(Any)], Any]]" +use_func_callable(partial(func_fn, b="")) +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]" \ + # N: "partial[Callable[[VarArg(Any)], Any]].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], Callable[[VarArg(Any)], Any]]" +use_func_callable(partial(func_fn_unpack, b="")) + +# But we should not erase typevars that aren't bound by function +# passed to `partial`: + +def outer_b(arg: Tb) -> None: + + def inner(a: Tb, b: str) -> Tb: + return a + + reveal_type(partial(inner, b="")) # N: Revealed type is "functools.partial[Tb`-1]" + use_int_callable(partial(inner, b="")) # E: Argument 1 to "use_int_callable" has incompatible type "partial[Tb]"; expected "Callable[[int], int]" \ + # N: "partial[Tb].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], Tb]" + +def outer_c(arg: Tc) -> None: + + def inner(a: Tc, b: str) -> Tc: + return a + + reveal_type(partial(inner, b="")) # N: Revealed type is "functools.partial[builtins.int]" \ + # N: Revealed type is "functools.partial[builtins.str]" + use_int_callable(partial(inner, b="")) # E: Argument 1 to "use_int_callable" has incompatible type "partial[str]"; expected "Callable[[int], int]" \ + # N: "partial[str].__call__" has type "Callable[[VarArg(Any), KwArg(Any)], str]" +[builtins fixtures/tuple.pyi]