Repository: python/mypy

Issue:
Recognizing wrapped classes as supporting a Protocol
```python
from dataclasses import dataclass
from typing import Dict

from typing_extensions import Protocol

@dataclass
class A:
    pass

class IsDataclass(Protocol):
    __dataclass_fields__: Dict

def dataclass_only(x: IsDataclass):
    pass

dataclass_only(A())
```
When running mypy on this code, I'd expect no warning, but I get 

> test.py:16: error: Argument 1 to "dataclass_only" has incompatible type "A"; expected "IsDataclass". 

While checking for `__dataclass_fields__` might not look like the best thing to go for, it's what `dataclasses.is_dataclass` checks for as well, so this seems like the most reliable condition right now.

I'd assume it happens because the dataclass-wrapper adds this attribute, and mypy only checks the class definition itself. Can this be fixed?


Discussion:
This can be supported by just adding `__dataclass_fields__` `Var` to the symbol table in the plugin. This is quite exotic use case, so setting priority to low. On the other hand, if you have time, PRs are welcome, this shouldn't be hard to fix.
I'm not 100% sure what you mean with adding the symbol to the plugin table, do you mean this?

``` python
@dataclass
class A:
    __dataclass_fields__: Dict
```
Because that does fix it without side effects, since the dataclass wrapper just overrides it. Then again, once I start adding fields to my dataclasses only to fulfill a protocol I might as well inherit from a classic no-op interface class.

Low prio is what I think as well, I'll look if I can manage to write a proper fix.
> I'm not 100% sure what you mean with adding the symbol to the plugin table, do you mean this?

You misread what I wrote, I meant fixing this in mypy plugin for dataclasses (dataclasses in mypy are supported by a bundled/builtin plugin).
I see, thanks for the info =)
So we are talking about `mypy/plugins/dataclasses.py` here, right @ilevkivskyi ?

I would like to have that fixed because I see no other reliable way of type hinting data classes.
Have never touched mypy source code, but I can give it a try.
> So we are talking about `mypy/plugins/dataclasses.py` here

Yes.
> Have never touched mypy source code, but I can give it a try.

Please go ahead! If you feel stuck, feel free to ask here or on Gitter.
I will certainly have some questions, thanks :)

For now, in order to leave it commented here, I managed to track the type checking to the `subtypes` submodule. Today, I am simply trying to get the gist of what is mypy's flow.

The function call that isn't returning what we expect in order to validate the protocol is at `subtypes.py(481)`:
```python
subtype = find_member(member, left, left)
```
Where member is the attribute we are checking (i.e.,`__dataclass_fields__`), and left is `A` class.

`find_member` consists of:
```python
	    def get_method(self, name: str) -> Optional[FuncBase]:
	        for cls in self.mro:
	            if name in cls.names:
	                node = cls.names[name].node
	                if isinstance(node, FuncBase):
	                    return node
	                else:
	                    return None
```
Where:
```
self = <TypeInfo foo.A>
self.mro = [<TypeInfo foo.A>, <TypeInfo builtins.object>]
name = '__dataclass_fields__'
```

Given `self.names` is an empty `SymbolTable`, `name` is never in `cls.names`. Tweaking with dataclasses' symbol table should make `__dataclass_fields__` visible here, then making `A` compliant with the protocol `IsDataclass`.

Thoughts here so far?
> Tweaking with dataclasses' symbol table should make `__dataclass_fields__` visible here, then making `A` compliant with the protocol `IsDataclass`.

Yes, we already add `__init__` and other things to the symbol table in `plugins/dataclasses.py`, just add a `Var` there with a type like `Dict[str, Any]`.
It seems I was following the wrong thread there 😅 Ended up at the `semanal.analyze_class` class, which analyzes the definitions (`defn.defs.body`). Still, that was very instructive :)

Gonna go for the plugin now, thanks
How does `Var` relate to `TypeInfo`? Creating a `Var` via `Var(random_name, Dict[Any, Any]` initializes it with `VAR_NO_INFO`, for example.
I see it is assigned at some places, like `semanal.make_name_lvalue_var`
> How does `Var` relate to `TypeInfo`?

For instance/class attributes, `Var.info` should be set to the class where it was defined (can be get as `ClassDefContext.cls.info`).
That makes sense, thanks!

Aren't we supposed to have some sort of `Dict` node-type at `mypy/types.py`? I see `TypedDict` there, which doesn't seem to be the right fit here
> Aren't we supposed to have some sort of `Dict` node-type at `mypy/types.py`?

No, just use `ctx.api.named_type` as in several other places in `dataclasses.py` plugin. 
I couldn't tell if there was any progress on this so I attempted solution.  It seems to work, but I'm sure I'm missing some things.

My solution was to add the following to the DataclassTransformer.transform method

```
dataclass_fields_var = Var('__dataclass_fields__')
dataclass_fields_var.info = ctx.cls.info
info.names['__dataclass_fields__'] = SymbolTableNode(MDEF, dataclass_fields_var)
```
I tried to give __dataclass_fields__ a type but couldn't get the syntax of `ctx.api.named_type()` correct to set it to `Dict[str, Any]`

Any / all comments are appreciated.

Resolving patch:
diff --git a/mypy/plugin.py b/mypy/plugin.py
--- a/mypy/plugin.py
+++ b/mypy/plugin.py
@@ -254,6 +254,18 @@ def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) ->
         """Construct an instance of a builtin type with given type arguments."""
         raise NotImplementedError
 
+    @abstractmethod
+    def named_type_or_none(self,
+                           qualified_name: str,
+                           args: Optional[List[Type]] = None) -> Optional[Instance]:
+        """Construct an instance of a type with given type arguments.
+
+        Return None if a type could not be constructed for the qualified
+        type name. This is possible when the qualified name includes a
+        module name and the module has not been imported.
+        """
+        raise NotImplementedError
+
     @abstractmethod
     def parse_bool(self, expr: Expression) -> Optional[bool]:
         """Parse True/False literals."""
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -21,8 +21,8 @@
     deserialize_and_fixup_type
 )
 from mypy.types import (
-    Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarType,
-    Overloaded, UnionType, FunctionLike, get_proper_type
+    TupleType, Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarType,
+    Overloaded, UnionType, FunctionLike, get_proper_type,
 )
 from mypy.typeops import make_simplified_union, map_type_from_supertype
 from mypy.typevars import fill_typevars
@@ -300,6 +300,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
             ctx.api.defer()
             return
 
+    _add_attrs_magic_attribute(ctx, raw_attr_types=[info[attr.name].type for attr in attributes])
+
     # Save the attributes so that subclasses can reuse them.
     ctx.cls.info.metadata['attrs'] = {
         'attributes': [attr.serialize() for attr in attributes],
@@ -703,6 +705,27 @@ def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute],
     adder.add_method('__init__', args, NoneType())
 
 
+def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext',
+                               raw_attr_types: 'List[Optional[Type]]') -> None:
+    attr_name = '__attrs_attrs__'
+    any_type = AnyType(TypeOfAny.explicit)
+    attributes_types: 'List[Type]' = [
+        ctx.api.named_type_or_none('attr.Attribute', [attr_type or any_type]) or any_type
+        for attr_type in raw_attr_types
+    ]
+    fallback_type = ctx.api.named_type('__builtins__.tuple', [
+        ctx.api.named_type_or_none('attr.Attribute', [any_type]) or any_type,
+    ])
+    var = Var(name=attr_name, type=TupleType(attributes_types, fallback=fallback_type))
+    var.info = ctx.cls.info
+    var._fullname = ctx.cls.info.fullname + '.' + attr_name
+    ctx.cls.info.names[attr_name] = SymbolTableNode(
+        kind=MDEF,
+        node=var,
+        plugin_generated=True,
+    )
+
+
 class MethodAdder:
     """Helper to add methods to a TypeInfo.
 
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -13,7 +13,10 @@
     add_method, _get_decorator_bool_argument, deserialize_and_fixup_type,
 )
 from mypy.typeops import map_type_from_supertype
-from mypy.types import Type, Instance, NoneType, TypeVarType, CallableType, get_proper_type
+from mypy.types import (
+    Type, Instance, NoneType, TypeVarType, CallableType, get_proper_type,
+    AnyType, TypeOfAny,
+)
 from mypy.server.trigger import make_wildcard_trigger
 
 # The set of decorators that generate dataclasses.
@@ -187,6 +190,8 @@ def transform(self) -> None:
 
         self.reset_init_only_vars(info, attributes)
 
+        self._add_dataclass_fields_magic_attribute()
+
         info.metadata['dataclass'] = {
             'attributes': [attr.serialize() for attr in attributes],
             'frozen': decorator_arguments['frozen'],
@@ -417,6 +422,23 @@ def _is_kw_only_type(self, node: Optional[Type]) -> bool:
             return False
         return node_type.type.fullname == 'dataclasses.KW_ONLY'
 
+    def _add_dataclass_fields_magic_attribute(self) -> None:
+        attr_name = '__dataclass_fields__'
+        any_type = AnyType(TypeOfAny.explicit)
+        field_type = self._ctx.api.named_type_or_none('dataclasses.Field', [any_type]) or any_type
+        attr_type = self._ctx.api.named_type('__builtins__.dict', [
+            self._ctx.api.named_type('__builtins__.str'),
+            field_type,
+        ])
+        var = Var(name=attr_name, type=attr_type)
+        var.info = self._ctx.cls.info
+        var._fullname = self._ctx.cls.info.fullname + '.' + attr_name
+        self._ctx.cls.info.names[attr_name] = SymbolTableNode(
+            kind=MDEF,
+            node=var,
+            plugin_generated=True,
+        )
+
 
 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-attr.test b/test-data/unit/check-attr.test
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -1390,3 +1390,15 @@ class B(A):
 
 reveal_type(B)  # N: Revealed type is "def (foo: builtins.int) -> __main__.B"
 [builtins fixtures/bool.pyi]
+
+[case testAttrsClassHasAttributeWithAttributes]
+import attr
+
+@attr.s
+class A:
+    b: int = attr.ib()
+    c: str = attr.ib()
+
+reveal_type(A.__attrs_attrs__)  # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str]]"
+
+[builtins fixtures/attr.pyi]
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
@@ -14,7 +14,7 @@ reveal_type(Person)  # N: Revealed type is "def (name: builtins.str, age: builti
 Person('John', 32)
 Person('Jonh', 21, None)  # E: Too many arguments for "Person"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [typing fixtures/typing-medium.pyi]
 
 [case testDataclassesCustomInit]
@@ -30,7 +30,7 @@ class A:
 
 A('1')
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesBasicInheritance]
 # flags: --python-version 3.7
@@ -52,7 +52,7 @@ Mammal(10)
 Person(32, 'John')
 Person(21, 'Jonh', None)  # E: Too many arguments for "Person"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [typing fixtures/typing-medium.pyi]
 
 [case testDataclassesDeepInheritance]
@@ -80,7 +80,7 @@ reveal_type(B)  # N: Revealed type is "def (a: builtins.int, b: builtins.int) ->
 reveal_type(C)  # N: Revealed type is "def (a: builtins.int, b: builtins.int, c: builtins.int) -> __main__.C"
 reveal_type(D)  # N: Revealed type is "def (a: builtins.int, b: builtins.int, c: builtins.int, d: builtins.int) -> __main__.D"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesMultipleInheritance]
 # flags: --python-version 3.7
@@ -103,7 +103,7 @@ class C(A, B):
 
 reveal_type(C)  # N: Revealed type is "def (b: builtins.bool, a: builtins.bool) -> __main__.C"
 
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDeepInitVarInheritance]
 # flags: --python-version 3.7
@@ -132,7 +132,7 @@ class D(C):
 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]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOverriding]
 # flags: --python-version 3.7
@@ -165,7 +165,7 @@ Person(21, 'John', None)  # E: Too many arguments for "Person"
 SpecialPerson(21, 'John', 0.5)
 ExtraSpecialPerson(21, 'John', 0.5)
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOverridingWithDefaults]
 # Issue #5681 https://github.com/python/mypy/issues/5681
@@ -185,7 +185,7 @@ class C(Base):
 
 reveal_type(C)  # N: Revealed type is "def (some_int: builtins.int, some_str: builtins.str =) -> __main__.C"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesFreezing]
 # flags: --python-version 3.7
@@ -198,7 +198,7 @@ class Person:
 john = Person('John')
 john.name = 'Ben'  # E: Property "name" defined in "Person" is read-only
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesFields]
 # flags: --python-version 3.7
@@ -214,7 +214,7 @@ john = Person('John')
 john.age = 'invalid'  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
 john.age = 24
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesBadInit]
 # flags: --python-version 3.7
@@ -228,7 +228,7 @@ class Person:
                                  # N:     def field(*, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[str, Any]] = ..., kw_only: bool = ...) -> Any \
                                  # N:     <2 more non-matching overloads not shown>
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesMultiInit]
 # flags: --python-version 3.7
@@ -244,7 +244,7 @@ class Person:
 
 reveal_type(Person)  # N: Revealed type is "def (name: builtins.str, friend_names: builtins.list[builtins.str], enemy_names: builtins.list[builtins.str]) -> __main__.Person"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesMultiInitDefaults]
 # flags: --python-version 3.7
@@ -261,7 +261,7 @@ class Person:
 
 reveal_type(Person)  # N: Revealed type is "def (name: builtins.str, friend_names: builtins.list[builtins.str], enemy_names: builtins.list[builtins.str], nickname: Union[builtins.str, None] =) -> __main__.Person"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDefaults]
 # flags: --python-version 3.7
@@ -275,7 +275,7 @@ class Application:
 reveal_type(Application)  # N: Revealed type is "def (name: builtins.str =, rating: builtins.int =) -> __main__.Application"
 app = Application()
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDefaultFactories]
 # flags: --python-version 3.7
@@ -287,7 +287,7 @@ class Application:
     rating: int = field(default_factory=int)
     rating_count: int = field()  # E: Attributes without a default cannot follow attributes with one
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDefaultFactoryTypeChecking]
 # flags: --python-version 3.7
@@ -298,7 +298,7 @@ class Application:
     name: str = 'Unnamed'
     rating: int = field(default_factory=str)  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDefaultOrdering]
 # flags: --python-version 3.7
@@ -309,7 +309,7 @@ class Application:
     name: str = 'Unnamed'
     rating: int  # E: Attributes without a default cannot follow attributes with one
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOrderingKwOnly]
 # flags: --python-version 3.10
@@ -327,9 +327,9 @@ Application('name')  # E: Too many positional arguments for "Application" # E: M
 Application('name', 123)  # E: Too many positional arguments for "Application"
 Application('name', rating=123)  # E: Too many positional arguments for "Application"
 Application(name=123, rating='name') # E: Argument "name" to "Application" has incompatible type "int"; expected "str" # E: Argument "rating" to "Application" has incompatible type "str"; expected "int"
-Application(rating='name', name=123) # E: Argument "rating" to "Application" has incompatible type "str"; expected "int" # E: Argument "name" to "Application" has incompatible type "int"; expected "str" 
+Application(rating='name', name=123) # E: Argument "rating" to "Application" has incompatible type "str"; expected "int" # E: Argument "name" to "Application" has incompatible type "int"; expected "str"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOrderingKwOnlyOnField]
 # flags: --python-version 3.10
@@ -348,7 +348,7 @@ Application('name')  # E: Missing named argument "rating" for "Application"
 Application('name', 123)  # E: Too many positional arguments for "Application"
 Application(123, rating='name') # E: Argument 1 to "Application" has incompatible type "int"; expected "str" # E: Argument "rating" to "Application" has incompatible type "str"; expected "int"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOrderingKwOnlyOnFieldFalse]
 # flags: --python-version 3.10
@@ -357,7 +357,7 @@ from dataclasses import dataclass, field
 @dataclass
 class Application:
     name: str = 'Unnamed'
-    rating: int = field(kw_only=False)  # E: Attributes without a default cannot follow attributes with one 
+    rating: int = field(kw_only=False)  # E: Attributes without a default cannot follow attributes with one
 
 Application(name='name', rating=5)
 Application('name', 123)
@@ -365,7 +365,7 @@ Application('name', rating=123)
 Application()  # E: Missing positional argument "name" in call to "Application"
 Application('name')  # E: Too few arguments for "Application"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOrderingKwOnlyWithSentinel]
 # flags: --python-version 3.10
@@ -384,7 +384,7 @@ Application('name')  # E: Too many positional arguments for "Application" # E: M
 Application('name', 123)  # E: Too many positional arguments for "Application"
 Application('name', rating=123)  # E: Too many positional arguments for "Application"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOrderingKwOnlyWithSentinelAndFieldOverride]
 # flags: --python-version 3.10
@@ -394,7 +394,7 @@ from dataclasses import dataclass, field, KW_ONLY
 class Application:
     _: KW_ONLY
     name: str = 'Unnamed'
-    rating: int = field(kw_only=False)  # E: Attributes without a default cannot follow attributes with one 
+    rating: int = field(kw_only=False)  # E: Attributes without a default cannot follow attributes with one
 
 Application(name='name', rating=5)
 Application()  # E: Missing positional argument "name" in call to "Application"
@@ -402,7 +402,7 @@ Application('name')  # E: Too many positional arguments for "Application" # E: T
 Application('name', 123)  # E: Too many positional arguments for "Application"
 Application('name', rating=123)  # E: Too many positional arguments for "Application"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOrderingKwOnlyWithSentinelAndSubclass]
 # flags: --python-version 3.10
@@ -429,6 +429,8 @@ D(123, "World")  # E: Argument 1 to "D" has incompatible type "int"; expected "s
 D("Hello", False)  # E: Argument 2 to "D" has incompatible type "bool"; expected "str"
 D(123, False)  # E: Argument 1 to "D" has incompatible type "int"; expected "str" # E: Argument 2 to "D" has incompatible type "bool"; expected "str"
 
+[builtins fixtures/dataclasses.pyi]
+
 [case testDataclassesOrderingKwOnlyWithMultipleSentinel]
 # flags: --python-version 3.10
 from dataclasses import dataclass, field, KW_ONLY
@@ -441,7 +443,7 @@ class Base:
     __: KW_ONLY  # E: There may not be more than one field with the KW_ONLY type
     w: int = 1
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesClassmethods]
 # flags: --python-version 3.7
@@ -457,8 +459,7 @@ class Application:
 
 app = Application.parse('')
 
-[builtins fixtures/list.pyi]
-[builtins fixtures/classmethod.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesOverloadsAndClassmethods]
 # flags: --python-version 3.7
@@ -491,7 +492,7 @@ class A:
 reveal_type(A.foo(3))      # N: Revealed type is "builtins.int"
 reveal_type(A.foo("foo"))  # N: Revealed type is "builtins.str"
 
-[builtins fixtures/classmethod.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesClassVars]
 # flags: --python-version 3.7
@@ -509,7 +510,7 @@ application = Application("example")
 application.COUNTER = 1  # E: Cannot assign to class variable "COUNTER" via instance
 Application.COUNTER = 1
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassOrdering]
 # flags: --python-version 3.7
@@ -540,7 +541,7 @@ app1 > app3
 app1 <= app3
 app1 >= app3
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassOrderingWithoutEquality]
 # flags: --python-version 3.7
@@ -550,7 +551,7 @@ from dataclasses import dataclass
 class Application:  # E: eq must be True if order is True
    ...
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassOrderingWithCustomMethods]
 # flags: --python-version 3.7
@@ -561,7 +562,7 @@ class Application:
   def __lt__(self, other: 'Application') -> bool: # E: You may not have a custom __lt__ method when order=True
     ...
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassDefaultsInheritance]
 # flags: --python-version 3.7
@@ -579,7 +580,7 @@ class SpecializedApplication(Application):
 
 reveal_type(SpecializedApplication)  # N: Revealed type is "def (id: Union[builtins.int, None], name: builtins.str, rating: builtins.int =) -> __main__.SpecializedApplication"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassGenerics]
 # flags: --python-version 3.7
@@ -612,7 +613,7 @@ reveal_type(a.y)  # N: Revealed type is "builtins.int*"
 reveal_type(a.z)  # N: Revealed type is "builtins.list[builtins.int*]"
 s: str = a.bar()  # E: Incompatible types in assignment (expression has type "int", variable has type "str")
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 
 [case testDataclassUntypedGenericInheritance]
@@ -634,6 +635,7 @@ sub = Sub(attr=1)
 reveal_type(sub)  # N: Revealed type is "__main__.Sub"
 reveal_type(sub.attr)  # N: Revealed type is "Any"
 
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassGenericSubtype]
 # flags: --python-version 3.7
@@ -660,6 +662,7 @@ sub_str = Sub[str](attr='ok')
 reveal_type(sub_str)  # N: Revealed type is "__main__.Sub[builtins.str*]"
 reveal_type(sub_str.attr)  # N: Revealed type is "builtins.str*"
 
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassGenericInheritance]
 # flags: --python-version 3.7
@@ -686,6 +689,7 @@ reveal_type(sub.one)  # N: Revealed type is "builtins.int*"
 reveal_type(sub.two)  # N: Revealed type is "builtins.str*"
 reveal_type(sub.three)  # N: Revealed type is "builtins.float*"
 
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassMultiGenericInheritance]
 # flags: --python-version 3.7
@@ -713,6 +717,7 @@ reveal_type(sub)  # N: Revealed type is "__main__.Sub"
 reveal_type(sub.base_attr)  # N: Revealed type is "builtins.int*"
 reveal_type(sub.middle_attr)  # N: Revealed type is "builtins.str*"
 
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassGenericsClassmethod]
 # flags: --python-version 3.7
@@ -734,7 +739,7 @@ class A(Generic[T]):
   def other(cls, x: T) -> A[T]: ...
 
 reveal_type(A(0).other)  # N: Revealed type is "def (x: builtins.int*) -> __main__.A[builtins.int*]"
-[builtins fixtures/classmethod.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesForwardRefs]
 # flags: --python-version 3.7
@@ -752,7 +757,7 @@ reveal_type(A)  # N: Revealed type is "def (b: __main__.B) -> __main__.A"
 A(b=B(42))
 A(b=42)  # E: Argument "b" to "A" has incompatible type "int"; expected "B"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 
 [case testDataclassesInitVars]
@@ -782,7 +787,7 @@ app.name
 app.rating
 app.database_name  # E: "SpecializedApplication" has no attribute "database_name"
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesInitVarsAndDefer]
 # flags: --python-version 3.7
@@ -802,7 +807,7 @@ app.name
 app.database_name  # E: "Application" has no attribute "database_name"
 
 class Yes: ...
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesNoInitInitVarInheritance]
 # flags: --python-version 3.7
@@ -819,7 +824,7 @@ class Sub(Super):
 sub = Sub(5)
 sub.foo  # E: "Sub" has no attribute "foo"
 sub.bar
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassFactory]
 # flags: --python-version 3.7
@@ -835,7 +840,7 @@ class A:
         reveal_type(cls)  # N: Revealed type is "Type[T`-1]"
         reveal_type(cls())  # N: Revealed type is "T`-1"
         return cls()
-[builtins fixtures/classmethod.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesInitVarOverride]
 # flags: --python-version 3.7
@@ -858,7 +863,7 @@ class B(A):
         super().__init__(b+1)
         self._b = b
 
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesInitVarNoOverride]
 # flags: --python-version 3.7
@@ -884,7 +889,7 @@ class B(A):
 B(1, 2)
 B(1, 'a') # E: Argument 2 to "B" has incompatible type "str"; expected "int"
 
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesInitVarPostInitOverride]
 # flags: --python-version 3.7
@@ -977,7 +982,7 @@ from typing import Optional
 @dataclass
 class Foo:
     bar: Optional[int] = field(default=None)
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 
 [case testNoComplainFieldNoneStrict]
@@ -989,7 +994,7 @@ from typing import Optional
 @dataclass
 class Foo:
     bar: Optional[int] = field(default=None)
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 
 [case testDisallowUntypedWorksForward]
@@ -1005,7 +1010,7 @@ class C(List[C]):
     pass
 
 reveal_type(B)  # N: Revealed type is "def (x: __main__.C) -> __main__.B"
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDisallowUntypedWorksForwardBad]
 # flags: --disallow-untyped-defs --python-version 3.7
@@ -1017,7 +1022,7 @@ class B:
     y = undefined()  # E: Name "undefined" is not defined
 
 reveal_type(B)  # N: Revealed type is "def (x: Any) -> __main__.B"
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testMemberExprWorksAsField]
 # flags: --python-version 3.7
@@ -1055,7 +1060,7 @@ b = Application('', 0)
 a < b
 
 class Yes: ...
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassFieldDeferred]
 # flags: --python-version 3.7
@@ -1067,7 +1072,7 @@ class C:
 
 def func() -> int: ...
 C('no')  # E: Argument 1 to "C" has incompatible type "str"; expected "int"
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassFieldDeferredFrozen]
 # flags: --python-version 3.7
@@ -1080,7 +1085,7 @@ class C:
 def func() -> int: ...
 c: C
 c.x = 1  # E: Property "x" defined in "C" is read-only
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testTypeInDataclassDeferredStar]
 # flags: --python-version 3.7
@@ -1099,7 +1104,7 @@ C()  # E: Missing positional argument "total" in call to "C"
 C('no')  # E: Argument 1 to "C" has incompatible type "str"; expected "int"
 [file other.py]
 import lib
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDeferredDataclassInitSignature]
 # flags: --python-version 3.7 --no-strict-optional
@@ -1116,7 +1121,7 @@ class C:
         return cls(x=None, y=None)
 
 class Deferred: pass
-[builtins fixtures/classmethod.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDeferredDataclassInitSignatureSubclass]
 # flags: --strict-optional --python-version 3.7
@@ -1132,7 +1137,7 @@ class C(B):
     y: str
 
 a = C(None, 'abc')
-[builtins fixtures/bool.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDefaultsIncremental]
 # flags: --python-version 3.7
@@ -1164,7 +1169,7 @@ class Person:
     b: int
     a: str = 'test'
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesDefaultsMroOtherFile]
 # flags: --python-version 3.7
@@ -1193,7 +1198,7 @@ class A1:
 class A2:
     b: str = 'test'
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassesInheritingDuplicateField]
 # flags: --python-version 3.7
@@ -1209,6 +1214,8 @@ class A:  # E: Name "x" already defined (possibly by an import)
 class B(A):
     pass
 
+[builtins fixtures/dataclasses.pyi]
+
 [case testDataclassInheritanceNoAnnotation]
 # flags: --python-version 3.7
 from dataclasses import dataclass
@@ -1224,6 +1231,8 @@ class B(A):
 
 reveal_type(B)  # N: Revealed type is "def (foo: builtins.int) -> __main__.B"
 
+[builtins fixtures/dataclasses.pyi]
+
 [case testDataclassInheritanceNoAnnotation2]
 # flags: --python-version 3.7
 from dataclasses import dataclass
@@ -1239,7 +1248,19 @@ class B(A):
 
 reveal_type(B)  # N: Revealed type is "def (foo: builtins.int) -> __main__.B"
 
-[builtins fixtures/property.pyi]
+[builtins fixtures/dataclasses.pyi]
+
+[case testDataclassHasAttributeWithFields]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+
+@dataclass
+class A:
+    pass
+
+reveal_type(A.__dataclass_fields__)  # N: Revealed type is "builtins.dict[builtins.str, dataclasses.Field[Any]]"
+
+[builtins fixtures/dict.pyi]
 
 [case testDataclassCallableFieldAccess]
 # flags: --python-version 3.7
@@ -1258,6 +1279,8 @@ reveal_type(a.x)  # N: Revealed type is "def (builtins.int) -> builtins.int"
 reveal_type(a.y)  # N: Revealed type is "def (builtins.int) -> builtins.int"
 reveal_type(A.y)  # N: Revealed type is "def (builtins.int) -> builtins.int"
 
+[builtins fixtures/dataclasses.pyi]
+
 [case testDataclassCallableFieldAssignment]
 # flags: --python-version 3.7
 from dataclasses import dataclass
@@ -1275,3 +1298,5 @@ def x2(s: str) -> str:
 a = A(lambda i:i)
 a.x = x
 a.x = x2 # E: Incompatible types in assignment (expression has type "Callable[[str], str]", variable has type "Callable[[int], int]")
+
+[builtins fixtures/dataclasses.pyi]
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -1193,6 +1193,8 @@ import attr
 class Unannotated:
     foo = attr.ib()
 
+[builtins fixtures/attr.pyi]
+
 [case testDisallowIncompleteDefsAttrsWithAnnotations]
 # flags: --disallow-incomplete-defs
 import attr
@@ -1201,6 +1203,8 @@ import attr
 class Annotated:
     bar: int = attr.ib()
 
+[builtins fixtures/attr.pyi]
+
 [case testDisallowIncompleteDefsAttrsPartialAnnotations]
 # flags: --disallow-incomplete-defs
 import attr
@@ -1210,6 +1214,8 @@ class PartiallyAnnotated:  # E: Function is missing a type annotation for one or
     bar: int = attr.ib()
     baz = attr.ib()
 
+[builtins fixtures/attr.pyi]
+
 [case testAlwaysTrueAlwaysFalseFlags]
 # flags: --always-true=YOLO --always-true=YOLO1 --always-false=BLAH1 --always-false BLAH --ignore-missing-imports
 from somewhere import YOLO, BLAH
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -3830,7 +3830,7 @@ class A:
     E = 7
     F: ClassVar[int] = 22
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 
@@ -3863,7 +3863,7 @@ from dataclasses import dataclass
 class A:
     x: int
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 tmp/b.py:8: note: Revealed type is "def (x: builtins.int) -> b.B"
@@ -3908,7 +3908,7 @@ class NoInit:
 class NoCmp:
     x: int
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 tmp/b.py:4: error: Property "x" defined in "Frozen" is read-only
@@ -3963,7 +3963,7 @@ from dataclasses import dataclass
 class A:
     a: int
 
-[builtins fixtures/attr.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 tmp/b.py:3: note: Revealed type is "def (a: builtins.int) -> a.A"
@@ -4010,7 +4010,7 @@ from dataclasses import dataclass
 class B(A):
     y: int
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 main:3: error: Argument 2 to "B" has incompatible type "str"; expected "int"
@@ -4044,7 +4044,7 @@ from dataclasses import dataclass
 class B(A):
     y: str
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 main:3: error: Argument 2 to "B" has incompatible type "str"; expected "int"
 
@@ -4087,7 +4087,7 @@ from dataclasses import dataclass
 class C(A, B):
     c: bool
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 tmp/c.py:7: error: Incompatible types in assignment (expression has type "bool", base class "B" defined the type as "str")
@@ -4119,7 +4119,7 @@ from dataclasses import dataclass
 class A:
     a: int = 6
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out1]
 [out2]
 main:3: error: Argument 1 to "A" has incompatible type "int"; expected "str"
diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test
--- a/test-data/unit/check-narrowing.test
+++ b/test-data/unit/check-narrowing.test
@@ -170,7 +170,7 @@ if x5["key"] is Key.A:
     reveal_type(x5)         # N: Revealed type is "TypedDict('__main__.TypedDict1', {'key': Literal[__main__.Key.A], 'foo': builtins.int})"
 else:
     reveal_type(x5)         # N: Revealed type is "TypedDict('__main__.TypedDict2', {'key': Literal[__main__.Key.B], 'foo': builtins.str})"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/narrowing.pyi]
 
 [case testNarrowingParentWithIsInstanceBasic]
 # flags: --python-version 3.7
@@ -236,7 +236,7 @@ if isinstance(x5["key"], int):
     reveal_type(x5)         # N: Revealed type is "TypedDict('__main__.TypedDict1', {'key': builtins.int})"
 else:
     reveal_type(x5)         # N: Revealed type is "TypedDict('__main__.TypedDict2', {'key': builtins.str})"
-[builtins fixtures/isinstance.pyi]
+[builtins fixtures/narrowing.pyi]
 
 [case testNarrowingParentMultipleKeys]
 # flags: --warn-unreachable
diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test
--- a/test-data/unit/deps.test
+++ b/test-data/unit/deps.test
@@ -1430,11 +1430,11 @@ class A:
 @dataclass
 class B(A):
     y: int
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [out]
 <m.A.(abstract)> -> <m.B.__init__>, m
-<m.A._DT> -> <m.B._DT>
+<m.A.__dataclass_fields__> -> <m.B.__dataclass_fields__>
 <m.A.__init__> -> <m.B.__init__>, m.B.__init__
 <m.A.__new__> -> <m.B.__new__>
 <m.A.x> -> <m.B.x>
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
@@ -688,7 +688,7 @@ class A:
 ==
 b.py:8: error: Argument 1 to "B" has incompatible type "int"; expected "str"
 ==
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassUpdate2]
 # flags: --python-version 3.7
@@ -719,7 +719,7 @@ B(1, 2)
 [out]
 ==
 b.py:8: error: Argument 1 to "B" has incompatible type "int"; expected "str"
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 
 [case testDataclassUpdate3]
 # flags: --python-version 3.7
@@ -743,7 +743,7 @@ from dataclasses import dataclass
 class A:
     a: int
     other: int
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 ==
 main:3: error: Missing positional argument "b" in call to "B"
@@ -770,7 +770,7 @@ from dataclasses import dataclass
 class A:
     a: int
     other: int
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 ==
 main:3: error: Missing positional argument "b" in call to "B"
@@ -804,7 +804,7 @@ from dataclasses import dataclass
 class A:
     a: int
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 ==
 main:3: error: Missing positional argument "b" in call to "B"
@@ -831,7 +831,7 @@ from dataclasses import dataclass
 @dataclass
 class A:
     a: int
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 ==
 main:3: error: Unsupported left operand type for < ("B")
@@ -864,7 +864,7 @@ from dataclasses import dataclass
 class A:
     a: int
     other: int
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 ==
 main:3: error: Missing positional argument "c" in call to "C"
@@ -904,7 +904,7 @@ from dataclasses import dataclass
 class A:
     a: int
 
-[builtins fixtures/list.pyi]
+[builtins fixtures/dataclasses.pyi]
 [out]
 ==
 main:3: error: Missing positional argument "c" in call to "C"