diff --git a/mypy/checker.py b/mypy/checker.py index f201a767a860..c2456c2dd395 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7797,11 +7797,8 @@ 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 + # Note: types can be an empty list, for example in `isinstance(x, ())`, + # which always returns False at runtime. return types def is_literal_enum(self, n: Expression) -> bool: @@ -8038,53 +8035,83 @@ def conditional_types( ) -> tuple[Type | None, Type | None]: """Takes in the current type and a proposed type of an expression. - Returns a 2-tuple: The first element is the proposed type, if the expression - can be the proposed type. The second element is the type it would hold - if it was not the proposed type, if any. UninhabitedType means unreachable. - None means no new information can be inferred. If default is set it is returned - instead.""" - 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(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 is_proper_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, + Returns a 2-tuple: + The first element is the proposed type, if the expression can be the proposed type. + The second element is the type it would hold if it was not the proposed type, if any. + UninhabitedType means unreachable. + None means no new information can be inferred. + If default is set it is returned instead. + """ + 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 + + 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) + ): + enum_name = target.fallback.type.fullname + current_type = try_expanding_sum_type_to_union(current_type, enum_name) + + current_type = get_proper_type(current_type) + if ( + isinstance(current_type, UnionType) + and not any(tr.is_upper_bound for tr in proposed_type_ranges) + and (default in (current_type, None)) + ): + # factorize over union types + # if we try to narrow A|B to C, we instead narrow A to C and B to C, and + # return the union of the results + 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(current_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(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 is_proper_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( diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 7da258a827f3..7c45702bd250 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2094,6 +2094,10 @@ def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool isinstance(). Currently, this just removes elements of a union type. """ p_t = get_proper_type(t) + s_t = get_proper_type(s) + if isinstance(s_t, TypeVarType): + s = s_t.upper_bound + if isinstance(p_t, UnionType): new_items = try_restrict_literal_union(p_t, s) if new_items is None: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 640fc10915d1..131087a8dbdb 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -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] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 7fffd3ce94e5..00d33c86414f 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2639,7 +2639,7 @@ def baz(item: Base) -> None: reveal_type(item) # N: Revealed type is "Union[__main__., __main__.]" if isinstance(item, FooMixin): - reveal_type(item) # N: Revealed type is "__main__.FooMixin" + reveal_type(item) # N: Revealed type is "__main__." item.foo() else: reveal_type(item) # N: Revealed type is "__main__." diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 80fd64fa3569..e3b5ebc20e80 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1760,6 +1760,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