Skip to content

Commit 40dd719

Browse files
authored
Allow overriding attribute with a settable property (#13475)
Fixes #4125 Previously the code compared the original signatures for properties. Now we compare just the return types, similar to how we do it in `checkmember.py`. Note that we still only allow invariant overrides, which is stricter that for regular variables that where we allow (unsafe) covariance.
1 parent 6208400 commit 40dd719

File tree

5 files changed

+75
-9
lines changed

5 files changed

+75
-9
lines changed

mypy/checker.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
ReturnStmt,
116116
StarExpr,
117117
Statement,
118+
SymbolNode,
118119
SymbolTable,
119120
SymbolTableNode,
120121
TempNode,
@@ -1720,6 +1721,7 @@ def check_method_override_for_base_with_name(
17201721
context = defn.func
17211722

17221723
# Construct the type of the overriding method.
1724+
# TODO: this logic is much less complete than similar one in checkmember.py
17231725
if isinstance(defn, (FuncDef, OverloadedFuncDef)):
17241726
typ: Type = self.function_type(defn)
17251727
override_class_or_static = defn.is_class or defn.is_static
@@ -1769,15 +1771,37 @@ def check_method_override_for_base_with_name(
17691771
original_class_or_static = fdef.is_class or fdef.is_static
17701772
else:
17711773
original_class_or_static = False # a variable can't be class or static
1774+
1775+
if isinstance(original_type, FunctionLike):
1776+
original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base)
1777+
if original_node and is_property(original_node):
1778+
original_type = get_property_type(original_type)
1779+
1780+
if isinstance(typ, FunctionLike) and is_property(defn):
1781+
typ = get_property_type(typ)
1782+
if (
1783+
isinstance(original_node, Var)
1784+
and not original_node.is_final
1785+
and (not original_node.is_property or original_node.is_settable_property)
1786+
and isinstance(defn, Decorator)
1787+
):
1788+
# We only give an error where no other similar errors will be given.
1789+
if not isinstance(original_type, AnyType):
1790+
self.msg.fail(
1791+
"Cannot override writeable attribute with read-only property",
1792+
# Give an error on function line to match old behaviour.
1793+
defn.func,
1794+
code=codes.OVERRIDE,
1795+
)
1796+
17721797
if isinstance(original_type, AnyType) or isinstance(typ, AnyType):
17731798
pass
17741799
elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike):
1775-
original = self.bind_and_map_method(base_attr, original_type, defn.info, base)
17761800
# Check that the types are compatible.
17771801
# TODO overloaded signatures
17781802
self.check_override(
17791803
typ,
1780-
original,
1804+
original_type,
17811805
defn.name,
17821806
name,
17831807
base.name,
@@ -1792,8 +1816,8 @@ def check_method_override_for_base_with_name(
17921816
#
17931817
pass
17941818
elif (
1795-
base_attr.node
1796-
and not self.is_writable_attribute(base_attr.node)
1819+
original_node
1820+
and not self.is_writable_attribute(original_node)
17971821
and is_subtype(typ, original_type)
17981822
):
17991823
# If the attribute is read-only, allow covariance
@@ -4311,7 +4335,8 @@ def visit_decorator(self, e: Decorator) -> None:
43114335
if len([k for k in sig.arg_kinds if k.is_required()]) > 1:
43124336
self.msg.fail("Too many arguments for property", e)
43134337
self.check_incompatible_property_override(e)
4314-
if e.func.info and not e.func.is_dynamic():
4338+
# For overloaded functions we already checked override for overload as a whole.
4339+
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
43154340
self.check_method_override(e)
43164341

43174342
if e.func.info and e.func.name in ("__init__", "__new__"):
@@ -6066,6 +6091,8 @@ def conditional_types_with_intersection(
60666091
def is_writable_attribute(self, node: Node) -> bool:
60676092
"""Check if an attribute is writable"""
60686093
if isinstance(node, Var):
6094+
if node.is_property and not node.is_settable_property:
6095+
return False
60696096
return True
60706097
elif isinstance(node, OverloadedFuncDef) and node.is_property:
60716098
first_item = cast(Decorator, node.items[0])
@@ -6973,6 +7000,23 @@ def is_static(func: FuncBase | Decorator) -> bool:
69737000
assert False, f"Unexpected func type: {type(func)}"
69747001

69757002

7003+
def is_property(defn: SymbolNode) -> bool:
7004+
if isinstance(defn, Decorator):
7005+
return defn.func.is_property
7006+
if isinstance(defn, OverloadedFuncDef):
7007+
if defn.items and isinstance(defn.items[0], Decorator):
7008+
return defn.items[0].func.is_property
7009+
return False
7010+
7011+
7012+
def get_property_type(t: ProperType) -> ProperType:
7013+
if isinstance(t, CallableType):
7014+
return get_proper_type(t.ret_type)
7015+
if isinstance(t, Overloaded):
7016+
return get_proper_type(t.items[0].ret_type)
7017+
return t
7018+
7019+
69767020
def is_subtype_no_promote(left: Type, right: Type) -> bool:
69777021
return is_subtype(left, right, ignore_promotions=True)
69787022

test-data/unit/check-abstract.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ class A(metaclass=ABCMeta):
789789
def x(self) -> int: pass
790790
class B(A):
791791
@property
792-
def x(self) -> str: pass # E: Return type "str" of "x" incompatible with return type "int" in supertype "A"
792+
def x(self) -> str: pass # E: Signature of "x" incompatible with supertype "A"
793793
b = B()
794794
b.x() # E: "str" not callable
795795
[builtins fixtures/property.pyi]

test-data/unit/check-classes.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7366,3 +7366,26 @@ class D(C[List[T]]): ...
73667366
di: D[int]
73677367
reveal_type(di) # N: Revealed type is "Tuple[builtins.list[builtins.int], builtins.list[builtins.int], fallback=__main__.D[builtins.int]]"
73687368
[builtins fixtures/tuple.pyi]
7369+
7370+
[case testOverrideAttrWithSettableProperty]
7371+
class Foo:
7372+
def __init__(self) -> None:
7373+
self.x = 42
7374+
7375+
class Bar(Foo):
7376+
@property
7377+
def x(self) -> int: ...
7378+
@x.setter
7379+
def x(self, value: int) -> None: ...
7380+
[builtins fixtures/property.pyi]
7381+
7382+
[case testOverrideAttrWithSettablePropertyAnnotation]
7383+
class Foo:
7384+
x: int
7385+
7386+
class Bar(Foo):
7387+
@property
7388+
def x(self) -> int: ...
7389+
@x.setter
7390+
def x(self, value: int) -> None: ...
7391+
[builtins fixtures/property.pyi]

test-data/unit/check-dataclasses.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1286,7 +1286,7 @@ class A:
12861286
@dataclass
12871287
class B(A):
12881288
@property
1289-
def foo(self) -> int: pass # E: Signature of "foo" incompatible with supertype "A"
1289+
def foo(self) -> int: pass
12901290

12911291
reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> __main__.B"
12921292

test-data/unit/check-inference.test

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,9 +1475,8 @@ class A:
14751475
self.x = [] # E: Need type annotation for "x" (hint: "x: List[<type>] = ...")
14761476

14771477
class B(A):
1478-
# TODO?: This error is kind of a false positive, unfortunately
14791478
@property
1480-
def x(self) -> List[int]: # E: Signature of "x" incompatible with supertype "A"
1479+
def x(self) -> List[int]: # E: Cannot override writeable attribute with read-only property
14811480
return [123]
14821481
[builtins fixtures/list.pyi]
14831482

0 commit comments

Comments
 (0)