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
35 changes: 26 additions & 9 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading