diff --git a/mypy/checker.py b/mypy/checker.py index a7875cbd2226..7ad2c0bfb6aa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8427,12 +8427,12 @@ def conditional_types( proposed_type: Type remaining_type: Type - proper_type = get_proper_type(current_type) + p_current_type = get_proper_type(current_type) # factorize over union types: isinstance(A|B, C) -> yes = A_yes | B_yes - if isinstance(proper_type, UnionType): + if isinstance(p_current_type, UnionType): yes_items: list[Type] = [] no_items: list[Type] = [] - for union_item in proper_type.items: + for union_item in p_current_type.items: yes_type, no_type = conditional_types( union_item, proposed_type_ranges, @@ -8458,7 +8458,7 @@ def conditional_types( items[i] = item proposed_type = get_proper_type(UnionType.make_union(items)) - if isinstance(proper_type, AnyType): + if isinstance(p_current_type, AnyType): return proposed_type, current_type if isinstance(proposed_type, AnyType): # We don't really know much about the proposed type, so we shouldn't @@ -8509,6 +8509,11 @@ def conditional_types( proposed_precise_type, consider_runtime_isinstance=consider_runtime_isinstance, ) + + # Avoid widening the type + if is_proper_subtype(p_current_type, proposed_type, ignore_promotions=True): + proposed_type = default if default is not None else current_type + return proposed_type, remaining_type diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index d98c5424156d..88b87765e973 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1583,6 +1583,61 @@ def f(x: Union[int, str], typ: type) -> None: reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" [builtins fixtures/isinstancelist.pyi] +[case testIsInstanceWithUnknownTypeMultipleNarrowing] +# flags: --strict-equality --warn-unreachable --python-version 3.10 +from __future__ import annotations +from typing import Iterable +from typing_extensions import TypeAlias +import types + +# Regression test for https://github.com/python/mypy/issues/21181 +# We don't have the same type context as with the real stubs, so sort of fake it +_ClassInfoLike: TypeAlias = "type | tuple[_ClassInfoLike, ...]" + +class A: ... +class B(A): ... + +def fake_type_context(ts: list[type[A]]) -> _ClassInfoLike: + return tuple(ts) # E: Too many arguments for "tuple" + + +def f1(x: A | None) -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, object): + reveal_type(x) # N: Revealed type is "__main__.A" + + +def f2(x: A | None, ts: list[type[A]]) -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, fake_type_context(ts)): + reveal_type(x) # N: Revealed type is "__main__.A" + + +def f3(x: A | None, t: type | type[A]) -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, t): + reveal_type(x) # N: Revealed type is "__main__.A" + + +def f4(x: A | None, t: type) -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, t): + reveal_type(x) # N: Revealed type is "__main__.A" + + +def f5(x: object | None, ta: type[A], tb: type[B]) -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "builtins.object" + if isinstance(x, ta): + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, tb): + reveal_type(x) # N: Revealed type is "__main__.B" +[builtins fixtures/isinstancelist.pyi] + [case testIsInstanceWithBoundedType] # flags: --warn-unreachable from typing import Union, Type