Skip to content
Merged
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
130 changes: 76 additions & 54 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7905,6 +7905,10 @@ def is_writable_attribute(self, node: Node) -> bool:
return False

def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
"""Get the type(s) resulting from an isinstance check.

Returns an empty list for isinstance(x, ()).
"""
if isinstance(expr, OpExpr) and expr.op == "|":
left = self.get_isinstance_type(expr.left)
if left is None and is_literal_none(expr.left):
Expand Down Expand Up @@ -7944,11 +7948,6 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
types.append(TypeRange(typ, is_upper_bound=False))
else: # we didn't see an actual type, but rather a variable with unknown value
return None
if not types:
# this can happen if someone has empty tuple as 2nd argument to isinstance
# strictly speaking, we should return UninhabitedType but for simplicity we will simply
# refuse to do any type inference for now
return None
return types

def is_literal_enum(self, n: Expression) -> bool:
Expand Down Expand Up @@ -8185,59 +8184,82 @@ def conditional_types(
UninhabitedType means unreachable.
None means no new information can be inferred.
"""
if proposed_type_ranges:
if len(proposed_type_ranges) == 1:
target = proposed_type_ranges[0].item
target = get_proper_type(target)
if isinstance(target, LiteralType) and (
target.is_enum_literal() or isinstance(target.value, bool)
):
enum_name = target.fallback.type.fullname
current_type = try_expanding_sum_type_to_union(current_type, enum_name)
proposed_items = [type_range.item for type_range in proposed_type_ranges]
proposed_type = make_simplified_union(proposed_items)
if isinstance(get_proper_type(current_type), AnyType):
return proposed_type, current_type
elif isinstance(proposed_type, AnyType):
# We don't really know much about the proposed type, so we shouldn't
# attempt to narrow anything. Instead, we broaden the expr to Any to
# avoid false positives
return proposed_type, default
elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and (
# concrete subtypes
is_proper_subtype(current_type, proposed_type, ignore_promotions=True)
# structural subtypes
or (
(
isinstance(proposed_type, CallableType)
or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol)
)
and is_subtype(current_type, proposed_type, ignore_promotions=True)
)
if proposed_type_ranges is None:
# An isinstance check, but we don't understand the type
return current_type, default

if not proposed_type_ranges:
# This is the case for `if isinstance(x, ())` which always returns False.
return UninhabitedType(), default
Comment on lines +8191 to +8193
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is actually redundant, it works just as well if we comment it out since make_simplified_union([]) returns UninhabitedType().


if len(proposed_type_ranges) == 1:
# expand e.g. bool -> Literal[True] | Literal[False]
target = proposed_type_ranges[0].item
target = get_proper_type(target)
if isinstance(target, LiteralType) and (
target.is_enum_literal() or isinstance(target.value, bool)
):
# Expression is always of one of the types in proposed_type_ranges
return default, UninhabitedType()
elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
# Expression is never of any type in proposed_type_ranges
return UninhabitedType(), default
else:
# we can only restrict when the type is precise, not bounded
proposed_precise_type = UnionType.make_union(
[
type_range.item
for type_range in proposed_type_ranges
if not type_range.is_upper_bound
]
)
remaining_type = restrict_subtype_away(
current_type,
proposed_precise_type,
enum_name = target.fallback.type.fullname
current_type = try_expanding_sum_type_to_union(current_type, enum_name)

proper_type = get_proper_type(current_type)
# factorize over union types: isinstance(A|B, C) -> yes = A_yes | B_yes
if isinstance(proper_type, UnionType):
result: list[tuple[Type | None, Type | None]] = [
conditional_types(
union_item,
proposed_type_ranges,
default=union_item,
consider_runtime_isinstance=consider_runtime_isinstance,
)
return proposed_type, remaining_type
for union_item in get_proper_types(proper_type.items)
]
# separate list of tuples into two lists
yes_types, no_types = zip(*result)
proposed_type = make_simplified_union([t for t in yes_types if t is not None])
else:
# An isinstance check, but we don't understand the type
return current_type, default
proposed_items = [type_range.item for type_range in proposed_type_ranges]
proposed_type = make_simplified_union(proposed_items)

if isinstance(proper_type, AnyType):
return proposed_type, current_type
elif isinstance(proposed_type, AnyType):
# We don't really know much about the proposed type, so we shouldn't
# attempt to narrow anything. Instead, we broaden the expr to Any to
# avoid false positives
return proposed_type, default
elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and (
# concrete subtypes
is_proper_subtype(current_type, proposed_type, ignore_promotions=True)
# structural subtypes
or (
(
isinstance(proposed_type, CallableType)
or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol)
)
and is_subtype(current_type, proposed_type, ignore_promotions=True)
)
):
# Expression is always of one of the types in proposed_type_ranges
return default, UninhabitedType()
elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
# Expression is never of any type in proposed_type_ranges
return UninhabitedType(), default
else:
# we can only restrict when the type is precise, not bounded
proposed_precise_type = UnionType.make_union(
[
type_range.item
for type_range in proposed_type_ranges
if not type_range.is_upper_bound
]
)
remaining_type = restrict_subtype_away(
current_type,
proposed_precise_type,
consider_runtime_isinstance=consider_runtime_isinstance,
)
return proposed_type, remaining_type


def conditional_types_to_typemaps(
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1483,11 +1483,12 @@ def f(x: Union[int, A], a: Type[A]) -> None:
[builtins fixtures/isinstancelist.pyi]

[case testIsInstanceWithEmtpy2ndArg]
# flags: --warn-unreachable
from typing import Union

def f(x: Union[int, str]) -> None:
if isinstance(x, ()):
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
reveal_type(x) # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
[builtins fixtures/isinstancelist.pyi]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-narrowing.test
Original file line number Diff line number Diff line change
Expand Up @@ -2639,7 +2639,7 @@ def baz(item: Base) -> None:

reveal_type(item) # N: Revealed type is "Union[__main__.<subclass of "__main__.Base" and "__main__.FooMixin">, __main__.<subclass of "__main__.Base" and "__main__.BarMixin">]"
if isinstance(item, FooMixin):
reveal_type(item) # N: Revealed type is "__main__.FooMixin"
reveal_type(item) # N: Revealed type is "__main__.<subclass of "__main__.Base" and "__main__.FooMixin">"
item.foo()
else:
reveal_type(item) # N: Revealed type is "__main__.<subclass of "__main__.Base" and "__main__.BarMixin">"
Expand Down
16 changes: 16 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,22 @@ def union(x: str | bool) -> None:
reveal_type(x) # N: Revealed type is "Union[builtins.str, Literal[False]]"
[builtins fixtures/tuple.pyi]

[case testMatchNarrowDownUnionUsingClassPattern]

class Foo: ...
class Bar(Foo): ...

def test_1(bar: Bar) -> None:
match bar:
case Foo() as foo:
reveal_type(foo) # N: Revealed type is "__main__.Bar"

def test_2(bar: Bar | str) -> None:
match bar:
case Foo() as foo:
reveal_type(foo) # N: Revealed type is "__main__.Bar"


[case testMatchAssertFalseToSilenceFalsePositives]
class C:
a: int | str
Expand Down