Skip to content

Commit fb16e93

Browse files
[match-case] Fix narrowing of class pattern with union-argument. (#19517)
Fixes #19468 Based on earlier PR #19473 - refactored the `conditional_types` function. - return `UninhabitedType(), default` when no ranges are given. This corresponds to `isinstance(x, ())`, with an empty tuple, which always returns `False` at runtime. - modified #19473 to change the proposed type, rather than directly returning. This is essential to maintain the behavior of the unit test `testIsinstanceWithOverlappingPromotionTypes` - Added special casing in `restrict_subtype_away`: if the second argument is a `TypeVar`, replace it with its upper bound (crucial to get correct result in `testNarrowSelfType`) - Allow `TypeChecker.get_isinstance_type` to return empty list (fixes `isinstance(x, ())` behavior). ## Modified tests - `testIsInstanceWithEmtpy2ndArg` now correctly infers unreachable for `isinstance(x, ())`. - `testNarrowingUnionMixins` now predicts the same results as pyright playground ## New Tests - `testMatchNarrowDownUnionUsingClassPattern` (https://mypy-play.net/?mypy=1.17.0&python=3.12&gist=e9ec514f49903022bd32a82ae1774abd)
1 parent cb0da62 commit fb16e93

File tree

4 files changed

+95
-56
lines changed

4 files changed

+95
-56
lines changed

mypy/checker.py

Lines changed: 76 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7905,6 +7905,10 @@ def is_writable_attribute(self, node: Node) -> bool:
79057905
return False
79067906

79077907
def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
7908+
"""Get the type(s) resulting from an isinstance check.
7909+
7910+
Returns an empty list for isinstance(x, ()).
7911+
"""
79087912
if isinstance(expr, OpExpr) and expr.op == "|":
79097913
left = self.get_isinstance_type(expr.left)
79107914
if left is None and is_literal_none(expr.left):
@@ -7944,11 +7948,6 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
79447948
types.append(TypeRange(typ, is_upper_bound=False))
79457949
else: # we didn't see an actual type, but rather a variable with unknown value
79467950
return None
7947-
if not types:
7948-
# this can happen if someone has empty tuple as 2nd argument to isinstance
7949-
# strictly speaking, we should return UninhabitedType but for simplicity we will simply
7950-
# refuse to do any type inference for now
7951-
return None
79527951
return types
79537952

79547953
def is_literal_enum(self, n: Expression) -> bool:
@@ -8185,59 +8184,82 @@ def conditional_types(
81858184
UninhabitedType means unreachable.
81868185
None means no new information can be inferred.
81878186
"""
8188-
if proposed_type_ranges:
8189-
if len(proposed_type_ranges) == 1:
8190-
target = proposed_type_ranges[0].item
8191-
target = get_proper_type(target)
8192-
if isinstance(target, LiteralType) and (
8193-
target.is_enum_literal() or isinstance(target.value, bool)
8194-
):
8195-
enum_name = target.fallback.type.fullname
8196-
current_type = try_expanding_sum_type_to_union(current_type, enum_name)
8197-
proposed_items = [type_range.item for type_range in proposed_type_ranges]
8198-
proposed_type = make_simplified_union(proposed_items)
8199-
if isinstance(get_proper_type(current_type), AnyType):
8200-
return proposed_type, current_type
8201-
elif isinstance(proposed_type, AnyType):
8202-
# We don't really know much about the proposed type, so we shouldn't
8203-
# attempt to narrow anything. Instead, we broaden the expr to Any to
8204-
# avoid false positives
8205-
return proposed_type, default
8206-
elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and (
8207-
# concrete subtypes
8208-
is_proper_subtype(current_type, proposed_type, ignore_promotions=True)
8209-
# structural subtypes
8210-
or (
8211-
(
8212-
isinstance(proposed_type, CallableType)
8213-
or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol)
8214-
)
8215-
and is_subtype(current_type, proposed_type, ignore_promotions=True)
8216-
)
8187+
if proposed_type_ranges is None:
8188+
# An isinstance check, but we don't understand the type
8189+
return current_type, default
8190+
8191+
if not proposed_type_ranges:
8192+
# This is the case for `if isinstance(x, ())` which always returns False.
8193+
return UninhabitedType(), default
8194+
8195+
if len(proposed_type_ranges) == 1:
8196+
# expand e.g. bool -> Literal[True] | Literal[False]
8197+
target = proposed_type_ranges[0].item
8198+
target = get_proper_type(target)
8199+
if isinstance(target, LiteralType) and (
8200+
target.is_enum_literal() or isinstance(target.value, bool)
82178201
):
8218-
# Expression is always of one of the types in proposed_type_ranges
8219-
return default, UninhabitedType()
8220-
elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
8221-
# Expression is never of any type in proposed_type_ranges
8222-
return UninhabitedType(), default
8223-
else:
8224-
# we can only restrict when the type is precise, not bounded
8225-
proposed_precise_type = UnionType.make_union(
8226-
[
8227-
type_range.item
8228-
for type_range in proposed_type_ranges
8229-
if not type_range.is_upper_bound
8230-
]
8231-
)
8232-
remaining_type = restrict_subtype_away(
8233-
current_type,
8234-
proposed_precise_type,
8202+
enum_name = target.fallback.type.fullname
8203+
current_type = try_expanding_sum_type_to_union(current_type, enum_name)
8204+
8205+
proper_type = get_proper_type(current_type)
8206+
# factorize over union types: isinstance(A|B, C) -> yes = A_yes | B_yes
8207+
if isinstance(proper_type, UnionType):
8208+
result: list[tuple[Type | None, Type | None]] = [
8209+
conditional_types(
8210+
union_item,
8211+
proposed_type_ranges,
8212+
default=union_item,
82358213
consider_runtime_isinstance=consider_runtime_isinstance,
82368214
)
8237-
return proposed_type, remaining_type
8215+
for union_item in get_proper_types(proper_type.items)
8216+
]
8217+
# separate list of tuples into two lists
8218+
yes_types, no_types = zip(*result)
8219+
proposed_type = make_simplified_union([t for t in yes_types if t is not None])
82388220
else:
8239-
# An isinstance check, but we don't understand the type
8240-
return current_type, default
8221+
proposed_items = [type_range.item for type_range in proposed_type_ranges]
8222+
proposed_type = make_simplified_union(proposed_items)
8223+
8224+
if isinstance(proper_type, AnyType):
8225+
return proposed_type, current_type
8226+
elif isinstance(proposed_type, AnyType):
8227+
# We don't really know much about the proposed type, so we shouldn't
8228+
# attempt to narrow anything. Instead, we broaden the expr to Any to
8229+
# avoid false positives
8230+
return proposed_type, default
8231+
elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and (
8232+
# concrete subtypes
8233+
is_proper_subtype(current_type, proposed_type, ignore_promotions=True)
8234+
# structural subtypes
8235+
or (
8236+
(
8237+
isinstance(proposed_type, CallableType)
8238+
or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol)
8239+
)
8240+
and is_subtype(current_type, proposed_type, ignore_promotions=True)
8241+
)
8242+
):
8243+
# Expression is always of one of the types in proposed_type_ranges
8244+
return default, UninhabitedType()
8245+
elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
8246+
# Expression is never of any type in proposed_type_ranges
8247+
return UninhabitedType(), default
8248+
else:
8249+
# we can only restrict when the type is precise, not bounded
8250+
proposed_precise_type = UnionType.make_union(
8251+
[
8252+
type_range.item
8253+
for type_range in proposed_type_ranges
8254+
if not type_range.is_upper_bound
8255+
]
8256+
)
8257+
remaining_type = restrict_subtype_away(
8258+
current_type,
8259+
proposed_precise_type,
8260+
consider_runtime_isinstance=consider_runtime_isinstance,
8261+
)
8262+
return proposed_type, remaining_type
82418263

82428264

82438265
def conditional_types_to_typemaps(

test-data/unit/check-isinstance.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1483,11 +1483,12 @@ def f(x: Union[int, A], a: Type[A]) -> None:
14831483
[builtins fixtures/isinstancelist.pyi]
14841484

14851485
[case testIsInstanceWithEmtpy2ndArg]
1486+
# flags: --warn-unreachable
14861487
from typing import Union
14871488

14881489
def f(x: Union[int, str]) -> None:
14891490
if isinstance(x, ()):
1490-
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
1491+
reveal_type(x) # E: Statement is unreachable
14911492
else:
14921493
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
14931494
[builtins fixtures/isinstancelist.pyi]

test-data/unit/check-narrowing.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2639,7 +2639,7 @@ def baz(item: Base) -> None:
26392639

26402640
reveal_type(item) # N: Revealed type is "Union[__main__.<subclass of "__main__.Base" and "__main__.FooMixin">, __main__.<subclass of "__main__.Base" and "__main__.BarMixin">]"
26412641
if isinstance(item, FooMixin):
2642-
reveal_type(item) # N: Revealed type is "__main__.FooMixin"
2642+
reveal_type(item) # N: Revealed type is "__main__.<subclass of "__main__.Base" and "__main__.FooMixin">"
26432643
item.foo()
26442644
else:
26452645
reveal_type(item) # N: Revealed type is "__main__.<subclass of "__main__.Base" and "__main__.BarMixin">"

test-data/unit/check-python310.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,22 @@ def union(x: str | bool) -> None:
19361936
reveal_type(x) # N: Revealed type is "Union[builtins.str, Literal[False]]"
19371937
[builtins fixtures/tuple.pyi]
19381938

1939+
[case testMatchNarrowDownUnionUsingClassPattern]
1940+
1941+
class Foo: ...
1942+
class Bar(Foo): ...
1943+
1944+
def test_1(bar: Bar) -> None:
1945+
match bar:
1946+
case Foo() as foo:
1947+
reveal_type(foo) # N: Revealed type is "__main__.Bar"
1948+
1949+
def test_2(bar: Bar | str) -> None:
1950+
match bar:
1951+
case Foo() as foo:
1952+
reveal_type(foo) # N: Revealed type is "__main__.Bar"
1953+
1954+
19391955
[case testMatchAssertFalseToSilenceFalsePositives]
19401956
class C:
19411957
a: int | str

0 commit comments

Comments
 (0)