Skip to content

Commit 27938e7

Browse files
authored
stubtest: error for read-only property at runtime, but not in stub (#12291)
1 parent 47b9f5e commit 27938e7

File tree

2 files changed

+86
-5
lines changed

2 files changed

+86
-5
lines changed

mypy/stubtest.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,18 @@ def verify_var(
773773
yield Error(object_path, "is not present at runtime", stub, runtime)
774774
return
775775

776+
if (
777+
stub.is_initialized_in_class
778+
and is_read_only_property(runtime)
779+
and (stub.is_settable_property or not stub.is_property)
780+
):
781+
yield Error(
782+
object_path,
783+
"is read-only at runtime but not in the stub",
784+
stub,
785+
runtime
786+
)
787+
776788
runtime_type = get_mypy_type_of_runtime_value(runtime)
777789
if (
778790
runtime_type is not None
@@ -805,7 +817,14 @@ def verify_overloadedfuncdef(
805817
return
806818

807819
if stub.is_property:
808-
# We get here in cases of overloads from property.setter
820+
# Any property with a setter is represented as an OverloadedFuncDef
821+
if is_read_only_property(runtime):
822+
yield Error(
823+
object_path,
824+
"is read-only at runtime but not in the stub",
825+
stub,
826+
runtime
827+
)
809828
return
810829

811830
if not is_probably_a_function(runtime):
@@ -848,7 +867,7 @@ def verify_typevarexpr(
848867
yield None
849868

850869

851-
def _verify_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]:
870+
def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]:
852871
assert stub.func.is_property
853872
if isinstance(runtime, property):
854873
return
@@ -923,7 +942,7 @@ def verify_decorator(
923942
yield Error(object_path, "is not present at runtime", stub, runtime)
924943
return
925944
if stub.func.is_property:
926-
for message in _verify_property(stub, runtime):
945+
for message in _verify_readonly_property(stub, runtime):
927946
yield Error(object_path, message, stub, runtime)
928947
return
929948

@@ -1044,6 +1063,10 @@ def is_probably_a_function(runtime: Any) -> bool:
10441063
)
10451064

10461065

1066+
def is_read_only_property(runtime: object) -> bool:
1067+
return isinstance(runtime, property) and runtime.fset is None
1068+
1069+
10471070
def safe_inspect_signature(runtime: Any) -> Optional[inspect.Signature]:
10481071
try:
10491072
return inspect.signature(runtime)

mypy/test/teststubtest.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,12 +547,12 @@ def test_property(self) -> Iterator[Case]:
547547
stub="""
548548
class Good:
549549
@property
550-
def f(self) -> int: ...
550+
def read_only_attr(self) -> int: ...
551551
""",
552552
runtime="""
553553
class Good:
554554
@property
555-
def f(self) -> int: return 1
555+
def read_only_attr(self): return 1
556556
""",
557557
error=None,
558558
)
@@ -592,6 +592,38 @@ class BadReadOnly:
592592
""",
593593
error="BadReadOnly.f",
594594
)
595+
yield Case(
596+
stub="""
597+
class Y:
598+
@property
599+
def read_only_attr(self) -> int: ...
600+
@read_only_attr.setter
601+
def read_only_attr(self, val: int) -> None: ...
602+
""",
603+
runtime="""
604+
class Y:
605+
@property
606+
def read_only_attr(self): return 5
607+
""",
608+
error="Y.read_only_attr",
609+
)
610+
yield Case(
611+
stub="""
612+
class Z:
613+
@property
614+
def read_write_attr(self) -> int: ...
615+
@read_write_attr.setter
616+
def read_write_attr(self, val: int) -> None: ...
617+
""",
618+
runtime="""
619+
class Z:
620+
@property
621+
def read_write_attr(self): return self._val
622+
@read_write_attr.setter
623+
def read_write_attr(self, val): self._val = val
624+
""",
625+
error=None,
626+
)
595627

596628
@collect_cases
597629
def test_var(self) -> Iterator[Case]:
@@ -630,6 +662,32 @@ def __init__(self):
630662
""",
631663
error=None,
632664
)
665+
yield Case(
666+
stub="""
667+
class Y:
668+
read_only_attr: int
669+
""",
670+
runtime="""
671+
class Y:
672+
@property
673+
def read_only_attr(self): return 5
674+
""",
675+
error="Y.read_only_attr",
676+
)
677+
yield Case(
678+
stub="""
679+
class Z:
680+
read_write_attr: int
681+
""",
682+
runtime="""
683+
class Z:
684+
@property
685+
def read_write_attr(self): return self._val
686+
@read_write_attr.setter
687+
def read_write_attr(self, val): self._val = val
688+
""",
689+
error=None,
690+
)
633691

634692
@collect_cases
635693
def test_type_alias(self) -> Iterator[Case]:

0 commit comments

Comments
 (0)