diff --git a/mypy/subtypes.py b/mypy/subtypes.py index c76b3569fdd4..e08620c0c985 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1603,6 +1603,17 @@ def are_parameters_compatible( return True trivial_suffix = is_trivial_suffix(right) and not is_proper_subtype + # def _(*a: Unpack[tuple[object, ...]]) allows any number of arguments, not just infinite. + if right_star and isinstance(right_star.typ, UnpackType): + right_star_inner_type = get_proper_type(right_star.typ.type) + trivial_varargs = ( + isinstance(right_star_inner_type, Instance) + and right_star_inner_type.type.fullname == "builtins.tuple" + and len(right_star_inner_type.args) == 1 + ) + else: + trivial_varargs = False + if ( right.arg_kinds == [ARG_STAR] and isinstance(get_proper_type(right.arg_types[0]), AnyType) @@ -1640,14 +1651,20 @@ def are_parameters_compatible( # Furthermore, if we're checking for compatibility in all cases, # we confirm that if R accepts an infinite number of arguments, # L must accept the same. - def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | None) -> bool: + def _incompatible( + left_arg: FormalArgument | None, right_arg: FormalArgument | None, varargs: bool + ) -> bool: if right_arg is None: return False if left_arg is None: - return not allow_partial_overlap and not trivial_suffix + return ( + not allow_partial_overlap + and not trivial_suffix + and (not varargs or not trivial_varargs) + ) return not is_compat(right_arg.typ, left_arg.typ) - if _incompatible(left_star, right_star) or _incompatible(left_star2, right_star2): + if _incompatible(left_star, right_star, True) or _incompatible(left_star2, right_star2, False): return False # Phase 1b: Check non-star args: for every arg right can accept, left must @@ -1672,8 +1689,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N # Phase 1c: Check var args. Right has an infinite series of optional positional # arguments. Get all further positional args of left, and make sure # they're more general than the corresponding member in right. - # TODO: are we handling UnpackType correctly here? - if right_star is not None and not trivial_suffix: + if right_star is not None and not trivial_suffix and not trivial_varargs: # Synthesize an anonymous formal argument for the right right_by_position = right.try_synthesizing_arg_from_vararg(None) assert right_by_position is not None diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index f49e1b3c6613..2c0d3d9e0d0c 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2487,3 +2487,26 @@ class C(Generic[P, R]): c: C[int, str] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int" reveal_type(c.fn) # N: Revealed type is "def (*Any, **Any)" [builtins fixtures/tuple.pyi] + +[case testSubtypingWithUnpackAndTuples] +# This test is convoluted as there is special casing to get past. +# (for instance, passing a keyword argument is necessary) +import operator +from collections.abc import Callable +from typing_extensions import TypeVarTuple, Unpack, ParamSpec + +Ts = TypeVarTuple("Ts") +P = ParamSpec("P") + +def run(func: Callable[[Unpack[Ts]], None], *args: Unpack[Ts], some_kwarg: None) -> None: + return func(*args) + +def call(func: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: + return func(*args, **kwargs) + +def foo() -> None: + return None + +run(foo, some_kwarg=None) +call(run, foo, some_kwarg=None) +[builtins fixtures/tuple.pyi]