Skip to content

Commit 922cbf6

Browse files
committed
Fix TypeIs negative narrowing of union of generics
1 parent 499adae commit 922cbf6

File tree

3 files changed

+60
-11
lines changed

3 files changed

+60
-11
lines changed

mypy/checker.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6009,6 +6009,7 @@ def find_isinstance_check_helper(
60096009
self.lookup_type(expr),
60106010
[TypeRange(node.callee.type_is, is_upper_bound=False)],
60116011
expr,
6012+
consider_runtime_isinstance=False,
60126013
),
60136014
)
60146015
elif isinstance(node, ComparisonExpr):
@@ -7419,11 +7420,19 @@ def conditional_types_with_intersection(
74197420
type_ranges: list[TypeRange] | None,
74207421
ctx: Context,
74217422
default: None = None,
7423+
*,
7424+
consider_runtime_isinstance: bool = True,
74227425
) -> tuple[Type | None, Type | None]: ...
74237426

74247427
@overload
74257428
def conditional_types_with_intersection(
7426-
self, expr_type: Type, type_ranges: list[TypeRange] | None, ctx: Context, default: Type
7429+
self,
7430+
expr_type: Type,
7431+
type_ranges: list[TypeRange] | None,
7432+
ctx: Context,
7433+
default: Type,
7434+
*,
7435+
consider_runtime_isinstance: bool = True,
74277436
) -> tuple[Type, Type]: ...
74287437

74297438
def conditional_types_with_intersection(
@@ -7432,8 +7441,15 @@ def conditional_types_with_intersection(
74327441
type_ranges: list[TypeRange] | None,
74337442
ctx: Context,
74347443
default: Type | None = None,
7444+
*,
7445+
consider_runtime_isinstance: bool = True,
74357446
) -> tuple[Type | None, Type | None]:
7436-
initial_types = conditional_types(expr_type, type_ranges, default)
7447+
initial_types = conditional_types(
7448+
expr_type,
7449+
type_ranges,
7450+
default,
7451+
consider_runtime_isinstance=consider_runtime_isinstance,
7452+
)
74377453
# For some reason, doing "yes_map, no_map = conditional_types_to_typemaps(...)"
74387454
# doesn't work: mypyc will decide that 'yes_map' is of type None if we try.
74397455
yes_type: Type | None = initial_types[0]
@@ -7712,18 +7728,30 @@ def visit_type_var(self, t: TypeVarType) -> None:
77127728

77137729
@overload
77147730
def conditional_types(
7715-
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: None = None
7731+
current_type: Type,
7732+
proposed_type_ranges: list[TypeRange] | None,
7733+
default: None = None,
7734+
*,
7735+
consider_runtime_isinstance: bool = True,
77167736
) -> tuple[Type | None, Type | None]: ...
77177737

77187738

77197739
@overload
77207740
def conditional_types(
7721-
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type
7741+
current_type: Type,
7742+
proposed_type_ranges: list[TypeRange] | None,
7743+
default: Type,
7744+
*,
7745+
consider_runtime_isinstance: bool = True,
77227746
) -> tuple[Type, Type]: ...
77237747

77247748

77257749
def conditional_types(
7726-
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type | None = None
7750+
current_type: Type,
7751+
proposed_type_ranges: list[TypeRange] | None,
7752+
default: Type | None = None,
7753+
*,
7754+
consider_runtime_isinstance: bool = True,
77277755
) -> tuple[Type | None, Type | None]:
77287756
"""Takes in the current type and a proposed type of an expression.
77297757
@@ -7765,7 +7793,11 @@ def conditional_types(
77657793
if not type_range.is_upper_bound
77667794
]
77677795
)
7768-
remaining_type = restrict_subtype_away(current_type, proposed_precise_type)
7796+
remaining_type = restrict_subtype_away(
7797+
current_type,
7798+
proposed_precise_type,
7799+
consider_runtime_isinstance=consider_runtime_isinstance,
7800+
)
77697801
return proposed_type, remaining_type
77707802
else:
77717803
# An isinstance check, but we don't understand the type

mypy/subtypes.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,7 +1925,7 @@ def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None:
19251925
return new_items
19261926

19271927

1928-
def restrict_subtype_away(t: Type, s: Type) -> Type:
1928+
def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool = True) -> Type:
19291929
"""Return t minus s for runtime type assertions.
19301930
19311931
If we can't determine a precise result, return a supertype of the
@@ -1939,14 +1939,19 @@ def restrict_subtype_away(t: Type, s: Type) -> Type:
19391939
new_items = try_restrict_literal_union(p_t, s)
19401940
if new_items is None:
19411941
new_items = [
1942-
restrict_subtype_away(item, s)
1942+
restrict_subtype_away(
1943+
item, s, consider_runtime_isinstance=consider_runtime_isinstance
1944+
)
19431945
for item in p_t.relevant_items()
1944-
if (isinstance(get_proper_type(item), AnyType) or not covers_at_runtime(item, s))
19451946
]
1946-
return UnionType.make_union(new_items)
1947+
return UnionType.make_union(
1948+
[item for item in new_items if not isinstance(get_proper_type(item), UninhabitedType)]
1949+
)
19471950
elif isinstance(p_t, TypeVarType):
19481951
return p_t.copy_modified(upper_bound=restrict_subtype_away(p_t.upper_bound, s))
1949-
elif covers_at_runtime(t, s):
1952+
elif consider_runtime_isinstance and covers_at_runtime(t, s):
1953+
return UninhabitedType()
1954+
elif is_proper_subtype(t, s, ignore_promotions=True):
19501955
return UninhabitedType()
19511956
else:
19521957
return t

test-data/unit/check-typeis.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ def main(a: object) -> None:
134134
reveal_type(a) # N: Revealed type is "Union[builtins.int, builtins.str]"
135135
[builtins fixtures/tuple.pyi]
136136

137+
[case testTypeIsUnionOfGenerics]
138+
from typing import List, Union
139+
from typing_extensions import TypeIs
140+
def is_foo(a: Union[List[int], List[str]]) -> TypeIs[List[str]]: pass
141+
def main(a: Union[List[int], List[str]]) -> None:
142+
if is_foo(a):
143+
reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]"
144+
else:
145+
reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]"
146+
reveal_type(a) # N: Revealed type is "Union[builtins.list[builtins.int], builtins.list[builtins.str]]"
147+
[builtins fixtures/tuple.pyi]
148+
137149
[case testTypeIsNonzeroFloat]
138150
from typing_extensions import TypeIs
139151
def is_nonzero(a: object) -> TypeIs[float]: pass

0 commit comments

Comments
 (0)