Skip to content

Commit

Permalink
Fix negative narrowing of tuples in match statement (#17817)
Browse files Browse the repository at this point in the history
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']]]"
```
  • Loading branch information
brianschubert authored Sep 27, 2024
1 parent 7237d55 commit 1c01858
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 1 deletion.
12 changes: 11 additions & 1 deletion mypy/checkpattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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]
Expand Down

0 comments on commit 1c01858

Please sign in to comment.