Skip to content

Commit 4c825e9

Browse files
authored
Fix properties with setters after deleters (#19248)
Fixes #19224 Note we must add an additional attribute on `OverloadedFuncDef` since decorator expressions are not serialized.
1 parent 325f776 commit 4c825e9

File tree

5 files changed

+50
-9
lines changed

5 files changed

+50
-9
lines changed

mypy/checker.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -697,11 +697,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
697697
assert isinstance(defn.items[0], Decorator)
698698
self.visit_decorator(defn.items[0])
699699
if defn.items[0].var.is_settable_property:
700-
# TODO: here and elsewhere we assume setter immediately follows getter.
701-
assert isinstance(defn.items[1], Decorator)
702700
# Perform a reduced visit just to infer the actual setter type.
703-
self.visit_decorator_inner(defn.items[1], skip_first_item=True)
704-
setter_type = defn.items[1].var.type
701+
self.visit_decorator_inner(defn.setter, skip_first_item=True)
702+
setter_type = defn.setter.var.type
705703
# Check if the setter can accept two positional arguments.
706704
any_type = AnyType(TypeOfAny.special_form)
707705
fallback_setter_type = CallableType(
@@ -712,7 +710,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
712710
fallback=self.named_type("builtins.function"),
713711
)
714712
if setter_type and not is_subtype(setter_type, fallback_setter_type):
715-
self.fail("Invalid property setter signature", defn.items[1].func)
713+
self.fail("Invalid property setter signature", defn.setter.func)
716714
setter_type = self.extract_callable_type(setter_type, defn)
717715
if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2:
718716
# TODO: keep precise type for callables with tricky but valid signatures.
@@ -2171,7 +2169,7 @@ def check_setter_type_override(self, defn: OverloadedFuncDef, base: TypeInfo) ->
21712169
assert typ is not None and original_type is not None
21722170

21732171
if not is_subtype(original_type, typ):
2174-
self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base)
2172+
self.msg.incompatible_setter_override(defn.setter, typ, original_type, base)
21752173

21762174
def check_method_override_for_base_with_name(
21772175
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo

mypy/checkmember.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,8 @@ def analyze_instance_member_access(
345345
assert isinstance(method, OverloadedFuncDef)
346346
getter = method.items[0]
347347
assert isinstance(getter, Decorator)
348-
if mx.is_lvalue and (len(items := method.items) > 1):
349-
mx.chk.warn_deprecated(items[1], mx.context)
348+
if mx.is_lvalue and getter.var.is_settable_property:
349+
mx.chk.warn_deprecated(method.setter, mx.context)
350350
return analyze_var(name, getter.var, typ, mx)
351351

352352
if mx.is_lvalue and not mx.suppress_errors:

mypy/nodes.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,19 +538,28 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement):
538538
Overloaded variants must be consecutive in the source file.
539539
"""
540540

541-
__slots__ = ("items", "unanalyzed_items", "impl", "deprecated", "_is_trivial_self")
541+
__slots__ = (
542+
"items",
543+
"unanalyzed_items",
544+
"impl",
545+
"deprecated",
546+
"setter_index",
547+
"_is_trivial_self",
548+
)
542549

543550
items: list[OverloadPart]
544551
unanalyzed_items: list[OverloadPart]
545552
impl: OverloadPart | None
546553
deprecated: str | None
554+
setter_index: int | None
547555

548556
def __init__(self, items: list[OverloadPart]) -> None:
549557
super().__init__()
550558
self.items = items
551559
self.unanalyzed_items = items.copy()
552560
self.impl = None
553561
self.deprecated = None
562+
self.setter_index = None
554563
self._is_trivial_self: bool | None = None
555564
if items:
556565
# TODO: figure out how to reliably set end position (we don't know the impl here).
@@ -586,6 +595,17 @@ def is_trivial_self(self) -> bool:
586595
self._is_trivial_self = True
587596
return True
588597

598+
@property
599+
def setter(self) -> Decorator:
600+
# Do some consistency checks first.
601+
first_item = self.items[0]
602+
assert isinstance(first_item, Decorator)
603+
assert first_item.var.is_settable_property
604+
assert self.setter_index is not None
605+
item = self.items[self.setter_index]
606+
assert isinstance(item, Decorator)
607+
return item
608+
589609
def accept(self, visitor: StatementVisitor[T]) -> T:
590610
return visitor.visit_overloaded_func_def(self)
591611

@@ -598,6 +618,7 @@ def serialize(self) -> JsonDict:
598618
"impl": None if self.impl is None else self.impl.serialize(),
599619
"flags": get_flags(self, FUNCBASE_FLAGS),
600620
"deprecated": self.deprecated,
621+
"setter_index": self.setter_index,
601622
}
602623

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

mypy/semanal.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,7 @@ def analyze_property_with_multi_part_definition(
15431543
)
15441544
assert isinstance(setter_func_type, CallableType)
15451545
bare_setter_type = setter_func_type
1546+
defn.setter_index = i + 1
15461547
if first_node.name == "deleter":
15471548
item.func.abstract_status = first_item.func.abstract_status
15481549
for other_node in item.decorators[1:]:

test-data/unit/check-classes.test

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8736,3 +8736,23 @@ class NoopPowerResource:
87368736
def hardware_type(self) -> None: # E: Invalid property setter signature
87378737
self.hardware_type = None # Note: intentionally recursive
87388738
[builtins fixtures/property.pyi]
8739+
8740+
[case testPropertyAllowsDeleterBeforeSetter]
8741+
class C:
8742+
@property
8743+
def foo(self) -> str: ...
8744+
@foo.deleter
8745+
def foo(self) -> None: ...
8746+
@foo.setter
8747+
def foo(self, val: int) -> None: ...
8748+
8749+
@property
8750+
def bar(self) -> int: ...
8751+
@bar.deleter
8752+
def bar(self) -> None: ...
8753+
@bar.setter
8754+
def bar(self, value: int, val: int) -> None: ... # E: Invalid property setter signature
8755+
8756+
C().foo = "no" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
8757+
C().bar = "fine"
8758+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)