Skip to content

Commit

Permalink
stubtest: error for read-only property at runtime, but not in stub (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored Mar 21, 2022
1 parent 47b9f5e commit 27938e7
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 5 deletions.
29 changes: 26 additions & 3 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,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
Expand Down Expand Up @@ -805,7 +817,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):
Expand Down Expand Up @@ -848,7 +867,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
Expand Down Expand Up @@ -923,7 +942,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

Expand Down Expand Up @@ -1044,6 +1063,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)
Expand Down
62 changes: 60 additions & 2 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,12 +547,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,
)
Expand Down Expand Up @@ -592,6 +592,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]:
Expand Down Expand Up @@ -630,6 +662,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]:
Expand Down

0 comments on commit 27938e7

Please sign in to comment.