Skip to content

Fix properties with setters after deleters #19248

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

Merged
merged 2 commits into from
Jun 7, 2025
Merged
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
10 changes: 4 additions & 6 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,11 +697,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
assert isinstance(defn.items[0], Decorator)
self.visit_decorator(defn.items[0])
if defn.items[0].var.is_settable_property:
# TODO: here and elsewhere we assume setter immediately follows getter.
assert isinstance(defn.items[1], Decorator)
# Perform a reduced visit just to infer the actual setter type.
self.visit_decorator_inner(defn.items[1], skip_first_item=True)
setter_type = defn.items[1].var.type
self.visit_decorator_inner(defn.setter, skip_first_item=True)
setter_type = defn.setter.var.type
# Check if the setter can accept two positional arguments.
any_type = AnyType(TypeOfAny.special_form)
fallback_setter_type = CallableType(
Expand All @@ -712,7 +710,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
fallback=self.named_type("builtins.function"),
)
if setter_type and not is_subtype(setter_type, fallback_setter_type):
self.fail("Invalid property setter signature", defn.items[1].func)
self.fail("Invalid property setter signature", defn.setter.func)
setter_type = self.extract_callable_type(setter_type, defn)
if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2:
# TODO: keep precise type for callables with tricky but valid signatures.
Expand Down Expand Up @@ -2171,7 +2169,7 @@ def check_setter_type_override(self, defn: OverloadedFuncDef, base: TypeInfo) ->
assert typ is not None and original_type is not None

if not is_subtype(original_type, typ):
self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base)
self.msg.incompatible_setter_override(defn.setter, typ, original_type, base)

def check_method_override_for_base_with_name(
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo
Expand Down
4 changes: 2 additions & 2 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ def analyze_instance_member_access(
assert isinstance(method, OverloadedFuncDef)
getter = method.items[0]
assert isinstance(getter, Decorator)
if mx.is_lvalue and (len(items := method.items) > 1):
mx.chk.warn_deprecated(items[1], mx.context)
if mx.is_lvalue and getter.var.is_settable_property:
mx.chk.warn_deprecated(method.setter, mx.context)
return analyze_var(name, getter.var, typ, mx)

if mx.is_lvalue and not mx.suppress_errors:
Expand Down
24 changes: 23 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,19 +538,28 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement):
Overloaded variants must be consecutive in the source file.
"""

__slots__ = ("items", "unanalyzed_items", "impl", "deprecated", "_is_trivial_self")
__slots__ = (
"items",
"unanalyzed_items",
"impl",
"deprecated",
"setter_index",
"_is_trivial_self",
)

items: list[OverloadPart]
unanalyzed_items: list[OverloadPart]
impl: OverloadPart | None
deprecated: str | None
setter_index: int | None

def __init__(self, items: list[OverloadPart]) -> None:
super().__init__()
self.items = items
self.unanalyzed_items = items.copy()
self.impl = None
self.deprecated = None
self.setter_index = None
self._is_trivial_self: bool | None = None
if items:
# TODO: figure out how to reliably set end position (we don't know the impl here).
Expand Down Expand Up @@ -586,6 +595,17 @@ def is_trivial_self(self) -> bool:
self._is_trivial_self = True
return True

@property
def setter(self) -> Decorator:
# Do some consistency checks first.
first_item = self.items[0]
assert isinstance(first_item, Decorator)
assert first_item.var.is_settable_property
assert self.setter_index is not None
item = self.items[self.setter_index]
assert isinstance(item, Decorator)
return item

def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_overloaded_func_def(self)

Expand All @@ -598,6 +618,7 @@ def serialize(self) -> JsonDict:
"impl": None if self.impl is None else self.impl.serialize(),
"flags": get_flags(self, FUNCBASE_FLAGS),
"deprecated": self.deprecated,
"setter_index": self.setter_index,
}

@classmethod
Expand All @@ -618,6 +639,7 @@ def deserialize(cls, data: JsonDict) -> OverloadedFuncDef:
res._fullname = data["fullname"]
set_flags(res, data["flags"])
res.deprecated = data["deprecated"]
res.setter_index = data["setter_index"]
# NOTE: res.info will be set in the fixup phase.
return res

Expand Down
1 change: 1 addition & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,7 @@ def analyze_property_with_multi_part_definition(
)
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:]:
Expand Down
20 changes: 20 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -8736,3 +8736,23 @@ class NoopPowerResource:
def hardware_type(self) -> None: # E: Invalid property setter signature
self.hardware_type = None # Note: intentionally recursive
[builtins fixtures/property.pyi]

[case testPropertyAllowsDeleterBeforeSetter]
class C:
@property
def foo(self) -> str: ...
@foo.deleter
def foo(self) -> None: ...
@foo.setter
def foo(self, val: int) -> None: ...

@property
def bar(self) -> int: ...
@bar.deleter
def bar(self) -> None: ...
@bar.setter
def bar(self, value: int, val: int) -> None: ... # E: Invalid property setter signature

C().foo = "no" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
C().bar = "fine"
[builtins fixtures/property.pyi]