Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 22 additions & 15 deletions mypy/checkpattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,21 +358,28 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType:
new_type = UninhabitedType()
else:
new_type = TupleType(new_inner_types, current_type.partial_fallback)

num_always_match = sum(is_uninhabited(typ) for typ in rest_inner_types)
if num_always_match == len(rest_inner_types):
# All subpatterns always match, so we can apply negative narrowing
rest_type = UninhabitedType()
elif num_always_match == len(rest_inner_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, rest_inner_types)
],
current_type.partial_fallback,
)
# A value doesn't match the pattern if some item doesn't match its
# subpattern while all the items before it do, so what remains is the
# union of one tuple per position whose subpattern may fail to match:
# tuple[A, B] - tuple[a, b] == tuple[A - a, B] | tuple[a, B - b]
# Positions whose subpattern always matches never fail and keep their type.
matched_types = [
curr if is_uninhabited(rest) else new
for curr, new, rest in zip(inner_types, new_inner_types, rest_inner_types)
]
rest_items = [
TupleType(
matched_types[:i] + [rest] + inner_types[i + 1 :],
current_type.partial_fallback,
)
for i, rest in enumerate(rest_inner_types)
if not is_uninhabited(rest)
]
if rest_items:
rest_type = make_simplified_union(rest_items)
else:
# All subpatterns always match, so nothing remains
rest_type = UninhabitedType()
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
87 changes: 86 additions & 1 deletion test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -2083,10 +2083,95 @@ match m6:
case (1, "a"):
reveal_type(m6) # N: Revealed type is "tuple[Literal[1], Literal['a']]"
case _:
reveal_type(m6) # N: Revealed type is "tuple[Literal[1] | Literal[2], Literal['a'] | Literal['b']]"
reveal_type(m6) # N: Revealed type is "tuple[Literal[2], Literal['a'] | Literal['b']] | tuple[Literal[1], Literal['b']]"

[builtins fixtures/tuple.pyi]

[case testMatchSequencePatternNegativeNarrowingMultiplePositions]
# flags: --strict-equality --warn-unreachable
from typing import Literal, Tuple

m1: Tuple[bool, bool]

match m1:
case (True, True):
reveal_type(m1) # N: Revealed type is "tuple[Literal[True], Literal[True]]"
case (True, False):
reveal_type(m1) # N: Revealed type is "tuple[Literal[True], Literal[False]]"
case (False, True):
reveal_type(m1) # N: Revealed type is "tuple[Literal[False], Literal[True]]"
case _:
reveal_type(m1) # N: Revealed type is "tuple[Literal[False], Literal[False]]"

m2: Tuple[bool, int, bool]

match m2:
case (True, _, True):
reveal_type(m2) # N: Revealed type is "tuple[Literal[True], builtins.int, Literal[True]]"
case _:
reveal_type(m2) # N: Revealed type is "tuple[Literal[False], builtins.int, builtins.bool] | tuple[Literal[True], builtins.int, Literal[False]]"

m3: Tuple[Literal[1], Literal[2]]

match m3:
case (1, 3):
reveal_type(m3) # E: Statement is unreachable
case _:
reveal_type(m3) # N: Revealed type is "tuple[Literal[1], Literal[2]]"

def f(a: bool, b: bool) -> None:
match a, b:
case True, True:
reveal_type(a) # N: Revealed type is "Literal[True]"
reveal_type(b) # N: Revealed type is "Literal[True]"
case True, False:
reveal_type(a) # N: Revealed type is "Literal[True]"
reveal_type(b) # N: Revealed type is "Literal[False]"
case False, _:
reveal_type(a) # N: Revealed type is "Literal[False]"
reveal_type(b) # N: Revealed type is "builtins.bool"
case _:
reveal_type(a) # E: Statement is unreachable
[builtins fixtures/tuple.pyi]

[case testMatchExhaustiveTupleOfBoolsAndEnums]
# flags: --enable-error-code exhaustive-match
from enum import Enum
from typing import Tuple

class Color(Enum):
RED = 1
BLUE = 2

def f1(a: bool, b: bool) -> float:
match a, b:
case True, True:
return 1
case True, False:
return 0.9
case False, True:
return 0.5
case False, False:
return 0

def f2(t: Tuple[Color, Color]) -> int:
match t:
case (Color.RED, Color.RED):
return 1
case (Color.RED, Color.BLUE):
return 2
case (Color.BLUE, _):
return 3

def f3(a: bool, b: bool) -> int: # E: Missing return statement
match a, b: # E: Match statement has unhandled case for values of type "tuple[Literal[False], Literal[False]]" \
# N: If match statement is intended to be non-exhaustive, add `case _: pass`
case True, _:
return 1
case False, True:
return 2
[builtins fixtures/tuple.pyi]

[case testMatchSequenceWildcardRefutability]
# flags: --strict-equality --warn-unreachable
def f1(x: int | list[int]):
Expand Down
Loading