diff --git a/mypy/semanal.py b/mypy/semanal.py index 87aef2595caf..60f5270a1b5b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1527,33 +1527,33 @@ def analyze_property_with_multi_part_definition( assert isinstance(first_item, Decorator) deleted_items = [] bare_setter_type = None + func_name = first_item.func.name for i, item in enumerate(items[1:]): if isinstance(item, Decorator): item.func.accept(self) if item.decorators: first_node = item.decorators[0] - if isinstance(first_node, MemberExpr): + if self._is_valid_property_decorator(first_node, func_name): + # Get abstractness from the original definition. + item.func.abstract_status = first_item.func.abstract_status if first_node.name == "setter": # The first item represents the entire property. first_item.var.is_settable_property = True - # Get abstractness from the original definition. - item.func.abstract_status = first_item.func.abstract_status setter_func_type = function_type( item.func, self.named_type("builtins.function") ) assert isinstance(setter_func_type, CallableType) bare_setter_type = setter_func_type defn.setter_index = i + 1 - if first_node.name == "deleter": - item.func.abstract_status = first_item.func.abstract_status for other_node in item.decorators[1:]: other_node.accept(self) else: self.fail( - f"Only supported top decorator is @{first_item.func.name}.setter", item + f"Only supported top decorators are @{func_name}.setter and @{func_name}.deleter", + item, ) else: - self.fail(f'Unexpected definition for property "{first_item.func.name}"', item) + self.fail(f'Unexpected definition for property "{func_name}"', item) deleted_items.append(i + 1) for i in reversed(deleted_items): del items[i] @@ -1567,6 +1567,23 @@ def analyze_property_with_multi_part_definition( ) return bare_setter_type + def _is_valid_property_decorator( + self, deco: Expression, property_name: str + ) -> TypeGuard[MemberExpr]: + if not isinstance(deco, MemberExpr): + return False + if not isinstance(deco.expr, NameExpr) or deco.expr.name != property_name: + return False + if deco.name not in {"setter", "deleter"}: + # This intentionally excludes getter. While `@prop.getter` is valid at + # runtime, that would mean replacing the already processed getter type. + # Such usage is almost definitely a mistake (except for overrides in + # subclasses but we don't support them anyway) and might be a typo + # (only one letter away from `setter`), it's likely almost never used, + # so supporting it properly won't pay off. + return False + return True + def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None: if self.is_class_scope(): assert self.type is not None diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index bf6c51e86446..9824c4598c18 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -772,7 +772,7 @@ class A: class B(A): @property def f(self) -> Callable[[object], None]: pass - @func.setter + @f.setter def f(self, x: object) -> None: pass [builtins fixtures/property.pyi] @@ -786,7 +786,7 @@ class A: class B(A): @property def f(self) -> Callable[[object], None]: pass - @func.setter + @f.setter def f(self, x: object) -> None: pass [builtins fixtures/property.pyi] @@ -1622,7 +1622,48 @@ class A: self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") return '' [builtins fixtures/property.pyi] -[out] + +[case testPropertyNameIsChecked] +class A: + @property + def f(self) -> str: ... + @not_f.setter # E: Only supported top decorators are @f.setter and @f.deleter + def f(self, val: str) -> None: ... + +a = A() +reveal_type(a.f) # N: Revealed type is "builtins.str" +a.f = '' # E: Property "f" defined in "A" is read-only + +class B: + @property + def f(self) -> str: ... + @not_f.deleter # E: Only supported top decorators are @f.setter and @f.deleter + def f(self) -> None: ... + +class C: + @property + def f(self) -> str: ... + @not_f.setter # E: Only supported top decorators are @f.setter and @f.deleter + def f(self, val: str) -> None: ... + @not_f.deleter # E: Only supported top decorators are @f.setter and @f.deleter + def f(self) -> None: ... +[builtins fixtures/property.pyi] + +[case testPropertyAttributeIsChecked] +class A: + @property + def f(self) -> str: ... + @f.unknown # E: Only supported top decorators are @f.setter and @f.deleter + def f(self, val: str) -> None: ... +[builtins fixtures/property.pyi] + +[case testPropertyGetterDecoratorIsRejected] +class A: + @property + def f(self) -> str: ... + @f.getter # E: Only supported top decorators are @f.setter and @f.deleter + def f(self, val: str) -> None: ... +[builtins fixtures/property.pyi] [case testDynamicallyTypedProperty] import typing @@ -7729,7 +7770,7 @@ class A: def y(self) -> int: ... @y.setter def y(self, value: int) -> None: ... - @dec # E: Only supported top decorator is @y.setter + @dec # E: Only supported top decorators are @y.setter and @y.deleter def y(self) -> None: ... reveal_type(A().y) # N: Revealed type is "builtins.int" diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index ceb7af433dce..f0559cbfd437 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -2752,7 +2752,7 @@ class B: @property @dec def f(self) -> int: pass - @dec # E: Only supported top decorator is @f.setter + @dec # E: Only supported top decorators are @f.setter and @f.deleter @f.setter def f(self, v: int) -> None: pass @@ -2764,7 +2764,6 @@ class C: @dec def f(self, v: int) -> None: pass [builtins fixtures/property.pyi] -[out] [case testInvalidArgCountForProperty] from typing import Callable, TypeVar @@ -2783,7 +2782,6 @@ class A: @property def h(self, *args, **kwargs) -> int: pass # OK [builtins fixtures/property.pyi] -[out] [case testSubtypingUnionGenericBounds] from typing import Callable, TypeVar, Union, Sequence diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 1e760799828a..dd0825c65501 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1236,7 +1236,7 @@ class A: @overload # E: Decorators on top of @property are not supported @property def f(self) -> int: pass - @property # E: Only supported top decorator is @f.setter + @property # E: Only supported top decorators are @f.setter and @f.deleter @overload def f(self) -> int: pass [builtins fixtures/property.pyi]