diff --git a/mypy/stubtest.py b/mypy/stubtest.py index a08b0aac1097..3024b74182d6 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -770,6 +770,18 @@ def verify_var( yield Error(object_path, "is not present at runtime", stub, runtime) return + if ( + stub.is_initialized_in_class + and is_read_only_property(runtime) + and (stub.is_settable_property or not stub.is_property) + ): + yield Error( + object_path, + "is read-only at runtime but not in the stub", + stub, + runtime + ) + runtime_type = get_mypy_type_of_runtime_value(runtime) if ( runtime_type is not None @@ -802,7 +814,14 @@ def verify_overloadedfuncdef( return if stub.is_property: - # We get here in cases of overloads from property.setter + # Any property with a setter is represented as an OverloadedFuncDef + if is_read_only_property(runtime): + yield Error( + object_path, + "is read-only at runtime but not in the stub", + stub, + runtime + ) return if not is_probably_a_function(runtime): @@ -845,7 +864,7 @@ def verify_typevarexpr( yield None -def _verify_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]: +def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]: assert stub.func.is_property if isinstance(runtime, property): return @@ -920,7 +939,7 @@ def verify_decorator( yield Error(object_path, "is not present at runtime", stub, runtime) return if stub.func.is_property: - for message in _verify_property(stub, runtime): + for message in _verify_readonly_property(stub, runtime): yield Error(object_path, message, stub, runtime) return @@ -1040,6 +1059,10 @@ def is_probably_a_function(runtime: Any) -> bool: ) +def is_read_only_property(runtime: object) -> bool: + return isinstance(runtime, property) and runtime.fset is None + + def safe_inspect_signature(runtime: Any) -> Optional[inspect.Signature]: try: return inspect.signature(runtime) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index fa7505e37191..0310ea04c78a 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -548,12 +548,12 @@ def test_property(self) -> Iterator[Case]: stub=""" class Good: @property - def f(self) -> int: ... + def read_only_attr(self) -> int: ... """, runtime=""" class Good: @property - def f(self) -> int: return 1 + def read_only_attr(self): return 1 """, error=None, ) @@ -593,6 +593,38 @@ class BadReadOnly: """, error="BadReadOnly.f", ) + yield Case( + stub=""" + class Y: + @property + def read_only_attr(self) -> int: ... + @read_only_attr.setter + def read_only_attr(self, val: int) -> None: ... + """, + runtime=""" + class Y: + @property + def read_only_attr(self): return 5 + """, + error="Y.read_only_attr", + ) + yield Case( + stub=""" + class Z: + @property + def read_write_attr(self) -> int: ... + @read_write_attr.setter + def read_write_attr(self, val: int) -> None: ... + """, + runtime=""" + class Z: + @property + def read_write_attr(self): return self._val + @read_write_attr.setter + def read_write_attr(self, val): self._val = val + """, + error=None, + ) @collect_cases def test_var(self) -> Iterator[Case]: @@ -631,6 +663,32 @@ def __init__(self): """, error=None, ) + yield Case( + stub=""" + class Y: + read_only_attr: int + """, + runtime=""" + class Y: + @property + def read_only_attr(self): return 5 + """, + error="Y.read_only_attr", + ) + yield Case( + stub=""" + class Z: + read_write_attr: int + """, + runtime=""" + class Z: + @property + def read_write_attr(self): return self._val + @read_write_attr.setter + def read_write_attr(self, val): self._val = val + """, + error=None, + ) @collect_cases def test_type_alias(self) -> Iterator[Case]: