Repository: python/mypy

Issue:
Overriding __init__ to not take an InitVar argument crashes mypy
Please provide more information to help us understand the issue:

* Are you reporting a bug, or opening a feature request?

A 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
import dataclasses

@dataclasses.dataclass
class MyClass:
    a: dataclasses.InitVar[int]
    _a: int = dataclasses.field(init=False)

    def __post_init__(self, a):
        self._a = a

@dataclasses.dataclass(init=False)
class MySubClass(MyClass):
    b: dataclasses.InitVar[int]
    _b: int = dataclasses.field(init=False)

    def __init__(self, b):
        super().__init__(b+1)
        self._b = b
```

* What is the actual behavior/output?

Mypy crashes with an assertion error.

* What is the behavior/output you expect?
I expect type checking to happen, etc.

* What are the versions of mypy and Python you are using?
`mypy 0.750+dev.596cde6bd8ddcdbbe5ca2b0d0019ec689a7e8b9c`
`Python 3.8.0`

If I downgrade to 0.740, I see the same error.

* What are the mypy flags you are using? (For example --strict-optional)

No flags.

* If mypy crashed with a traceback, please paste
  the full traceback below.
```
crash_mypy_2.py:14: 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.750+dev.596cde6bd8ddcdbbe5ca2b0d0019ec689a7e8b9c
Traceback (most recent call last):
  File "/Users/jake/.pyenv/versions/dbt38/bin/mypy", line 11, in <module>
    load_entry_point('mypy', 'console_scripts', 'mypy')()
  File "/Users/jake/src/mypy/mypy/__main__.py", line 8, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "/Users/jake/src/mypy/mypy/main.py", line 89, in main
    res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
  File "/Users/jake/src/mypy/mypy/build.py", line 166, in build
    result = _build(
  File "/Users/jake/src/mypy/mypy/build.py", line 235, in _build
    graph = dispatch(sources, manager, stdout)
  File "/Users/jake/src/mypy/mypy/build.py", line 2620, in dispatch
    process_graph(graph, manager)
  File "/Users/jake/src/mypy/mypy/build.py", line 2929, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/jake/src/mypy/mypy/build.py", line 3022, in process_stale_scc
    mypy.semanal_main.semantic_analysis_for_scc(graph, scc, manager.errors)
  File "/Users/jake/src/mypy/mypy/semanal_main.py", line 77, in semantic_analysis_for_scc
    process_top_levels(graph, scc, patches)
  File "/Users/jake/src/mypy/mypy/semanal_main.py", line 198, in process_top_levels
    deferred, incomplete, progress = semantic_analyze_target(next_id, state,
  File "/Users/jake/src/mypy/mypy/semanal_main.py", line 325, in semantic_analyze_target
    analyzer.refresh_partial(refresh_node,
  File "/Users/jake/src/mypy/mypy/semanal.py", line 378, in refresh_partial
    self.refresh_top_level(node)
  File "/Users/jake/src/mypy/mypy/semanal.py", line 389, in refresh_top_level
    self.accept(d)
  File "/Users/jake/src/mypy/mypy/semanal.py", line 4659, in accept
    node.accept(self)
  File "/Users/jake/src/mypy/mypy/nodes.py", line 939, in accept
    return visitor.visit_class_def(self)
  File "/Users/jake/src/mypy/mypy/semanal.py", line 1029, in visit_class_def
    self.analyze_class(defn)
  File "/Users/jake/src/mypy/mypy/semanal.py", line 1106, in analyze_class
    self.analyze_class_body_common(defn)
  File "/Users/jake/src/mypy/mypy/semanal.py", line 1115, in analyze_class_body_common
    self.apply_class_plugin_hooks(defn)
  File "/Users/jake/src/mypy/mypy/semanal.py", line 1161, in apply_class_plugin_hooks
    hook(ClassDefContext(defn, decorator, self))
  File "/Users/jake/src/mypy/mypy/plugins/dataclasses.py", line 374, in dataclass_class_maker_callback
    transformer.transform()
  File "/Users/jake/src/mypy/mypy/plugins/dataclasses.py", line 87, in transform
    assert attr.is_init_var and not attr.is_in_init
```


Discussion:
Thanks for reporting! It looks like a bug in the attributes bookkeeping logic (or maybe there should actually be an `or` in that assert?)
I'm very unfamiliar with mypy's inner workings, but I think that assert needs to include a check that `init=True` at the dataclass level. That check is totally reasonable if python is creating the `__init__` function.

Resolving patch:
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -290,7 +290,6 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
         # copy() because we potentially modify all_attrs below and if this code requires debugging
         # we'll have unmodified attrs laying around.
         all_attrs = attrs.copy()
-        init_method = cls.info.get_method('__init__')
         for info in cls.info.mro[1:-1]:
             if 'dataclass' not in info.metadata:
                 continue
@@ -303,15 +302,17 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
                 name = data['name']  # type: str
                 if name not in known_attrs:
                     attr = DataclassAttribute.deserialize(info, data)
-                    if attr.is_init_var and isinstance(init_method, FuncDef):
+                    if attr.is_init_var:
                         # InitVars are removed from classes so, in order for them to be inherited
                         # properly, we need to re-inject them into subclasses' sym tables here.
                         # To do that, we look 'em up from the parents' __init__.  These variables
                         # are subsequently removed from the sym table at the end of
                         # DataclassTransformer.transform.
-                        for arg, arg_name in zip(init_method.arguments, init_method.arg_names):
-                            if arg_name == attr.name:
-                                cls.info.names[attr.name] = SymbolTableNode(MDEF, arg.variable)
+                        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:
+                                cls.info.names[attr.name] = attr_node
 
                     known_attrs.add(name)
                     super_attrs.append(attr)
@@ -367,6 +368,14 @@ def _freeze(self, attributes: List[DataclassAttribute]) -> None:
                 info.names[var.name] = SymbolTableNode(MDEF, var)
 
 
+def _get_arg_from_init(init_method: FuncDef, attr_name: str) -> Optional[SymbolTableNode]:
+    """Given an init method and an attribute name, find the Var in the init method's args."""
+    for arg, arg_name in zip(init_method.arguments, init_method.arg_names):
+        if arg_name == attr_name:
+            return SymbolTableNode(MDEF, arg.variable)
+    return None
+
+
 def dataclass_class_maker_callback(ctx: ClassDefContext) -> None:
     """Hooks into the class typechecking process to add support for dataclasses.
     """


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
@@ -80,6 +80,28 @@ reveal_type(D)  # N: Revealed type is 'def (a: builtins.int, b: builtins.int, c:
 
 [builtins fixtures/list.pyi]
 
+[case testDataclassesMultipleInheritance]
+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
+class C(A, B):
+    pass
+
+reveal_type(C)  # N: Revealed type is 'def (b: builtins.bool, a: builtins.bool) -> __main__.C'
+
+[builtins fixtures/bool.pyi]
+
 [case testDataclassesOverriding]
 # flags: --python-version 3.6
 from dataclasses import dataclass
@@ -589,6 +611,88 @@ class A:
         return cls()
 [builtins fixtures/classmethod.pyi]
 
+[case testDataclassesInitVarOverride]
+import dataclasses
+
+@dataclasses.dataclass
+class A:
+    a: dataclasses.InitVar[int]
+    _a: int = dataclasses.field(init=False)
+
+    def __post_init__(self, a):
+        self._a = a
+
+@dataclasses.dataclass(init=False)
+class B(A):
+    b: dataclasses.InitVar[int]
+    _b: int = dataclasses.field(init=False)
+
+    def __init__(self, b):
+        super().__init__(b+1)
+        self._b = b
+
+[builtins fixtures/bool.pyi]
+
+[case testDataclassesInitVarNoOverride]
+import dataclasses
+
+@dataclasses.dataclass
+class A:
+    a: dataclasses.InitVar[int]
+    _a: int = dataclasses.field(init=False)
+
+    def __post_init__(self, a):
+        self._a = a
+
+@dataclasses.dataclass(init=True)
+class B(A):
+    b: dataclasses.InitVar[int]
+    _b: int = dataclasses.field(init=False)
+
+    def __post_init__(self, a, b):
+        self._a = a
+        self._b = b
+
+B(1, 2)
+B(1, 'a') # E: Argument 2 to "B" has incompatible type "str"; expected "int"
+
+[builtins fixtures/bool.pyi]
+
+[case testDataclassesInitVarPostInitOverride]
+import dataclasses
+
+@dataclasses.dataclass
+class A:
+    a: dataclasses.InitVar[int]
+    _a: int = dataclasses.field(init=False)
+
+    def __post_init__(self, a: int) -> None:
+        self._a = a
+
+@dataclasses.dataclass
+class B(A):
+    b: int = dataclasses.field(init=False)
+
+    def __post_init__(self, a: int) -> None:
+        super().__post_init__(a)
+        self.b = a + 1
+
+@dataclasses.dataclass(init=False)
+class C(B):
+    c: int
+
+    def __init__(self, a: int, c: int) -> None:
+        super().__init__(a)
+        self.c = c + self.b
+
+A(1)
+B(1)
+B(1, 2)  # E: Too many arguments for "B"
+C(1, 2)
+C(1, 'a')  # E: Argument 2 to "C" has incompatible type "str"; expected "int"
+
+[builtins fixtures/primitives.pyi]
+
 [case testNoComplainFieldNone]
 # flags: --python-version 3.6
 # flags: --no-strict-optional