Repository: python/mypy

Issue:
dataclass inheritance: deeply nested InitVar overrides crash mypy
This is related to the fix I provided in #8159 - That fix works as long as you don't inherit from the final subclass created there and set `init=False` on the inheritor.

The issue is that when an InitVar is found in a parent class but isn't present in that parent's `__init__` method, mypy should to keep walking the MRO to find the parent's parent (etc...) until it finds the actual definition to add to `info` before it adds the attribute to the set of `known_attrs`. Currently it adds the attribute to `known_attrs` even if it wasn't added to `info`, which results in an assertion error.

I believe I have a fix for this, I'll open a PR with my fix (and a new test!)

Please provide more information to help us understand the issue:

* Are you reporting a bug, or opening a feature request?
bug

* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.

```python
from dataclasses import dataclass, field, InitVar
@dataclass
class A:
    a: InitVar[bool]
    _a: bool = field(init=False)

    def __post_init__(self, a: bool):
        self._a = a

@dataclass(init=False)
class B(A):
    def __init__(self):
        super().__init__(True)

@dataclass(init=False)
class C(B):
    pass
```

* What is the actual behavior/output?
Mypy crashes
* What is the behavior/output you expect?
Mypy should not crash (and this should pass type checking)
* What are the versions of mypy and Python you are using?
  Do you see the same issue after installing mypy from Git master?
Python 3.6.5
This is on the current git master of mypy (9101707bd)

* What are the mypy flags you are using? (For example --strict-optional)
None required

* If mypy crashed with a traceback, please paste
  the full traceback below.

```
crash_mypy.py:16: error: INTERNAL ERROR -- Please try using mypy master on Github:
https://mypy.rtfd.io/en/latest/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.770+dev.c6ebe764da96ec6671c3549f7f513237616898dd
Traceback (most recent call last):
  File "/Users/jake/.pyenv/versions/dbt36/bin/mypy", line 11, in <module>
    load_entry_point('mypy', 'console_scripts', 'mypy')()
  File "/Users/jake/src/fishtown/mypy/mypy/__main__.py", line 8, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "/Users/jake/src/fishtown/mypy/mypy/main.py", line 89, in main
    res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
  File "/Users/jake/src/fishtown/mypy/mypy/build.py", line 167, in build
    sources, options, alt_lib_path, flush_errors, fscache, stdout, stderr, extra_plugins
  File "/Users/jake/src/fishtown/mypy/mypy/build.py", line 235, in _build
    graph = dispatch(sources, manager, stdout)
  File "/Users/jake/src/fishtown/mypy/mypy/build.py", line 2611, in dispatch
    process_graph(graph, manager)
  File "/Users/jake/src/fishtown/mypy/mypy/build.py", line 2918, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/jake/src/fishtown/mypy/mypy/build.py", line 3011, in process_stale_scc
    mypy.semanal_main.semantic_analysis_for_scc(graph, scc, manager.errors)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal_main.py", line 77, in semantic_analysis_for_scc
    process_top_levels(graph, scc, patches)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal_main.py", line 202, in process_top_levels
    patches)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal_main.py", line 330, in semantic_analyze_target
    active_type=active_type)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 378, in refresh_partial
    self.refresh_top_level(node)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 389, in refresh_top_level
    self.accept(d)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 4695, in accept
    node.accept(self)
  File "/Users/jake/src/fishtown/mypy/mypy/nodes.py", line 939, in accept
    return visitor.visit_class_def(self)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 1029, in visit_class_def
    self.analyze_class(defn)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 1106, in analyze_class
    self.analyze_class_body_common(defn)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 1115, in analyze_class_body_common
    self.apply_class_plugin_hooks(defn)
  File "/Users/jake/src/fishtown/mypy/mypy/semanal.py", line 1161, in apply_class_plugin_hooks
    hook(ClassDefContext(defn, decorator, self))
  File "/Users/jake/src/fishtown/mypy/mypy/plugins/dataclasses.py", line 383, in dataclass_class_maker_callback
    transformer.transform()
  File "/Users/jake/src/fishtown/mypy/mypy/plugins/dataclasses.py", line 87, in transform
    assert attr.is_init_var and not attr.is_in_init
```



Discussion:

Resolving patch:
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -311,9 +311,13 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
                         superclass_init = info.get_method('__init__')
                         if isinstance(superclass_init, FuncDef):
                             attr_node = _get_arg_from_init(superclass_init, attr.name)
-                            if attr_node is not None:
+                            if attr_node is None:
+                                # Continue the loop: we will look it up in the next MRO entry.
+                                # Don't add it to the known or super attrs because we don't know
+                                # anything about it yet
+                                continue
+                            else:
                                 cls.info.names[attr.name] = attr_node
-
                     known_attrs.add(name)
                     super_attrs.append(attr)
                 elif all_attrs:


Test patch:
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -104,6 +104,34 @@ reveal_type(C)  # N: Revealed type is 'def (b: builtins.bool, a: builtins.bool)
 
 [builtins fixtures/bool.pyi]
 
+[case testDataclassesDeepInitVarInheritance]
+from dataclasses import dataclass, field, InitVar
+@dataclass
+class A:
+    a: bool
+
+@dataclass
+class B:
+    b: InitVar[bool]
+    _b: bool = field(init=False)
+
+    def __post_init__(self, b: bool):
+        self._b = b
+
+@dataclass(init=False)
+class C(B):
+    def __init__(self):
+        super().__init__(True)
+
+@dataclass
+class D(C):
+    pass
+
+reveal_type(C)  # N: Revealed type is 'def () -> __main__.C'
+reveal_type(D)  # N: Revealed type is 'def (b: builtins.bool) -> __main__.D'
+
+[builtins fixtures/bool.pyi]
+
 [case testDataclassesOverriding]
 # flags: --python-version 3.6
 from dataclasses import dataclass