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
58 changes: 57 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8331,7 +8331,18 @@ def conditional_types_with_intersection(
yes_type: Type | None = initial_types[0]
no_type: Type | None = initial_types[1]

if not isinstance(get_proper_type(yes_type), UninhabitedType) or type_ranges is None:
if type_ranges is None:
return yes_type, no_type

if not isinstance(get_proper_type(yes_type), UninhabitedType):
if yes_type is not None and len(type_ranges) > 1:
# Some items of a tuple of types (e.g. isinstance(x, (B, C))) may not
# overlap with expr_type at all, but a common subclass of expr_type
# and such an item could still match at runtime. Add ad-hoc
# intersections for those items, so that they are not dropped.
yes_type = self.add_intersections_for_disjoint_ranges(
expr_type, yes_type, type_ranges, consider_runtime_isinstance
)
return yes_type, no_type

# If conditional_types was unable to successfully narrow the expr_type
Expand Down Expand Up @@ -8374,6 +8385,51 @@ def conditional_types_with_intersection(
new_yes_type = make_simplified_union(out)
return new_yes_type, expr_type

def add_intersections_for_disjoint_ranges(
self,
expr_type: Type,
yes_type: Type,
type_ranges: list[TypeRange],
consider_runtime_isinstance: bool,
) -> Type:
"""Extend yes_type with intersections for type ranges disjoint from expr_type.

This is used when narrowing to a tuple of types where some, but not all,
of the types overlap with expr_type.
"""
proper_type = get_proper_type(expr_type)
if isinstance(proper_type, UnionType):
possible_expr_types = get_proper_types(proper_type.relevant_items())
else:
possible_expr_types = [proper_type]
if not all(isinstance(v, Instance) for v in possible_expr_types):
return yes_type

items: list[Type] = []
found_disjoint = False
for tr in type_ranges:
single_yes, _ = conditional_types(
expr_type,
[tr],
default=expr_type,
consider_runtime_isinstance=consider_runtime_isinstance,
)
target = get_proper_type(tr.item)
if not isinstance(get_proper_type(single_yes), UninhabitedType) or not isinstance(
target, Instance
):
items.append(single_yes)
continue
found_disjoint = True
for v in possible_expr_types:
assert isinstance(v, Instance)
intersection = self.intersect_instances((v, target), [])
if intersection is not None:
items.append(intersection)
if not found_disjoint:
return yes_type
return make_simplified_union(items)

def is_writable_attribute(self, node: Node) -> bool:
"""Check if an attribute is writable"""
if isinstance(node, Var):
Expand Down
41 changes: 40 additions & 1 deletion test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -2617,7 +2617,7 @@ def f(var: Union[int, str]) -> None:
some_types = (str, tuple)
another_type = list
if isinstance(var, (*some_types, another_type)):
reveal_type(var) # N: Revealed type is "builtins.str"
reveal_type(var) # N: Revealed type is "__main__.<subclass of "builtins.int" and "tuple[Any, ...]"> | builtins.str"
[builtins fixtures/isinstancelist.pyi]

[case testIsInstanceWithWrongStarExpression]
Expand Down Expand Up @@ -2926,6 +2926,45 @@ if isinstance(v3, (C, D)):
reveal_type(v3) # N: Revealed type is "__main__.<subclass of "__main__.A" and "__main__.C"> | __main__.<subclass of "__main__.A" and "__main__.D"> | __main__.<subclass of "__main__.B" and "__main__.C"> | __main__.<subclass of "__main__.B" and "__main__.D">"
[builtins fixtures/isinstance.pyi]

[case testIsInstanceAdHocIntersectionTuplePartialOverlap]
# flags: --warn-unreachable
from typing import Union
class A: pass
class B(A): pass
class C: pass
class D(A, C): pass

def g(x: Union[B, C]) -> None: ...

def f(x: A) -> None:
if isinstance(x, (B, C)):
reveal_type(x) # N: Revealed type is "__main__.B | __main__.<subclass of "__main__.A" and "__main__.C">"
g(x)
else:
reveal_type(x) # N: Revealed type is "__main__.A"
if isinstance(x, (C, B)):
reveal_type(x) # N: Revealed type is "__main__.<subclass of "__main__.A" and "__main__.C"> | __main__.B"
if isinstance(x, (B, D)):
reveal_type(x) # N: Revealed type is "__main__.B | __main__.D"

def h(x: Union[A, int]) -> None:
if isinstance(x, (B, C)):
reveal_type(x) # N: Revealed type is "__main__.B | __main__.<subclass of "__main__.A" and "__main__.C"> | __main__.<subclass of "builtins.int" and "__main__.C">"
[builtins fixtures/isinstance.pyi]

[case testIsInstanceAdHocIntersectionTuplePartialOverlapFinal]
# flags: --warn-unreachable
from typing import final
class A: pass
class B(A): pass
@final
class C: pass

def f(x: A) -> None:
if isinstance(x, (B, C)):
reveal_type(x) # N: Revealed type is "__main__.B"
[builtins fixtures/isinstance.pyi]

[case testIsInstanceAdHocIntersectionSameNames]
# flags: --warn-unreachable
from foo import A as A2
Expand Down
Loading