diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 9dd630d61d4a..5973d82f2383 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -2049,6 +2049,10 @@ def _named_type(name: str) -> mypy.types.Instance: return mypy.types.TupleType(items, fallback) fallback = mypy.types.Instance(type_info, [anytype() for _ in type_info.type_vars]) + if type(runtime) != runtime.__class__: + # Since `__class__` is redefined for an instance, we can't trust + # its `isinstance` checks, it can be dynamic. See #20919 + return fallback value: bool | int | str if isinstance(runtime, enum.Enum) and isinstance(runtime.name, str): diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 4023a191b548..d373b1560271 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1564,6 +1564,49 @@ def f(): return 3 error=None, ) + @collect_cases + def test_proxy_object(self) -> Iterator[Case]: + yield Case( + stub=""" + class LazyObject: + def __init__(self, func: object) -> None: ... + def __bool__(self) -> bool: ... + """, + runtime=""" + class LazyObject: + def __init__(self, func): + self.__dict__["_wrapped"] = None + self.__dict__["_setupfunc"] = func + def _setup(self): + self.__dict__["_wrapped"] = self._setupfunc() + @property + def __class__(self): + if self._wrapped is None: + self._setup() + return type(self._wrapped) + def __bool__(self): + if self._wrapped is None: + self._setup() + return bool(self._wrapped) + """, + error="test_module.LazyObject.__class__", + ) + yield Case( + stub=""" + def default_value() -> bool: ... + + DEFAULT_VALUE: bool + """, + runtime=""" + def default_value(): + return True + + DEFAULT_VALUE = LazyObject(default_value) + bool(DEFAULT_VALUE) # evaluate the lazy object + """, + error="test_module.DEFAULT_VALUE", + ) + @collect_cases def test_all_at_runtime_not_stub(self) -> Iterator[Case]: yield Case(