diff --git a/mypy/checker.py b/mypy/checker.py index fd4486218dc1..f030613fc6cc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5499,14 +5499,17 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: self.try_infer_partial_generic_type_from_assignment(s.lvalue, s.rvalue, s.op) if isinstance(s.lvalue, MemberExpr): # Special case, some additional errors may be given for - # assignments to read-only or final attributes. + # assignments to read-only or final attributes. Also, the type used + # for writing (e.g. a property setter or descriptor __set__ type) may + # differ from the type used for reading. lvalue_type = self.expr_checker.visit_member_expr(s.lvalue, True) + read_type = self.expr_checker.accept(s.lvalue) else: - lvalue_type = self.expr_checker.accept(s.lvalue) - inplace, method = infer_operator_assignment_method(lvalue_type, s.op) + lvalue_type = read_type = self.expr_checker.accept(s.lvalue) + inplace, method = infer_operator_assignment_method(read_type, s.op) if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) - rvalue_type, _ = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) + rvalue_type, _ = self.expr_checker.check_op(method, read_type, s.rvalue, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index b6eee21c50af..3b2d9ede14d6 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9370,6 +9370,111 @@ a.foo = 1 # E: Incompatible types in assignment (expression has type "int", var reveal_type(a.foo) # N: Revealed type is "builtins.int" [builtins fixtures/property.pyi] +[case testPropertySetterTypeOperatorAssignment] +from typing import Union + +class P: + def __iadd__(self, x: int) -> "P": ... +class Q: + def __iadd__(self, x: int) -> "Q": ... + +class N: + @property + def a(self) -> P: ... + @a.setter + def a(self, v: P) -> None: ... + + @property + def b(self) -> P: ... + @b.setter + def b(self, v: Union[P, Q]) -> None: ... + + c: P + + @property + def d(self) -> P: ... + +n = N() +n.a += 1 +n.b += 1 +n.c += 1 +n.d += 1 # E: Property "d" defined in "N" is read-only +n.a += "" # E: Argument 1 to "__iadd__" of "P" has incompatible type "str"; expected "int" +n.b += "" # E: Argument 1 to "__iadd__" of "P" has incompatible type "str"; expected "int" +[builtins fixtures/property.pyi] + +[case testPropertySetterTypeOperatorAssignmentInplaceMethodOnSetterType] +class P: + def __iadd__(self, x: str) -> "P": ... +class Q: + def __iadd__(self, x: int) -> "Q": ... + +class N: + @property + def a(self) -> P: ... + @a.setter + def a(self, v: Q) -> None: ... + +n = N() +n.a += "x" # E: Result type of + incompatible in assignment +n.a += 1 # E: Result type of + incompatible in assignment \ + # E: Argument 1 to "__iadd__" of "P" has incompatible type "int"; expected "str" +[builtins fixtures/property.pyi] + +[case testPropertySetterTypeOperatorAssignmentNoInplaceMethod] +class P: + def __add__(self, x: int) -> "P": ... +class Q: + def __iadd__(self, x: int) -> "Q": ... + +class N: + @property + def a(self) -> P: ... + @a.setter + def a(self, v: P) -> None: ... + + @property + def b(self) -> P: ... + @b.setter + def b(self, v: Q) -> None: ... + +n = N() +n.a += 1 +n.b += 1 # E: Incompatible types in assignment (expression has type "P", variable has type "Q") +[builtins fixtures/property.pyi] + +[case testDescriptorSetTypeOperatorAssignment] +from typing import Any, Union, overload + +class P: + def __iadd__(self, x: int) -> "P": ... +class Q: ... + +class Desc: + @overload + def __get__(self, obj: None, owner: Any) -> "Desc": ... + @overload + def __get__(self, obj: "M", owner: Any) -> P: ... + def __get__(self, obj: Union["M", None], owner: Any) -> Union[P, "Desc"]: ... + def __set__(self, obj: "M", v: Union[P, Q]) -> None: ... + +class Desc2: + @overload + def __get__(self, obj: None, owner: Any) -> "Desc2": ... + @overload + def __get__(self, obj: "M", owner: Any) -> P: ... + def __get__(self, obj: Union["M", None], owner: Any) -> Union[P, "Desc2"]: ... + def __set__(self, obj: "M", v: Q) -> None: ... + +class M: + d = Desc() + e = Desc2() + +m = M() +m.d += 1 +m.e += 1 # E: Result type of + incompatible in assignment +[builtins fixtures/property.pyi] + [case testPropertyDeleterNoSetterOK] class C: @property