Skip to content

Allow overriding an attribute with a property (#4125) #11349

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

Closed
Closed
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
58 changes: 57 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,31 @@ def check_method_override_for_base_with_name(
original_class_or_static = fdef.is_class or fdef.is_static
else:
original_class_or_static = False # a variable can't be class or static
if isinstance(original_type, AnyType) or isinstance(typ, AnyType):

if context.is_property and isinstance(original_node, Var):
if original_node.property_funcdef:
type_ = original_node.property_funcdef.type
assert isinstance(type_, CallableType)
original_type = get_proper_type(type_.ret_type)
if isinstance(defn, Decorator):
if defn.var.is_settable_property:
assert isinstance(typ, CallableType)
if not is_equivalent(typ.ret_type, original_type):
self.fail('Signature of "{}" incompatible with {}'.format(
defn.name, base.name), context, code=codes.OVERRIDE)
elif original_node.property_funcdef:
if original_node.is_settable_property:
self.fail(message_registry.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE,
context, code=codes.OVERRIDE)
else:
self.fail('Overriding an attribute with a property requires '
'defining a setter method', context, code=codes.OVERRIDE)
elif isinstance(defn, OverloadedFuncDef):
# potential errors already reported by the checks above
pass
else:
assert False, 'should be unreachable'
elif isinstance(original_type, AnyType) or isinstance(typ, AnyType):
pass
elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike):
original = self.bind_and_map_method(base_attr, original_type,
Expand Down Expand Up @@ -2370,6 +2394,38 @@ def check_compatibility_super(self, lvalue: RefExpr, lvalue_type: Optional[Type]
if isinstance(compare_node, Decorator):
compare_node = compare_node.func

if isinstance(rvalue, CallExpr) and refers_to_fullname(rvalue.callee, 'builtins.property'):
if isinstance(base_node, OverloadedFuncDef) and base_node.is_property:
if len(rvalue.args) < len(base_node.items):
self.fail(message_registry.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE, rvalue)
return False
if base_node.items:
item = base_node.items[0]
assert isinstance(item, Decorator)
base_node = item.var
if isinstance(base_node, Var):
if base_node.property_funcdef:
type_ = base_node.property_funcdef.type
if isinstance(type_, CallableType):
base_type = type_.ret_type
if (len(rvalue.args) < 2) and base_node.is_settable_property:
self.fail(message_registry.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE, rvalue)
return False
else:
if len(rvalue.args) < 2:
self.fail('Overriding an attribute with a property requires '
'defining a setter method', rvalue)
return False
compare_node = rvalue.args[0].node # type: ignore[attr-defined]
if isinstance(compare_node, FuncDef) and isinstance(compare_node.type, CallableType):
compare_type = compare_node.type.ret_type
if not is_equivalent(compare_type, get_proper_type(base_type)):
assert isinstance(lvalue.node, Var)
self.fail('Signature of "{}" incompatible with {}'.format(
lvalue.node.name, base.name), rvalue)
return False
return True

base_type = get_proper_type(base_type)
compare_type = get_proper_type(compare_type)
if compare_type:
Expand Down
11 changes: 10 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,16 @@ def analyze_var(name: str,
"""
# Found a member variable.
itype = map_instance_to_supertype(itype, var.info)
typ = var.type
if not var.property_funcdef:
typ = var.type
elif isinstance(var.property_funcdef.type, CallableType):
typ = var.property_funcdef.type.ret_type
elif not var.property_funcdef.type:
if mx.is_lvalue and not var.is_settable_property:
mx.msg.read_only_property(name, itype.type, mx.context)
return AnyType(TypeOfAny.special_form)
else:
assert False, str(type(var.property_funcdef.type))
if typ:
if isinstance(typ, PartialType):
return mx.chk.handle_partial_var_type(typ, mx.is_lvalue, var, mx.context)
Expand Down
2 changes: 2 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ class Var(SymbolNode):
'is_classmethod',
'is_property',
'is_settable_property',
'property_funcdef',
'is_classvar',
'is_abstract_var',
'is_final',
Expand Down Expand Up @@ -884,6 +885,7 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
self.is_classmethod = False
self.is_property = False
self.is_settable_property = False
self.property_funcdef: Optional[FuncDef] = None
self.is_classvar = False
self.is_abstract_var = False
# Set to true when this variable refers to a module we were unable to
Expand Down
17 changes: 17 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,23 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None:
self.analyze_lvalue(lval,
explicit_type=explicit,
is_final=s.is_final_def)
rval = s.rvalue
if (
(len(s.lvalues) == 1)
and isinstance(lval, NameExpr)
and isinstance(lval.node, Var)
and isinstance(rval, CallExpr)
and (rval.callee is not None)
and refers_to_fullname(rval.callee, 'builtins.property')
and rval.args
and isinstance(rval.args[0], NameExpr)
and isinstance(rval.args[0].node, FuncDef)
):
lval.node.is_property = True
lval.node.property_funcdef = rval.args[0].node
if len(rval.args) > 1:
lval.node.is_settable_property = True


def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None:
if len(s.lvalues) > 1:
Expand Down
Loading