Skip to content

Commit c17074d

Browse files
JP BeaudryJP Beaudry
authored andcommitted
Fix isinstance narrowing for classes with __getattr__ returning Any
1 parent 9790459 commit c17074d

2 files changed

Lines changed: 55 additions & 9 deletions

File tree

mypy/meet.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,9 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
136136
narrowed_items = narrowed.relevant_items()
137137
else:
138138
narrowed_items = [narrowed]
139-
return make_simplified_union(
140-
[
141-
narrow_declared_type(d, n)
142-
for d in declared_items
143-
for n in narrowed_items
139+
results = []
140+
for d in declared_items:
141+
for n in narrowed_items:
144142
# This (ugly) special-casing is needed to support checking
145143
# branches like this:
146144
# x: Union[float, complex]
@@ -150,12 +148,31 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
150148
# x: float | None
151149
# y: int | None
152150
# x = y
153-
if (
151+
if not (
154152
is_overlapping_types(d, n, ignore_promotions=True)
155153
or is_subtype(n, d, ignore_promotions=False)
156-
)
157-
]
158-
)
154+
):
155+
continue
156+
result = narrow_declared_type(d, n)
157+
# If narrowing a union member d to n returned d unchanged,
158+
# but d is not nominally related to n (only structurally,
159+
# e.g. via __getattr__ returning Any), exclude it.
160+
# Otherwise a class with __getattr__ -> Any leaks back into
161+
# a union narrowed to a protocol it only structurally satisfies.
162+
# See https://github.com/python/mypy/issues/16590
163+
d_proper = get_proper_type(d)
164+
n_proper = get_proper_type(n)
165+
if (
166+
result == d
167+
and d != n
168+
and isinstance(d_proper, Instance)
169+
and isinstance(n_proper, Instance)
170+
and n_proper.type.is_protocol
171+
and not d_proper.type.has_base(n_proper.type.fullname)
172+
):
173+
continue
174+
results.append(result)
175+
return make_simplified_union(results)
159176
if is_enum_overlapping_union(declared, narrowed):
160177
# Quick check before reaching `is_overlapping_types`. If it's enum/literal overlap,
161178
# avoid full expansion and make it faster.

test-data/unit/check-isinstance.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3241,3 +3241,32 @@ def f2(x: A | None, t: type[A]):
32413241
else:
32423242
reveal_type(x) # N: Revealed type is "None"
32433243
[builtins fixtures/isinstancelist.pyi]
3244+
3245+
[case testIsinstanceNarrowingWithGetattr]
3246+
# flags: --warn-unreachable
3247+
# https://github.com/python/mypy/issues/16590
3248+
from typing import Any, Union
3249+
from typing_extensions import Protocol
3250+
3251+
class A:
3252+
def __getattr__(self, name: str) -> Any: ...
3253+
3254+
class MyProto(Protocol):
3255+
def method(self) -> int: ...
3256+
3257+
def with_getattr_any(a: Union[A, MyProto]) -> None:
3258+
if isinstance(a, A):
3259+
reveal_type(a) # N: Revealed type is "__main__.A"
3260+
else:
3261+
reveal_type(a) # N: Revealed type is "__main__.MyProto"
3262+
3263+
class B:
3264+
def __getattr__(self, name: str) -> str: ...
3265+
3266+
def with_getattr_str(a: Union[B, MyProto]) -> None:
3267+
if isinstance(a, B):
3268+
reveal_type(a) # N: Revealed type is "__main__.B"
3269+
else:
3270+
reveal_type(a) # N: Revealed type is "__main__.MyProto"
3271+
[builtins fixtures/isinstance.pyi]
3272+
[typing fixtures/typing-full.pyi]

0 commit comments

Comments
 (0)