From 1c018589a621e38e4851a64f500a42ebe99ef21d Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Fri, 27 Sep 2024 02:32:59 -0400 Subject: [PATCH] Fix negative narrowing of tuples in match statement (#17817) Fixes #17328 ### Before Lines marked with `!!!` denote incorrect behavior. ([Playground link](https://mypy-play.net/?mypy=1.11.2&python=3.12&flags=strict%2Cwarn-unreachable&gist=7a7081c5fbc2fac9987f24e02421f24f)) ```python from typing import Literal m4: tuple[Literal[1], int] match m4: case (1, 5): reveal_type(m4) # N: Revealed type is "tuple[Literal[1], Literal[5]]" case (1, 6): reveal_type(m4) # !!! E: Statement is unreachable [unreachable] case _: reveal_type(m4) # !!! N: Revealed type is "tuple[Never, builtins.int]" m5: tuple[Literal[1, 2], Literal["a", "b"]] match m5: case (1, str()): reveal_type(m5) # N: Revealed type is "tuple[Literal[1], Union[Literal['a'], Literal['b']]]" case _: reveal_type(m5) # !!! N: Revealed type is "tuple[Literal[2], Never]" match m5: case (1, "a"): reveal_type(m5) # N: Revealed type is "tuple[Literal[1], Literal['a']]" case _: reveal_type(m5) # !!! N: Revealed type is "tuple[Literal[2], Literal['b']]" ``` ### After ```python from typing import Literal m4: tuple[Literal[1], int] match m4: case (1, 5): reveal_type(m4) # N: Revealed type is "tuple[Literal[1], Literal[5]]" case (1, 6): reveal_type(m4) # N: Revealed type is "tuple[Literal[1], Literal[6]]" case _: reveal_type(m4) # N: Revealed type is "tuple[Literal[1], builtins.int]" m5: tuple[Literal[1, 2], Literal["a", "b"]] match m5: case (1, str()): reveal_type(m5) # N: Revealed type is "tuple[Literal[1], Union[Literal['a'], Literal['b']]]" case _: reveal_type(m5) # N: Revealed type is "tuple[Literal[2], Union[Literal['a'], Literal['b']]]" match m5: case (1, "a"): reveal_type(m5) # N: Revealed type is "tuple[Literal[1], Literal['a']]" case _: reveal_type(m5) # N: Revealed type is "tuple[Union[Literal[1], Literal[2]], Union[Literal['a'], Literal['b']]]" ``` --- mypy/checkpattern.py | 12 +++++++++++- test-data/unit/check-python310.test | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index a23be464b825..cb3577ce2f6e 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -307,7 +307,7 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: for inner_type, new_inner_type in zip(inner_types, new_inner_types): (narrowed_inner_type, inner_rest_type) = ( self.chk.conditional_types_with_intersection( - new_inner_type, [get_type_range(inner_type)], o, default=new_inner_type + inner_type, [get_type_range(new_inner_type)], o, default=inner_type ) ) narrowed_inner_types.append(narrowed_inner_type) @@ -320,6 +320,16 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: if all(is_uninhabited(typ) for typ in inner_rest_types): # All subpatterns always match, so we can apply negative narrowing rest_type = TupleType(rest_inner_types, current_type.partial_fallback) + elif sum(not is_uninhabited(typ) for typ in inner_rest_types) == 1: + # Exactly one subpattern may conditionally match, the rest always match. + # We can apply negative narrowing to this one position. + rest_type = TupleType( + [ + curr if is_uninhabited(rest) else rest + for curr, rest in zip(inner_types, inner_rest_types) + ], + current_type.partial_fallback, + ) elif isinstance(current_type, TupleType): # For variadic tuples it is too tricky to match individual items like for fixed # tuples, so we instead try to narrow the entire type. diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 5ecc69dc7c32..e7028a027e25 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1424,6 +1424,7 @@ def f(value: Literal[1] | Literal[2]) -> int: [case testMatchSequencePatternNegativeNarrowing] from typing import Union, Sequence, Tuple +from typing_extensions import Literal m1: Sequence[int | str] @@ -1448,6 +1449,31 @@ match m3: reveal_type(m3) # N: Revealed type is "Tuple[Literal[1]]" case r2: reveal_type(m3) # N: Revealed type is "Tuple[Union[builtins.int, builtins.str]]" + +m4: Tuple[Literal[1], int] + +match m4: + case (1, 5): + reveal_type(m4) # N: Revealed type is "Tuple[Literal[1], Literal[5]]" + case (1, 6): + reveal_type(m4) # N: Revealed type is "Tuple[Literal[1], Literal[6]]" + case _: + reveal_type(m4) # N: Revealed type is "Tuple[Literal[1], builtins.int]" + +m5: Tuple[Literal[1, 2], Literal["a", "b"]] + +match m5: + case (1, str()): + reveal_type(m5) # N: Revealed type is "Tuple[Literal[1], Union[Literal['a'], Literal['b']]]" + case _: + reveal_type(m5) # N: Revealed type is "Tuple[Literal[2], Union[Literal['a'], Literal['b']]]" + +match m5: + case (1, "a"): + reveal_type(m5) # N: Revealed type is "Tuple[Literal[1], Literal['a']]" + case _: + reveal_type(m5) # N: Revealed type is "Tuple[Union[Literal[1], Literal[2]], Union[Literal['a'], Literal['b']]]" + [builtins fixtures/tuple.pyi] [case testMatchEnumSingleChoice]