Skip to content
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
65 changes: 64 additions & 1 deletion mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,70 @@ def fix_function_overloads(self, stmts: list[Statement]) -> list[Statement]:
ret.append(last_if_overload)
elif last_if_stmt is not None:
ret.append(last_if_stmt)
return ret
return self._merge_non_adjacent_property_overloads(ret)

def _merge_non_adjacent_property_overloads(self, stmts: list[Statement]) -> list[Statement]:
"""Merge non-adjacent @x.setter and @x.deleter into their property getter.

fix_function_overloads only groups *consecutive* same-named decorators.
When a property getter and its setter/deleter are separated by other
statements, the setter/deleter ends up as an isolated Decorator in the
output. This second pass finds those stray components and folds them
back into the property's OverloadedFuncDef (or promotes the lone getter
Decorator to an OverloadedFuncDef).

See https://github.com/python/mypy/issues/1465.
"""
# Build a map: property name -> index of its getter / OverloadedFuncDef in stmts.
prop_getter_pos: dict[str, int] = {}
for i, stmt in enumerate(stmts):
if isinstance(stmt, Decorator):
if any(isinstance(d, NameExpr) and d.name == "property" for d in stmt.decorators):
prop_getter_pos[stmt.name] = i
elif isinstance(stmt, OverloadedFuncDef) and stmt.items:
first = stmt.items[0]
if isinstance(first, Decorator) and any(
isinstance(d, NameExpr) and d.name == "property" for d in first.decorators
):
prop_getter_pos[first.name] = i

if not prop_getter_pos:
return stmts

# Find setter/deleter Decorators whose getter appeared earlier, then merge.
result: list[Statement | None] = list(stmts)
for i, stmt in enumerate(stmts):
if not isinstance(stmt, Decorator):
continue
# Identify @prop_name.setter or @prop_name.deleter
prop_name: str | None = None
for d in stmt.decorators:
if (
isinstance(d, MemberExpr)
and isinstance(d.expr, NameExpr)
and d.name in {"setter", "deleter"}
):
prop_name = d.expr.name
break
if prop_name is None:
continue
getter_pos = prop_getter_pos.get(prop_name)
if getter_pos is None or getter_pos >= i:
# No matching getter found earlier in the same scope.
continue
# This is a non-adjacent setter/deleter; merge it into the getter's node.
existing = result[getter_pos]
if isinstance(existing, Decorator):
# Promote the lone getter Decorator to an OverloadedFuncDef.
ovl = OverloadedFuncDef([existing, stmt])
ovl.set_line(existing)
result[getter_pos] = ovl
elif isinstance(existing, OverloadedFuncDef):
existing.items.append(stmt)
existing.unanalyzed_items.append(stmt)
result[i] = None # Remove from its original (non-adjacent) position.

return [s for s in result if s is not None]

def _check_ifstmt_for_overloads(
self, stmt: IfStmt, current_overload_name: str | None = None
Expand Down
53 changes: 53 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,59 @@ a.f = a.f # E: Property "f" defined in "A" is read-only
a.f.x # E: "int" has no attribute "x"
[builtins fixtures/property.pyi]

[case testPropertyWithNonAdjacentSetter]
# Regression test for https://github.com/python/mypy/issues/1465
# @f.setter need not be immediately after @property def f.
class A:
@property
def f(self) -> int:
return 1
def other(self) -> None:
pass
@f.setter
def f(self, x: int) -> None:
pass
a = A()
a.f = a.f
a.f = 1
a.f = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
reveal_type(a.f) # N: Revealed type is "builtins.int"
[builtins fixtures/property.pyi]

[case testPropertyWithNonAdjacentDeleter]
class A:
@property
def f(self) -> int:
return 1
def other(self) -> None:
pass
@f.deleter
def f(self) -> None:
pass
a = A()
a.f = a.f # E: Property "f" defined in "A" is read-only
[builtins fixtures/property.pyi]

[case testPropertyWithNonAdjacentSetterAndDeleter]
class A:
@property
def f(self) -> int:
return 1
def other(self) -> None:
pass
@f.setter
def f(self, x: int) -> None:
pass
def another(self) -> None:
pass
@f.deleter
def f(self) -> None:
pass
a = A()
a.f = 1
a.f = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
[builtins fixtures/property.pyi]

-- Descriptors
-- -----------

Expand Down