From 37db499dcafb8105c2b6424f21385b0073dad0a1 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 17 Oct 2021 20:32:40 +0200 Subject: [PATCH 01/15] Allow overriding an attribute with a property defined in the "decorator-style" (#4125). --- mypy/checker.py | 18 +++++++++++++++++- test-data/unit/check-classes.test | 23 +++++++++++++++++++++++ test-data/unit/check-dataclasses.test | 5 +++-- test-data/unit/check-final.test | 5 +++++ test-data/unit/check-inference.test | 6 ++++-- 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9c389a3d08d8c..db5033583aa19 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1536,7 +1536,23 @@ 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 isinstance(defn, Decorator): + if defn.var.is_settable_property: + assert isinstance(defn.var.type, CallableType) + if not is_equivalent(defn.var.type.ret_type, original_type): + self.fail('Signature of "{}" incompatible with {}'.format( + defn.name, base.name), context) + else: + self.fail('Overriding an attribute with a property requires ' + 'defining a setter method', context) + 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, diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 78d2618183808..a76bee8db2409 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1360,6 +1360,29 @@ 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 testOverrideAttributeWithProperty] +class A: + x: int + y: int + z: int +class B(A): + @property + def x(self) -> int: + ... + @x.setter + def x(self, v: int) -> None: + ... + @property + def y(self) -> int: # E: Overriding an attribute with a property requires defining a setter method + ... + @property + def z(self) -> str: # E: Signature of "z" incompatible with A + ... + @z.setter + def z(self, v: str) -> None: + ... +[builtins fixtures/property.pyi] + -- Descriptors -- ----------- diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 2e5e1cfd53838..f778309f599bf 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1245,8 +1245,9 @@ class A: @dataclass class B(A): @property - def foo(self) -> int: pass # E: Signature of "foo" incompatible with supertype "A" - + def foo(self) -> int: pass + @foo.setter + def foo(self, v: int) -> None: pass reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> __main__.B" [builtins fixtures/dataclasses.pyi] diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 063b2b8aba886..ad43d99249662 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -762,8 +762,13 @@ class B(A): class C(A): @property # E: Cannot override final attribute "x" (previously declared in base class "A") def x(self) -> None: pass + @x.setter + def x(self, v: int) -> None: pass @property # E: Cannot override final attribute "y" (previously declared in base class "A") def y(self) -> None: pass + @y.setter + def y(self, v: int) -> None: pass + [builtins fixtures/property.pyi] [out] diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 80bc40b6ca98d..3488ce4786265 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -1475,10 +1475,12 @@ class A: self.x = [] # E: Need type annotation for "x" (hint: "x: List[] = ...") class B(A): - # TODO?: This error is kind of a false positive, unfortunately @property - def x(self) -> List[int]: # E: Signature of "x" incompatible with supertype "A" + def x(self) -> List[int]: return [123] + @x.setter + def x(self, v: int) -> None: + ... [builtins fixtures/list.pyi] [case testInferSetInitializedToEmpty] From e854c9b24db841f55d821897903c810ef8e1224e Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 17 Oct 2021 22:48:53 +0200 Subject: [PATCH 02/15] fix assertion --- mypy/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index db5033583aa19..d02033a3d96bb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1540,8 +1540,8 @@ def check_method_override_for_base_with_name( if context.is_property and isinstance(original_node, Var): if isinstance(defn, Decorator): if defn.var.is_settable_property: - assert isinstance(defn.var.type, CallableType) - if not is_equivalent(defn.var.type.ret_type, original_type): + 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) else: From 99d8889af940e7b6e95c371717f199cfa7dbe9e6 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 17 Oct 2021 22:53:16 +0200 Subject: [PATCH 03/15] fix error code --- mypy/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d02033a3d96bb..36efb270cd83c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1543,10 +1543,10 @@ def check_method_override_for_base_with_name( 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) + defn.name, base.name), context, code=codes.OVERRIDE) else: self.fail('Overriding an attribute with a property requires ' - 'defining a setter method', context) + 'defining a setter method', context, code=codes.OVERRIDE) elif isinstance(defn, OverloadedFuncDef): # potential errors already reported by the checks above pass From 6e85abe5a0c1b3152191f1dcb465dd571b6f4941 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 1 Nov 2021 22:32:01 +0100 Subject: [PATCH 04/15] Try to support the "x = property(get_x, set_x)" syntax. add test cases --- test-data/unit/check-classes.test | 260 +++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index a76bee8db2409..4fa2eb92633cb 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1360,7 +1360,7 @@ 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 testOverrideAttributeWithProperty] +[case testOverrideAttributeWithDecoratedProperty] class A: x: int y: int @@ -1381,8 +1381,266 @@ class B(A): @z.setter def z(self, v: str) -> None: ... +b: B +reveal_type(b.x) # N: Revealed type is "builtins.int" +b.x = "y" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/property.pyi] + +[case testOverrideAttributeWithAssignedProperty] +class A: + x: int + y: int + z: int +class B(A): + def gx(self) -> int: + ... + def sx(self, v: int) -> None: + ... + x = property(gx, sx) + def gy(self) -> int: + ... + y = property(gy) # E: Overriding an attribute with a property requires defining a setter method + def gz(self) -> str: + ... + def sz(self, v: str) -> None: + ... + z = property(gz, sz) # E: Signature of "z" incompatible with A +b: B +reveal_type(b.x) # N: Revealed type is "builtins.int" +b.x = "y" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/property.pyi] + +[case testOverrideDecoratedPropertyWithDecoratedProperty] +class A: + @property + def v(self) -> int: + ... + @v.setter + def v(self, v: int) -> None: + ... + @property + def w(self) -> int: + ... + @w.setter + def w(self, v: int) -> None: + ... + @property + def x(self) -> int: + ... + @property + def y(self) -> int: + ... + @property + def z(self) -> int: + ... + @z.setter + def z(self, v: int) -> None: + ... +class B(A): + @property + def v(self) -> int: + ... + @w.setter + def v(self, v: int) -> None: + ... + @property # E + def w(self) -> int: + ... + @property + def x(self) -> int: + ... + @property + def y(self) -> int: + ... + @y.setter + def y(self, v: int) -> None: + ... + @property + def z(self) -> str: # E + ... + @z.setter + def z(self, v: str) -> None: + ... +b: B +reveal_type(b.v) +b.v = "w" +[out] +main:33: error: Read-only property cannot override read-write property +main:45: error: Signature of "z" incompatible with supertype "A" +main:45: note: Superclass: +main:45: note: @overload +main:45: note: def z(self) -> int +main:45: note: Subclass: +main:45: note: @overload +main:45: note: def z(self) -> str +main:46: error: Signature of "z" incompatible with supertype "A" +main:46: note: Superclass: +main:46: note: @overload +main:46: note: def z(self) -> int +main:46: note: Subclass: +main:46: note: def z(self) -> str +main:52: note: Revealed type is "builtins.int" +main:53: error: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/property.pyi] + +[case testOverrideAssignedPropertyWithDecoratedProperty] +class A: + def gv(self) -> int: + ... + def sv(self, v: int) -> None: + ... + v = property(gv, sv) + def gw(self) -> int: + ... + def sw(self, v: int) -> None: + ... + w = property(gw, sw) + def sx(self) -> int: + ... + x = property(sx) + def gy(self) -> int: + ... + y = property(gy) + def gz(self) -> int: + ... + def sz(self, v: int) -> None: + ... + z = property(gz, sz) +class B(A): + @property + def v(self) -> int: + ... + @w.setter + def v(self, v: int) -> None: + ... + @property + def w(self) -> int: # E: Read-only property cannot override read-write property + ... + @property + def x(self) -> int: + ... + @property + def y(self) -> int: + ... + @y.setter + def y(self, v: int) -> None: + ... + @property + def z(self) -> str: # E: Signature of "z" incompatible with A + ... + @z.setter + def z(self, v: str) -> None: + ... +b: B +reveal_type(b.v) # N: Revealed type is "builtins.int" +b.v = "w" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/property.pyi] + +[case testOverrideDecoratedPropertyWithAssignedProperty] +class A: + @property + def v(self) -> int: + ... + @v.setter + def v(self, v: int) -> None: + ... + @property + def w(self) -> int: + ... + @w.setter + def w(self, v: int) -> None: + ... + @property + def x(self) -> int: + ... + @property + def y(self) -> int: + ... + @property + def z(self) -> int: + ... + @z.setter + def z(self, v: int) -> None: + ... +class B(A): + def gv(self) -> int: + ... + def sv(self, v: int) -> None: + ... + v = property(gv, sv) + def gw(self) -> int: + ... + w = property(gw) # E: Read-only property cannot override read-write property + def gx(self) -> int: + ... + x = property(gx) + def gy(self) -> int: + ... + def sy(self, v: int) -> None: + ... + y = property(gy, sy) + def gz(self) -> str: + ... + def sz(self, v: str) -> None: + ... + z = property(gz, sz) # E: Signature of "z" incompatible with A +b: B +reveal_type(b.v) # N: Revealed type is "builtins.int" +b.v = "w" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [builtins fixtures/property.pyi] + +[case testOverrideAssignedPropertyWithAssignedProperty] +class A: + def gv(self) -> int: + ... + def sv(self, v: int) -> None: + ... + v = property(gv, sv) + def gw(self) -> int: + ... + def sw(self, v: int) -> None: + ... + w = property(gw, sw) + def sx(self) -> int: + ... + x = property(sx) + def gy(self) -> int: + ... + y = property(gy) + def gz(self) -> int: + ... + def sz(self, v: int) -> None: + ... + z = property(gz, sz) +class B(A): + def gv(self) -> int: + ... + def sv(self, v: int) -> None: + ... + v = property(gv, sv) + def gw(self) -> int: + ... + w = property(gw) # E: Read-only property cannot override read-write property + def gx(self) -> int: + ... + x = property(gx) + def gy(self) -> int: + ... + def sy(self, v: int) -> None: + ... + y = property(gy, sy) + def _gz(self) -> str: + ... + def _sz(self, v: str) -> None: + ... + z = property(_gz, _sz) # E: Signature of "z" incompatible with A +b: B +reveal_type(b.v) # N: Revealed type is "builtins.int" +b.v = "w" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/property.pyi] + + -- Descriptors -- ----------- From cea3e37d09303aa0d88d1efa07914d737c6a4c39 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 1 Nov 2021 22:33:33 +0100 Subject: [PATCH 05/15] Try to support the "x = property(get_x, set_x)" syntax. improve the property fixture --- test-data/unit/fixtures/property.pyi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index b3f60abaf8a0c..6019504ae05d1 100644 --- a/test-data/unit/fixtures/property.pyi +++ b/test-data/unit/fixtures/property.pyi @@ -10,7 +10,9 @@ class type: class function: pass -property = object() # Dummy definition +class property(object): + def __init__(self, fget, fset=None) -> None: + ... class dict: pass class int: pass From 43a9e10b927f4442fc00dd315c621154d561fbf6 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 1 Nov 2021 22:35:23 +0100 Subject: [PATCH 06/15] Try to support the "x = property(get_x, set_x)" syntax. add attribute `property_funcdef` to class `Var`. --- mypy/nodes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/nodes.py b/mypy/nodes.py index 1501c20514c03..0ce0092adb1c7 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -856,6 +856,7 @@ class Var(SymbolNode): 'is_classmethod', 'is_property', 'is_settable_property', + 'property_funcdef', 'is_classvar', 'is_abstract_var', 'is_final', @@ -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 = None # Optional[FuncDef] self.is_classvar = False self.is_abstract_var = False # Set to true when this variable refers to a module we were unable to From 64a32ba4b3d62d08157d8856802b3f3074949c18 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 1 Nov 2021 22:35:56 +0100 Subject: [PATCH 07/15] Try to support the "x = property(get_x, set_x)" syntax. prepare attribute `property_funcdef` of class `Var` during semantic analysis --- mypy/semanal.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index 555f04a69a62c..06d9468f5f57b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2285,6 +2285,20 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None: self.analyze_lvalue(lval, explicit_type=explicit, is_final=s.is_final_def) + if len(s.lvalues) == 1: + rval = s.rvalue + if ( + (len(s.lvalues) == 1) + and isinstance(lval.node, Var) + and isinstance(rval, CallExpr) + and (rval.callee is not None) + and refers_to_fullname(rval.callee, 'builtins.property') + ): + 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: From 7cc9e450d05d88fc712a9d5eff3cede90eeefd92 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 1 Nov 2021 22:36:38 +0100 Subject: [PATCH 08/15] Try to support the "x = property(get_x, set_x)" syntax. adjust checks in module `checker.py`. --- mypy/checker.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 36efb270cd83c..eabe221c2ef0b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1538,12 +1538,18 @@ def check_method_override_for_base_with_name( original_class_or_static = False # a variable can't be class or static if context.is_property and isinstance(original_node, Var): + if original_node.property_funcdef: + original_type = original_node.property_funcdef.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) @@ -2386,6 +2392,31 @@ 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 + base_node = base_node.items[0].var + if isinstance(base_node, Var): + if base_node.property_funcdef: + base_type = base_node.property_funcdef.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 + compare_type = compare_node.type.ret_type + if not is_equivalent(compare_type, get_proper_type(base_type)): + 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: From 3c0e5a271169ef87d528f0be41e56c37c6f27650 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 1 Nov 2021 22:36:59 +0100 Subject: [PATCH 09/15] Try to support the "x = property(get_x, set_x)" syntax. adjust checks in module `checkmember`. --- mypy/checkmember.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3579fda2e7f44..3092dc9b32b51 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -559,7 +559,10 @@ def analyze_var(name: str, """ # Found a member variable. itype = map_instance_to_supertype(itype, var.info) - typ = var.type + if var.property_funcdef: + typ = var.property_funcdef.type.ret_type + else: + typ = var.type if typ: if isinstance(typ, PartialType): return mx.chk.handle_partial_var_type(typ, mx.is_lvalue, var, mx.context) From 9ea9c9f7086f4e10015c5c5fd227206fd16ad89a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Nov 2021 08:38:49 +0100 Subject: [PATCH 10/15] Try to support the "x = property(get_x, set_x)" syntax. quick fixes to satisfy mypy_self_check --- mypy/checker.py | 27 ++++++++++++++++++--------- mypy/checkmember.py | 7 ++++--- mypy/nodes.py | 2 +- mypy/semanal.py | 29 ++++++++++++++++------------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index eabe221c2ef0b..4e2b40616c8c0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1539,7 +1539,9 @@ def check_method_override_for_base_with_name( if context.is_property and isinstance(original_node, Var): if original_node.property_funcdef: - original_type = original_node.property_funcdef.type.ret_type + 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) @@ -2397,10 +2399,15 @@ def check_compatibility_super(self, lvalue: RefExpr, lvalue_type: Optional[Type] if len(rvalue.args) < len(base_node.items): self.fail(message_registry.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE, rvalue) return False - base_node = base_node.items[0].var + 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: - base_type = base_node.property_funcdef.type.ret_type + type_ = base_node.property_funcdef.type + assert 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 @@ -2409,12 +2416,14 @@ def check_compatibility_super(self, lvalue: RefExpr, lvalue_type: Optional[Type] self.fail('Overriding an attribute with a property requires ' 'defining a setter method', rvalue) return False - compare_node = rvalue.args[0].node - compare_type = compare_node.type.ret_type - if not is_equivalent(compare_type, get_proper_type(base_type)): - self.fail('Signature of "{}" incompatible with {}'.format( - lvalue.node.name, base.name), 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) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3092dc9b32b51..b8afb44848c26 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -559,10 +559,11 @@ def analyze_var(name: str, """ # Found a member variable. itype = map_instance_to_supertype(itype, var.info) - if var.property_funcdef: - typ = var.property_funcdef.type.ret_type - else: + if not var.property_funcdef: typ = var.type + else: + assert isinstance(var.property_funcdef.type, CallableType) + typ = var.property_funcdef.type.ret_type if typ: if isinstance(typ, PartialType): return mx.chk.handle_partial_var_type(typ, mx.is_lvalue, var, mx.context) diff --git a/mypy/nodes.py b/mypy/nodes.py index 0ce0092adb1c7..ff1851b3df936 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -885,7 +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 = None # Optional[FuncDef] + 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 diff --git a/mypy/semanal.py b/mypy/semanal.py index 06d9468f5f57b..7981810bb353b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2285,19 +2285,22 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None: self.analyze_lvalue(lval, explicit_type=explicit, is_final=s.is_final_def) - if len(s.lvalues) == 1: - rval = s.rvalue - if ( - (len(s.lvalues) == 1) - and isinstance(lval.node, Var) - and isinstance(rval, CallExpr) - and (rval.callee is not None) - and refers_to_fullname(rval.callee, 'builtins.property') - ): - lval.node.is_property = True - lval.node.property_funcdef = rval.args[0].node - if len(rval.args) > 1: - lval.node.is_settable_property = True + 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: From 1b14933b9280dacf8c62e39c8202c39a0c26894a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Nov 2021 09:03:48 +0100 Subject: [PATCH 11/15] Try to support the "x = property(get_x, set_x)" syntax. fix typo in last commit --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4e2b40616c8c0..42a0a1f383a8c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2406,7 +2406,7 @@ def check_compatibility_super(self, lvalue: RefExpr, lvalue_type: Optional[Type] if isinstance(base_node, Var): if base_node.property_funcdef: type_ = base_node.property_funcdef.type - assert isinstance(type, CallableType) + assert 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) From fa3917e45a3f5bfa54070eb5f06d76b33929adbe Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Nov 2021 11:52:09 +0100 Subject: [PATCH 12/15] Try to support the "x = property(get_x, set_x)" syntax. replace assertion by isinstance --- mypy/checker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 42a0a1f383a8c..46e2fa6baab27 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2406,11 +2406,11 @@ def check_compatibility_super(self, lvalue: RefExpr, lvalue_type: Optional[Type] if isinstance(base_node, Var): if base_node.property_funcdef: type_ = base_node.property_funcdef.type - assert 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 + 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 ' From 404f23764eae937573d0848c1e48bd4b7ae43f7f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Nov 2021 11:52:44 +0100 Subject: [PATCH 13/15] Try to support the "x = property(get_x, set_x)" syntax. (lazily) complete typing of property.pyi --- test-data/unit/fixtures/property.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index 6019504ae05d1..3c50d1574b5c2 100644 --- a/test-data/unit/fixtures/property.pyi +++ b/test-data/unit/fixtures/property.pyi @@ -11,7 +11,7 @@ class type: class function: pass class property(object): - def __init__(self, fget, fset=None) -> None: + def __init__(self, fget: typing.Any, fset: typing.Any = None) -> None: ... class dict: pass From 57ddb00e35cc0f13c772d87c2649c164b4207795 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Nov 2021 16:53:57 +0100 Subject: [PATCH 14/15] Try to support the "x = property(get_x, set_x)" syntax. clearer assertion in `checkmember`. --- mypy/checkmember.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index b8afb44848c26..b11691622c853 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -561,9 +561,12 @@ def analyze_var(name: str, itype = map_instance_to_supertype(itype, var.info) if not var.property_funcdef: typ = var.type - else: - assert isinstance(var.property_funcdef.type, CallableType) + elif isinstance(var.property_funcdef.type, CallableType): typ = var.property_funcdef.type.ret_type + elif not var.property_funcdef.type: + typ = None + 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) From 906ef6aada4550b4a431bdd27a577fdd7f0a68fc Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 3 Nov 2021 05:53:40 +0100 Subject: [PATCH 15/15] Try to support the "x = property(get_x, set_x)" syntax. Quick fix for "Cannot determine type of" errors with suitable test extensions. (so far, skipping get_attribute_hook) --- mypy/checkmember.py | 4 +++- test-data/unit/check-classes.test | 28 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index b11691622c853..9f02deb9d2ea6 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -564,7 +564,9 @@ def analyze_var(name: str, elif isinstance(var.property_funcdef.type, CallableType): typ = var.property_funcdef.type.ret_type elif not var.property_funcdef.type: - typ = None + 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: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 4fa2eb92633cb..6d9d69b587d13 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1278,20 +1278,38 @@ main:8: note: def f(self) -> None [case testAccessingReadOnlyProperty] -import typing class A: @property - def f(self) -> str: pass + def x1(self) -> str: pass + def gx2(self) -> str: pass + x2 = property(gx2) + @property + def x3(self): pass + def gx4(self): pass + x4 = property(gx4) a = A() -reveal_type(a.f) # N: Revealed type is "builtins.str" +reveal_type(a.x1) # N: Revealed type is "builtins.str" +reveal_type(a.x2) # N: Revealed type is "builtins.str" +reveal_type(a.x3) # N: Revealed type is "Any" +reveal_type(a.x4) # N: Revealed type is "Any" [builtins fixtures/property.pyi] [case testAssigningToReadOnlyProperty] import typing class A: @property - def f(self) -> str: pass -A().f = '' # E: Property "f" defined in "A" is read-only + def x1(self) -> str: pass + def gx2(self) -> str: pass + x2 = property(gx2) + @property + def x3(self): pass + def gx4(self): pass + x4 = property(gx4) +a = A() +a.x1 = '' # E: Property "x1" defined in "A" is read-only +a.x2 = '' # E: Property "x2" defined in "A" is read-only +a.x3 = '' # E: Property "x3" defined in "A" is read-only +a.x4 = '' # E: Property "x4" defined in "A" is read-only [builtins fixtures/property.pyi] [case testAssigningToInheritedReadOnlyProperty]