Skip to content

Commit b6411cf

Browse files
committed
Check property setter/deleter decorators stricter
1 parent ed88d82 commit b6411cf

File tree

3 files changed

+56
-14
lines changed

3 files changed

+56
-14
lines changed

mypy/semanal.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,33 +1527,33 @@ def analyze_property_with_multi_part_definition(
15271527
assert isinstance(first_item, Decorator)
15281528
deleted_items = []
15291529
bare_setter_type = None
1530+
func_name = first_item.func.name
15301531
for i, item in enumerate(items[1:]):
15311532
if isinstance(item, Decorator):
15321533
item.func.accept(self)
15331534
if item.decorators:
15341535
first_node = item.decorators[0]
1535-
if isinstance(first_node, MemberExpr):
1536+
if self._is_valid_property_decorator(first_node, func_name):
1537+
# Get abstractness from the original definition.
1538+
item.func.abstract_status = first_item.func.abstract_status
15361539
if first_node.name == "setter":
15371540
# The first item represents the entire property.
15381541
first_item.var.is_settable_property = True
1539-
# Get abstractness from the original definition.
1540-
item.func.abstract_status = first_item.func.abstract_status
15411542
setter_func_type = function_type(
15421543
item.func, self.named_type("builtins.function")
15431544
)
15441545
assert isinstance(setter_func_type, CallableType)
15451546
bare_setter_type = setter_func_type
15461547
defn.setter_index = i + 1
1547-
if first_node.name == "deleter":
1548-
item.func.abstract_status = first_item.func.abstract_status
15491548
for other_node in item.decorators[1:]:
15501549
other_node.accept(self)
15511550
else:
15521551
self.fail(
1553-
f"Only supported top decorator is @{first_item.func.name}.setter", item
1552+
f"Only supported top decorators are @{func_name}.setter and @{func_name}.deleter",
1553+
item,
15541554
)
15551555
else:
1556-
self.fail(f'Unexpected definition for property "{first_item.func.name}"', item)
1556+
self.fail(f'Unexpected definition for property "{func_name}"', item)
15571557
deleted_items.append(i + 1)
15581558
for i in reversed(deleted_items):
15591559
del items[i]
@@ -1567,6 +1567,15 @@ def analyze_property_with_multi_part_definition(
15671567
)
15681568
return bare_setter_type
15691569

1570+
def _is_valid_property_decorator(self, deco: Expression, property_name: str) -> bool:
1571+
if not isinstance(deco, MemberExpr):
1572+
return False
1573+
if not isinstance(deco.expr, NameExpr) or deco.expr.name != property_name:
1574+
return False
1575+
if deco.name not in {"setter", "deleter"}:
1576+
return False
1577+
return True
1578+
15701579
def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None:
15711580
if self.is_class_scope():
15721581
assert self.type is not None

test-data/unit/check-classes.test

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ class A:
772772
class B(A):
773773
@property
774774
def f(self) -> Callable[[object], None]: pass
775-
@func.setter
775+
@f.setter
776776
def f(self, x: object) -> None: pass
777777
[builtins fixtures/property.pyi]
778778

@@ -786,7 +786,7 @@ class A:
786786
class B(A):
787787
@property
788788
def f(self) -> Callable[[object], None]: pass
789-
@func.setter
789+
@f.setter
790790
def f(self, x: object) -> None: pass
791791
[builtins fixtures/property.pyi]
792792

@@ -1622,7 +1622,42 @@ class A:
16221622
self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
16231623
return ''
16241624
[builtins fixtures/property.pyi]
1625-
[out]
1625+
1626+
[case testPropertyNameIsChecked]
1627+
import typing
1628+
class A:
1629+
@property
1630+
def f(self) -> str: ...
1631+
@not_f.setter # E: Only supported top decorators are @f.setter and @f.deleter
1632+
def f(self, val: str) -> None: ...
1633+
1634+
a = A()
1635+
reveal_type(a.f) # N: Revealed type is "builtins.str"
1636+
a.f = '' # E: Property "f" defined in "A" is read-only
1637+
1638+
class B:
1639+
@property
1640+
def f(self) -> str: ...
1641+
@not_f.deleter # E: Only supported top decorators are @f.setter and @f.deleter
1642+
def f(self) -> None: ...
1643+
1644+
class C:
1645+
@property
1646+
def f(self) -> str: ...
1647+
@not_f.setter # E: Only supported top decorators are @f.setter and @f.deleter
1648+
def f(self, val: str) -> None: ...
1649+
@not_f.deleter # E: Only supported top decorators are @f.setter and @f.deleter
1650+
def f(self) -> None: ...
1651+
[builtins fixtures/property.pyi]
1652+
1653+
[case testPropertyAttributeIsChecked]
1654+
import typing
1655+
class A:
1656+
@property
1657+
def f(self) -> str: ...
1658+
@f.unknown # E: Only supported top decorators are @f.setter and @f.deleter
1659+
def f(self, val: str) -> None: ...
1660+
[builtins fixtures/property.pyi]
16261661

16271662
[case testDynamicallyTypedProperty]
16281663
import typing
@@ -7627,7 +7662,7 @@ class A:
76277662
def y(self) -> int: ...
76287663
@y.setter
76297664
def y(self, value: int) -> None: ...
7630-
@dec # E: Only supported top decorator is @y.setter
7665+
@dec # E: Only supported top decorators are @y.setter and @y.deleter
76317666
def y(self) -> None: ...
76327667

76337668
reveal_type(A().y) # N: Revealed type is "builtins.int"

test-data/unit/check-functions.test

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2752,7 +2752,7 @@ class B:
27522752
@property
27532753
@dec
27542754
def f(self) -> int: pass
2755-
@dec # E: Only supported top decorator is @f.setter
2755+
@dec # E: Only supported top decorators are @f.setter and @f.deleter
27562756
@f.setter
27572757
def f(self, v: int) -> None: pass
27582758

@@ -2764,7 +2764,6 @@ class C:
27642764
@dec
27652765
def f(self, v: int) -> None: pass
27662766
[builtins fixtures/property.pyi]
2767-
[out]
27682767

27692768
[case testInvalidArgCountForProperty]
27702769
from typing import Callable, TypeVar
@@ -2783,7 +2782,6 @@ class A:
27832782
@property
27842783
def h(self, *args, **kwargs) -> int: pass # OK
27852784
[builtins fixtures/property.pyi]
2786-
[out]
27872785

27882786
[case testSubtypingUnionGenericBounds]
27892787
from typing import Callable, TypeVar, Union, Sequence

0 commit comments

Comments
 (0)