From 6f0c0b53216e1beba8e80a2664c0dfe02692bf52 Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Fri, 27 Sep 2024 11:14:13 -0400 Subject: [PATCH 1/6] Fix subtyping for `def f(*a: Unpack[tuple[T, ...]])` --- mypy/subtypes.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index c76b3569fdd4..73a9c7987289 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) @@ -1644,7 +1655,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N 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 trivial_varargs return not is_compat(right_arg.typ, left_arg.typ) if _incompatible(left_star, right_star) or _incompatible(left_star2, right_star2): @@ -1672,8 +1683,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 From 72802dd8dc55fea19828ddd218101468d03ddf0a Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Fri, 27 Sep 2024 11:27:14 -0400 Subject: [PATCH 2/6] Add a test --- test-data/unit/check-typevar-tuple.test | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index f49e1b3c6613..7671ccde5738 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2487,3 +2487,22 @@ 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 + +Ts = TypeVarTuple("Ts") + +def run(func: Callable[[Unpack[Ts]], None], *args: Unpack[Ts], some_kwarg: None) -> None: + return func(*args) + +def foo() -> None: + raise NotImplementedError + +run(foo, some_kwarg=None) +operator.call(run, foo, some_kwarg=None) +[builtins fixtures/tuple.pyi] \ No newline at end of file From f6c10b1d7e6d2099ecebfff58a571534fc5523d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:29:51 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-data/unit/check-typevar-tuple.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 7671ccde5738..dcee8b4c92f7 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2505,4 +2505,4 @@ def foo() -> None: run(foo, some_kwarg=None) operator.call(run, foo, some_kwarg=None) -[builtins fixtures/tuple.pyi] \ No newline at end of file +[builtins fixtures/tuple.pyi] From af52b83fa92a87ed384045b1ef7974b8d2863f36 Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Fri, 27 Sep 2024 11:42:09 -0400 Subject: [PATCH 4/6] Tighten conditions --- mypy/subtypes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 73a9c7987289..a7d5650f40c6 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1651,14 +1651,14 @@ 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 and not trivial_varargs + 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 From a0613c031b459eb8d1b0b059956a5735ba5ede69 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:43:19 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/subtypes.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a7d5650f40c6..e08620c0c985 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1651,11 +1651,17 @@ 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, varargs: bool) -> 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 and (not varargs or not trivial_varargs) + 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, True) or _incompatible(left_star2, right_star2, False): From 8711684bc47694f25620bee0c97e5fa424d2bc4d Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Fri, 27 Sep 2024 12:04:05 -0400 Subject: [PATCH 6/6] Update test --- test-data/unit/check-typevar-tuple.test | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index dcee8b4c92f7..2c0d3d9e0d0c 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2493,16 +2493,20 @@ reveal_type(c.fn) # N: Revealed type is "def (*Any, **Any)" # (for instance, passing a keyword argument is necessary) import operator from collections.abc import Callable -from typing_extensions import TypeVarTuple, Unpack +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: - raise NotImplementedError + return None run(foo, some_kwarg=None) -operator.call(run, foo, some_kwarg=None) +call(run, foo, some_kwarg=None) [builtins fixtures/tuple.pyi]