Repository: python/mypy

Issue:
Daemon crash if base class adds attribute that shadows subclass
In particular, this test case fails:

```
[case testAddAttributeThroughNewBaseClass]
import a

[file a.py]
class C:
    def __init__(self) -> None:
        self.x = 0

[file a.py.2]
from b import B

class C(B):
    def __init__(self) -> None:
        self.x = 0

[file b.py.2]
class B:
    def __init__(self) -> None:
        self.x = 0
```

Here's the traceback:

```
/Users/jukka/src/mypy/mypy/test/testfinegrained.py:122: in run_case
    new_messages = self.run_check(server, sources)
/Users/jukka/src/mypy/mypy/test/testfinegrained.py:199: in run_check
    response = server.check(sources)
/Users/jukka/src/mypy/mypy/dmypy_server.py:362: in check
    res = self.fine_grained_increment(sources)
/Users/jukka/src/mypy/mypy/dmypy_server.py:459: in fine_grained_increment
    messages = self.fine_grained_manager.update(changed, removed)
/Users/jukka/src/mypy/mypy/server/update.py:237: in update
    result = self.update_one(changed_modules, initial_set, removed_set)
/Users/jukka/src/mypy/mypy/server/update.py:303: in update_one
    result = self.update_module(next_id, next_path, next_id in removed_set)
/Users/jukka/src/mypy/mypy/server/update.py:385: in update_module
    targets_with_errors=set(), processed_targets=self.processed_targets)
/Users/jukka/src/mypy/mypy/server/update.py:831: in propagate_changes_using_dependencies
    triggered |= reprocess_nodes(manager, graph, id, nodes, deps, processed_targets)
/Users/jukka/src/mypy/mypy/server/update.py:956: in reprocess_nodes
    strip_target_new(deferred.node, saved_attrs)
/Users/jukka/src/mypy/mypy/server/aststripnew.py:43: in strip_target_new
    node.accept(visitor)
/Users/jukka/src/mypy/mypy/nodes.py:663: in accept
    return visitor.visit_func_def(self)
/Users/jukka/src/mypy/mypy/server/aststripnew.py:117: in visit_func_def
    super().visit_func_def(node)
/Users/jukka/src/mypy/mypy/traverser.py:52: in visit_func_def
    self.visit_func(o)
/Users/jukka/src/mypy/mypy/traverser.py:49: in visit_func
    o.body.accept(self)
/Users/jukka/src/mypy/mypy/nodes.py:987: in accept
    return visitor.visit_block(self)
/Users/jukka/src/mypy/mypy/server/aststripnew.py:74: in visit_block
    super().visit_block(b)
/Users/jukka/src/mypy/mypy/traverser.py:37: in visit_block
    s.accept(self)
/Users/jukka/src/mypy/mypy/nodes.py:1045: in accept
    return visitor.visit_assignment_stmt(self)
/Users/jukka/src/mypy/mypy/server/aststripnew.py:147: in visit_assignment_stmt
    self.process_lvalue_in_method(lvalue)
/Users/jukka/src/mypy/mypy/server/aststripnew.py:196: in process_lvalue_in_method
    del self.type.names[lvalue.name]
E   KeyError: 'x'
```


Discussion:

Resolving patch:
diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py
--- a/mypy/server/aststrip.py
+++ b/mypy/server/aststrip.py
@@ -219,7 +219,8 @@ def process_lvalue_in_method(self, lvalue: Node) -> None:
                 # true for a MemberExpr, we know that it must be an assignment through
                 # self, since only those can define new attributes.
                 assert self.type is not None
-                del self.type.names[lvalue.name]
+                if lvalue.name in self.type.names:
+                    del self.type.names[lvalue.name]
                 key = (self.type.defn, lvalue.name)
                 if key in self.saved_class_attrs:
                     del self.saved_class_attrs[key]


Test patch:
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -9202,3 +9202,25 @@ class A: # type: ignore
 
 [out]
 ==
+
+[case testAddAttributeThroughNewBaseClass]
+import a
+
+[file a.py]
+class C:
+    def __init__(self) -> None:
+        self.x = 0
+
+[file a.py.2]
+from b import B
+
+class C(B):
+    def __init__(self) -> None:
+        self.x = 0
+
+[file b.py.2]
+class B:
+    def __init__(self) -> None:
+        self.x = 0
+[out]
+==