Skip to content

Check property decorators stricter #19313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -1567,6 +1567,17 @@ 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"}:
Copy link
Collaborator

@brianschubert brianschubert Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if deco.name not in {"setter", "deleter"}:
if deco.name not in {"getter", "setter", "deleter"}:

@x.getter is valid at runtime (albeit not common). We probably don't want to emit an "Only supported top decorators are ..." error for it.

I guess we don't have any tests for it 🤷

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. This definitely needs a test, but I think rejecting @prop.getter is a reasonable thing to do: it should override the existing getter, supporting it properly won't be trivial, so explicitly saying "we don't support redefined getters" is not that bad (given that such usage should be really rare, 0 primer hits)

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
Expand Down
43 changes: 39 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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]

Expand Down Expand Up @@ -1622,7 +1622,42 @@ 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]
import typing
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]
import typing
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 testDynamicallyTypedProperty]
import typing
Expand Down Expand Up @@ -7627,7 +7662,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"
Expand Down
4 changes: 1 addition & 3 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/semanal-errors.test
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,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]
Expand Down