diff --git a/mypy/meet.py b/mypy/meet.py index ee32f239df8c3..3dafabeeb1a1f 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -136,11 +136,9 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: narrowed_items = narrowed.relevant_items() else: narrowed_items = [narrowed] - return make_simplified_union( - [ - narrow_declared_type(d, n) - for d in declared_items - for n in narrowed_items + results = [] + for d in declared_items: + for n in narrowed_items: # This (ugly) special-casing is needed to support checking # branches like this: # x: Union[float, complex] @@ -150,12 +148,31 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: # x: float | None # y: int | None # x = y - if ( + if not ( is_overlapping_types(d, n, ignore_promotions=True) or is_subtype(n, d, ignore_promotions=False) - ) - ] - ) + ): + continue + result = narrow_declared_type(d, n) + # If narrowing a union member d to n returned d unchanged, + # but d is not nominally related to n (only structurally, + # e.g. via __getattr__ returning Any), exclude it. + # Otherwise a class with __getattr__ -> Any leaks back into + # a union narrowed to a protocol it only structurally satisfies. + # See https://github.com/python/mypy/issues/16590 + d_proper = get_proper_type(d) + n_proper = get_proper_type(n) + if ( + result == d + and d != n + and isinstance(d_proper, Instance) + and isinstance(n_proper, Instance) + and n_proper.type.is_protocol + and not d_proper.type.has_base(n_proper.type.fullname) + ): + continue + results.append(result) + return make_simplified_union(results) if is_enum_overlapping_union(declared, narrowed): # Quick check before reaching `is_overlapping_types`. If it's enum/literal overlap, # avoid full expansion and make it faster. diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 0b20d5911151f..0460b385d5791 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3241,3 +3241,32 @@ def f2(x: A | None, t: type[A]): else: reveal_type(x) # N: Revealed type is "None" [builtins fixtures/isinstancelist.pyi] + +[case testIsinstanceNarrowingWithGetattr] +# flags: --warn-unreachable +# https://github.com/python/mypy/issues/16590 +from typing import Any, Union +from typing_extensions import Protocol + +class A: + def __getattr__(self, name: str) -> Any: ... + +class MyProto(Protocol): + def method(self) -> int: ... + +def with_getattr_any(a: Union[A, MyProto]) -> None: + if isinstance(a, A): + reveal_type(a) # N: Revealed type is "__main__.A" + else: + reveal_type(a) # N: Revealed type is "__main__.MyProto" + +class B: + def __getattr__(self, name: str) -> str: ... + +def with_getattr_str(a: Union[B, MyProto]) -> None: + if isinstance(a, B): + reveal_type(a) # N: Revealed type is "__main__.B" + else: + reveal_type(a) # N: Revealed type is "__main__.MyProto" +[builtins fixtures/isinstance.pyi] +[typing fixtures/typing-full.pyi]