diff --git a/mypy/checker.py b/mypy/checker.py index 9c389a3d08d8..46e2fa6baab2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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, @@ -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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3579fda2e7f4..9f02deb9d2ea 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -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) diff --git a/mypy/nodes.py b/mypy/nodes.py index 1501c20514c0..ff1851b3df93 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: 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 555f04a69a62..7981810bb353 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 78d261818380..6d9d69b587d1 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] @@ -1360,6 +1378,287 @@ 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 testOverrideAttributeWithDecoratedProperty] +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: + ... +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 -- ----------- diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 2e5e1cfd5383..f778309f599b 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 063b2b8aba88..ad43d9924966 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 80bc40b6ca98..3488ce478626 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] diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index b3f60abaf8a0..3c50d1574b5c 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: typing.Any, fset: typing.Any = None) -> None: + ... class dict: pass class int: pass