Repository: python/mypy
Pull request: Fix __init__ in Dataclasses inheriting from Any
### Description

Fixes #11921

Dataclasses that inherit from Any (actually another type that we don't
know) should allow any constructor.

The way this bug was noticed was while importing a class that mypy didn't know
about with the `--ignore-missing-imports` flag:

```python
import dataclasses
from mypackage import MyClass

# @dataclasses.dataclass
# class MyClass:
#     bar: str

@dataclasses.dataclass
class Inherits(MyClass):
    foo: str

i = Inherits(foo="hi", bar="bye")
```

## Test Plan

Added appropriate test case 

Problem statement:
Inheriting from Any with dataclass
I'm importing a package at a path mypy doesn't know about and using the `--ignore-missing-imports` flag.

```python a.py
import dataclasses
from mypackage import MyClass

# @dataclasses.dataclass
# class MyClass:
#     bar: str

@dataclasses.dataclass
class Inherits(MyClass):
    foo: str

reveal_type(MyClass)
reveal_type(Inherits)

i = Inherits(foo="hi", bar="bye")
```

The type of MyClass is Any because mypy doesn't know what it is. I would expect a class that inherits from Any to also be Any or at least be able to take any number of arguments in its constructor. Instead I get:

```
a.py:12: note: Revealed type is "Any"
a.py:13: note: Revealed type is "def (foo: builtins.str) -> a.Inherits"
a.py:15: error: Unexpected keyword argument "bar" for "Inherits"
Found 1 error in 1 file (checked 1 source file)
```

Hints from issue discussion:

Commit message: Fix __init__ in Dataclasses inheriting from Any

Dataclasses that inherit from Any (actually another type that we don't
know) should allow any constructor.

Fixes #11921

Patch to review:
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
index e3daec58922f..6b8b036fa4ec 100644
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -4,9 +4,10 @@
 from typing_extensions import Final
 
 from mypy.nodes import (
-    ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, CallExpr,
-    Context, Expression, JsonDict, NameExpr, RefExpr,
-    SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr, PlaceholderNode
+    ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_POS, ARG_STAR, ARG_STAR2, MDEF,
+    Argument, AssignmentStmt, CallExpr,    Context, Expression, JsonDict,
+    NameExpr, RefExpr, SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr,
+    PlaceholderNode
 )
 from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface
 from mypy.plugins.common import (
@@ -141,11 +142,19 @@ def transform(self) -> None:
         if (decorator_arguments['init'] and
                 ('__init__' not in info.names or info.names['__init__'].plugin_generated) and
                 attributes):
+
+            args = [attr.to_argument() for attr in attributes if attr.is_in_init
+                    and not self._is_kw_only_type(attr.type)]
+
+            if info.fallback_to_any:
+                args = [Argument(Var('args'), AnyType(TypeOfAny.explicit), None, ARG_STAR),
+                        Argument(Var('kwargs'), AnyType(TypeOfAny.explicit), None, ARG_STAR2),
+                        ]
+
             add_method(
                 ctx,
                 '__init__',
-                args=[attr.to_argument() for attr in attributes if attr.is_in_init
-                      and not self._is_kw_only_type(attr.type)],
+                args=args,
                 return_type=NoneType(),
             )
 
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index 80e5f81e110f..32382ab37440 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -1522,3 +1522,16 @@ PublishedMessagesVar = dict[int, 'PublishedMessages']
 class PublishedMessages:
     left: int
 [builtins fixtures/dataclasses.pyi]
+
+[case testDataclassesAnyInherit]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+from typing import Any
+B: Any
+@dataclass
+class A(B):
+    a: int
+
+A(a=1, b=2)
+
+[builtins fixtures/dataclasses.pyi]


Code review comments:
diff_hunk: @@ -141,11 +142,19 @@ def transform(self) -> None:
         if (decorator_arguments['init'] and
                 ('__init__' not in info.names or info.names['__init__'].plugin_generated) and
                 attributes):
+
+            args = [attr.to_argument() for attr in attributes if attr.is_in_init
+                    and not self._is_kw_only_type(attr.type)]
+
+            if info.fallback_to_any:
+                args = [Argument(Var('args'), AnyType(TypeOfAny.explicit), None, ARG_STAR),
+                        Argument(Var('kwargs'), AnyType(TypeOfAny.explicit), None, ARG_STAR2),
path: mypy/plugins/dataclasses.py
text: @author:
I didn't really want to give the variables names (args, kwargs) but I couldn't find a better way to do it without writing a lot of code to duplicate `add_method`

@user1:
What happens if the child dataclass has a field named `args` or `kwargs`?

@author:
Still works fine because we completely overwrite the args to __init__ with these args. The original fields won't appear

@author:
Found a workaround without using variable names. Using an empty var for both arguments won't work unless it is the same empty var, that way mypy won't throw an error.

The main difference is in the result of `reveal_type(A)` where A is a dataclass inheriting from any. When using args and kwargs it appeared as:

```
a.py:9: note: Revealed type is "def (*args: Any, **kwargs: Any) -> a.A"
```

and now it appears as

```
a.py:9: note: Revealed type is "def (*Any, **Any) -> a.A"
```