Fix spurious errors in inherited dataclasses in incremental mode (#7596)
The dataclasses plugin incorrectly expected json to preserve
the ordering of its dictionaries, which led to spurious errors
about "Attributes without a default cannot follow attributes with one".
Worse, the errors are reported in the wrong file: they use the line
number of the attribute but the file of the dataclass being checked!
Fix the spurious error by serializing attributes in a list, since we
care about the order.
Reporting this error for attributes in parent classes is actually
useful, since multiple inheritance can cause this error. Report the
error at the class definition site causing the problem instead.
(The error message here could certainly be improved, but right
now I just want to fix the "wrong file" bugs, which are I think
the worst kind of bugs after crashes.)
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
index 305b759..dff6963 100644
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -304,7 +304,7 @@
last_default = False
last_kw_only = False
- for attribute in attributes:
+ for i, attribute in enumerate(attributes):
if not attribute.init:
continue
@@ -313,14 +313,18 @@
last_kw_only = True
continue
+ # If the issue comes from merging different classes, report it
+ # at the class definition point.
+ context = attribute.context if i >= len(super_attrs) else ctx.cls
+
if not attribute.has_default and last_default:
ctx.api.fail(
"Non-default attributes not allowed after default attributes.",
- attribute.context)
+ context)
if last_kw_only:
ctx.api.fail(
"Non keyword-only attributes are not allowed after a keyword-only attribute.",
- attribute.context
+ context
)
last_default |= attribute.has_default
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
index f897552..3aa16b0 100644
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -1,7 +1,5 @@
"""Plugin that provides support for dataclasses."""
-from collections import OrderedDict
-
from typing import Dict, List, Set, Tuple, Optional
from typing_extensions import Final
@@ -181,7 +179,7 @@
self.reset_init_only_vars(info, attributes)
info.metadata['dataclass'] = {
- 'attributes': OrderedDict((attr.name, attr.serialize()) for attr in attributes),
+ 'attributes': [attr.serialize() for attr in attributes],
'frozen': decorator_arguments['frozen'],
}
@@ -296,7 +294,8 @@
# Each class depends on the set of attributes in its dataclass ancestors.
ctx.api.add_plugin_dependency(make_wildcard_trigger(info.fullname()))
- for name, data in info.metadata['dataclass']['attributes'].items():
+ for data in info.metadata['dataclass']['attributes']:
+ 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):
@@ -329,9 +328,13 @@
# doesn't have a default after one that does have one,
# then that's an error.
if found_default and attr.is_in_init and not attr.has_default:
+ # If the issue comes from merging different classes, report it
+ # at the class definition point.
+ context = (Context(line=attr.line, column=attr.column) if attr in attrs
+ else ctx.cls)
ctx.api.fail(
'Attributes without a default cannot follow attributes with one',
- Context(line=attr.line, column=attr.column),
+ context,
)
found_default = found_default or (attr.has_default and attr.is_in_init)
diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test
index bc5ad32..02a6e66 100644
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -1169,3 +1169,27 @@
[file other.py]
import lib
[builtins fixtures/bool.pyi]
+
+[case testAttrsDefaultsMroOtherFile]
+import a
+
+[file a.py]
+import attr
+from b import A1, A2
+
+@attr.s
+class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes.
+ pass
+
+[file b.py]
+import attr
+
+@attr.s
+class A1:
+ a: str = attr.ib('test')
+
+@attr.s
+class A2:
+ b: int = attr.ib()
+
+[builtins fixtures/list.pyi]
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index 95ed85f..d76c4a5 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -746,3 +746,64 @@
a = C(None, 'abc')
[builtins fixtures/bool.pyi]
+
+[case testDataclassesDefaultsIncremental]
+# flags: --python-version 3.6
+import a
+
+[file a.py]
+from dataclasses import dataclass
+from b import Person
+
+@dataclass
+class Asdf(Person):
+ c: str = 'test'
+
+[file a.py.2]
+from dataclasses import dataclass
+from b import Person
+
+@dataclass
+class Asdf(Person):
+ c: str = 'test'
+
+# asdf
+
+[file b.py]
+from dataclasses import dataclass
+
+@dataclass
+class Person:
+ b: int
+ a: str = 'test'
+
+[builtins fixtures/list.pyi]
+
+[case testDataclassesDefaultsMroOtherFile]
+# flags: --python-version 3.6
+import a
+
+[file a.py]
+from dataclasses import dataclass
+from b import A1, A2
+
+@dataclass
+class Asdf(A1, A2): # E: Attributes without a default cannot follow attributes with one
+ pass
+
+[file b.py]
+from dataclasses import dataclass
+
+# a bunch of blank lines to make sure the error doesn't accidentally line up...
+
+
+
+@dataclass
+class A1:
+ a: int
+
+@dataclass
+class A2:
+ b: str = 'test'
+
+[builtins fixtures/list.pyi]