Skip to content

Commit 79e9c78

Browse files
authored
Fix narrowing for unions (#20728)
Previously we considered the else branch unreachable in the `testNarrowingAnyUnion` test case. It's also nice that the new code is more obviously correct Fixes #20330 This will help with landing #20727 as well
1 parent 29a6f68 commit 79e9c78

4 files changed

Lines changed: 84 additions & 15 deletions

File tree

mypy/checker.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8305,24 +8305,29 @@ def conditional_types(
83058305
enum_name = target.fallback.type.fullname
83068306
current_type = try_expanding_sum_type_to_union(current_type, enum_name)
83078307

8308+
proposed_type: Type
8309+
remaining_type: Type
8310+
83088311
proper_type = get_proper_type(current_type)
83098312
# factorize over union types: isinstance(A|B, C) -> yes = A_yes | B_yes
83108313
if isinstance(proper_type, UnionType):
8311-
result: list[tuple[Type | None, Type | None]] = [
8312-
conditional_types(
8314+
yes_items: list[Type] = []
8315+
no_items: list[Type] = []
8316+
for union_item in proper_type.items:
8317+
yes_type, no_type = conditional_types(
83138318
union_item,
83148319
proposed_type_ranges,
83158320
default=union_item,
83168321
consider_runtime_isinstance=consider_runtime_isinstance,
83178322
)
8318-
for union_item in get_proper_types(proper_type.items)
8319-
]
8320-
# separate list of tuples into two lists
8321-
yes_types, no_types = zip(*result)
8322-
proposed_type = make_simplified_union([t for t in yes_types if t is not None])
8323-
else:
8324-
proposed_items = [type_range.item for type_range in proposed_type_ranges]
8325-
proposed_type = make_simplified_union(proposed_items)
8323+
yes_items.append(yes_type)
8324+
no_items.append(no_type)
8325+
8326+
proposed_type = make_simplified_union(yes_items)
8327+
remaining_type = make_simplified_union(no_items)
8328+
return proposed_type, remaining_type
8329+
8330+
proposed_type = make_simplified_union([type_range.item for type_range in proposed_type_ranges])
83268331

83278332
if isinstance(proper_type, AnyType):
83288333
return proposed_type, current_type

test-data/unit/check-isinstance.test

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,20 +1407,21 @@ def f(x: Union[A, B]) -> None:
14071407
# flags: --warn-unreachable
14081408
from typing import Union
14091409

1410-
class FloatLike: pass
1411-
class IntLike(FloatLike): pass
1412-
14131410
def f1(x: Union[float, int]) -> None:
14141411
# We ignore promotions in isinstance checks
14151412
if isinstance(x, float):
14161413
reveal_type(x) # N: Revealed type is "builtins.float"
14171414
else:
14181415
reveal_type(x) # N: Revealed type is "builtins.int"
14191416

1417+
class FloatLike: pass
1418+
class IntLike(FloatLike): pass
1419+
14201420
def f2(x: Union[FloatLike, IntLike]) -> None:
1421-
# ...but not regular subtyping relationships
14221421
if isinstance(x, FloatLike):
1423-
reveal_type(x) # N: Revealed type is "__main__.FloatLike | __main__.IntLike"
1422+
reveal_type(x) # N: Revealed type is "__main__.FloatLike"
1423+
else:
1424+
reveal_type(x) # E: Statement is unreachable
14241425
[builtins fixtures/isinstance.pyi]
14251426

14261427
[case testIsinstanceOfSuperclass]

test-data/unit/check-narrowing.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,21 @@ else:
16521652
reveal_type(t) # N: Revealed type is "Any"
16531653
[builtins fixtures/isinstancelist.pyi]
16541654

1655+
[case testNarrowingAnyUnion]
1656+
# flags: --strict-equality --warn-unreachable
1657+
from __future__ import annotations
1658+
from unknown import A, AA # E: Cannot find implementation or library stub for module named "unknown" \
1659+
# N: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
1660+
1661+
class B: ...
1662+
1663+
def f(x: A | B) -> None:
1664+
if isinstance(x, (AA, B)):
1665+
reveal_type(x) # N: Revealed type is "Any | __main__.B"
1666+
else:
1667+
reveal_type(x) # N: Revealed type is "Any"
1668+
[builtins fixtures/isinstancelist.pyi]
1669+
16551670
[case testNarrowingLenItemAndLenCompare]
16561671
# flags: --strict-equality --warn-unreachable
16571672
from typing import Any

test-data/unit/check-typeis.test

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,3 +950,51 @@ def main(x: Union[A, B]) -> None:
950950
else:
951951
reveal_type(x) # N: Revealed type is "__main__.B"
952952
[builtins fixtures/tuple.pyi]
953+
954+
955+
[case testTypeIsGeneric]
956+
from __future__ import annotations
957+
from typing import Any, Callable, TypeVar
958+
from typing_extensions import TypeIs
959+
960+
T = TypeVar('T')
961+
962+
def positive(x: T | int, checker: Callable[[Any], TypeIs[T]], default: int) -> int:
963+
if checker(x):
964+
reveal_type(x) # N: Revealed type is "T`-1"
965+
return default
966+
else:
967+
reveal_type(x) # N: Revealed type is "builtins.int"
968+
return x
969+
970+
def negative(x: T | int, checker: Callable[[Any], TypeIs[int]], default: T) -> T:
971+
if checker(x):
972+
reveal_type(x) # N: Revealed type is "builtins.int"
973+
return default
974+
else:
975+
reveal_type(x) # N: Revealed type is "T`-1"
976+
return x
977+
[builtins fixtures/tuple.pyi]
978+
979+
980+
[case testTypeIsAwaitable]
981+
from __future__ import annotations
982+
from typing import Any, Awaitable, Callable, TypeVar, overload
983+
from typing_extensions import TypeIs
984+
985+
T = TypeVar("T")
986+
AwaitableCallable = Callable[..., Awaitable[T]]
987+
988+
@overload
989+
def is_async_callable(obj: AwaitableCallable[T]) -> TypeIs[AwaitableCallable[T]]: ...
990+
@overload
991+
def is_async_callable(obj: Any) -> TypeIs[AwaitableCallable[Any]]: ...
992+
def is_async_callable(obj): ...
993+
994+
def main(f: Callable[[], int | Awaitable[int]] | Callable[[], Awaitable[int]]) -> None:
995+
if is_async_callable(f):
996+
reveal_type(f) # N: Revealed type is "(def (*Any, **Any) -> typing.Awaitable[Any]) | (def () -> typing.Awaitable[builtins.int])"
997+
else:
998+
reveal_type(f) # N: Revealed type is "def () -> builtins.int | typing.Awaitable[builtins.int]"
999+
1000+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)