Repository: python/mypy

Issue:
class with __setattr__ reports properties are read-only
When a class implements `__setattr__`, mypy still reports properties as read-only.

Minimum example:

````
class Foo:
    def __init__(self) -> None:
        self._d = {'a': 1}

    def __setattr__(self, name: str, value: Any) -> None:
        if hasattr(self, '_d'):
            self._d[name] = value
        else:
            super(self.__class__, self).__setattr__(name, value)

    @property
    def a(self) -> int:
        return self._d['a']

f = Foo()
f.a = 2
````

The error points to the last line in the above code: `error: Property "a" defined in "Foo" is read-only`.

Expectation: Since `Foo` implements `__setattr__`, mypy should allow the attribute to be set, even though it overlaps with a class property.

There are no issues identified if the property `a` is removed, and I am using version 0.781.


Discussion:
Yeah, this looks like false positive.
Anybody want to contribute a fix -- it should be easy?

Add a check about `__setattr__` in `mypy.checkmember.analyze_var` (before the call to `mx.msg.read_only_property`).
@JukkaL I can take care of this.

Resolving patch:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -550,7 +550,8 @@ def analyze_var(name: str,
             return mx.chk.handle_partial_var_type(typ, mx.is_lvalue, var, mx.context)
         if mx.is_lvalue and var.is_property and not var.is_settable_property:
             # TODO allow setting attributes in subclass (although it is probably an error)
-            mx.msg.read_only_property(name, itype.type, mx.context)
+            if info.get('__setattr__') is None:
+                mx.msg.read_only_property(name, itype.type, mx.context)
         if mx.is_lvalue and var.is_classvar:
             mx.msg.cant_assign_to_classvar(name, mx.context)
         t = get_proper_type(expand_type_by_instance(typ, itype))
@@ -560,7 +561,8 @@ def analyze_var(name: str,
             if mx.is_lvalue:
                 if var.is_property:
                     if not var.is_settable_property:
-                        mx.msg.read_only_property(name, itype.type, mx.context)
+                        if info.get('__setattr__') is None:
+                            mx.msg.read_only_property(name, itype.type, mx.context)
                 else:
                     mx.msg.cant_assign_to_method(mx.context)
 


Test patch:
diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py
--- a/mypy/test/testcheck.py
+++ b/mypy/test/testcheck.py
@@ -90,6 +90,7 @@
     'check-reports.test',
     'check-errorcodes.test',
     'check-annotated.test',
+    'check-setattr.test',
     'check-parameter-specification.test',
 ]
 
diff --git a/test-data/unit/check-setattr.test b/test-data/unit/check-setattr.test
new file mode 100644
--- /dev/null
+++ b/test-data/unit/check-setattr.test
@@ -0,0 +1,39 @@
+[case testSetAttrWithProperty]
+from typing import Any
+class Foo:
+    _a = 1
+    def __setattr__(self, name: str, value: Any) -> None:
+        self._a = value
+    @property
+    def a(self) -> int:
+        return self._a
+f = Foo()
+f.a = 2
+[builtins fixtures/property.pyi]
+
+[case testInheritedSetAttrWithProperty]
+from typing import Any
+class Foo:
+    _a = 1
+    def __setattr__(self, name: str, value: Any) -> None:
+        self._a = value
+class Bar(Foo):
+    @property
+    def a(self) -> int:
+        return self._a
+f = Bar()
+f.a = 2
+[builtins fixtures/property.pyi]
+
+[case testSetAttrWithIncompatibleType]
+from typing import Any
+class Foo:
+    _a = 1
+    def __setattr__(self, name: str, value: Any) -> None:
+        self._a = value
+    @property
+    def a(self) -> int:
+        return self._a
+f = Foo()
+f.a = 'hello'  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+[builtins fixtures/property.pyi]