Skip to content

Commit 8b86349

Browse files
authored
Fix spurious errors in inherited dataclasses in incremental mode (python#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.)
1 parent 583b99e commit 8b86349

File tree

4 files changed

+100
-8
lines changed

4 files changed

+100
-8
lines changed

mypy/plugins/attrs.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
304304
last_default = False
305305
last_kw_only = False
306306

307-
for attribute in attributes:
307+
for i, attribute in enumerate(attributes):
308308
if not attribute.init:
309309
continue
310310

@@ -313,14 +313,18 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
313313
last_kw_only = True
314314
continue
315315

316+
# If the issue comes from merging different classes, report it
317+
# at the class definition point.
318+
context = attribute.context if i >= len(super_attrs) else ctx.cls
319+
316320
if not attribute.has_default and last_default:
317321
ctx.api.fail(
318322
"Non-default attributes not allowed after default attributes.",
319-
attribute.context)
323+
context)
320324
if last_kw_only:
321325
ctx.api.fail(
322326
"Non keyword-only attributes are not allowed after a keyword-only attribute.",
323-
attribute.context
327+
context
324328
)
325329
last_default |= attribute.has_default
326330

mypy/plugins/dataclasses.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Plugin that provides support for dataclasses."""
22

3-
from collections import OrderedDict
4-
53
from typing import Dict, List, Set, Tuple, Optional
64
from typing_extensions import Final
75

@@ -181,7 +179,7 @@ def transform(self) -> None:
181179
self.reset_init_only_vars(info, attributes)
182180

183181
info.metadata['dataclass'] = {
184-
'attributes': OrderedDict((attr.name, attr.serialize()) for attr in attributes),
182+
'attributes': [attr.serialize() for attr in attributes],
185183
'frozen': decorator_arguments['frozen'],
186184
}
187185

@@ -296,7 +294,8 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
296294
# Each class depends on the set of attributes in its dataclass ancestors.
297295
ctx.api.add_plugin_dependency(make_wildcard_trigger(info.fullname()))
298296

299-
for name, data in info.metadata['dataclass']['attributes'].items():
297+
for data in info.metadata['dataclass']['attributes']:
298+
name = data['name'] # type: str
300299
if name not in known_attrs:
301300
attr = DataclassAttribute.deserialize(info, data)
302301
if attr.is_init_var and isinstance(init_method, FuncDef):
@@ -329,9 +328,13 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
329328
# doesn't have a default after one that does have one,
330329
# then that's an error.
331330
if found_default and attr.is_in_init and not attr.has_default:
331+
# If the issue comes from merging different classes, report it
332+
# at the class definition point.
333+
context = (Context(line=attr.line, column=attr.column) if attr in attrs
334+
else ctx.cls)
332335
ctx.api.fail(
333336
'Attributes without a default cannot follow attributes with one',
334-
Context(line=attr.line, column=attr.column),
337+
context,
335338
)
336339

337340
found_default = found_default or (attr.has_default and attr.is_in_init)

test-data/unit/check-attr.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,3 +1169,27 @@ C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "int"
11691169
[file other.py]
11701170
import lib
11711171
[builtins fixtures/bool.pyi]
1172+
1173+
[case testAttrsDefaultsMroOtherFile]
1174+
import a
1175+
1176+
[file a.py]
1177+
import attr
1178+
from b import A1, A2
1179+
1180+
@attr.s
1181+
class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes.
1182+
pass
1183+
1184+
[file b.py]
1185+
import attr
1186+
1187+
@attr.s
1188+
class A1:
1189+
a: str = attr.ib('test')
1190+
1191+
@attr.s
1192+
class A2:
1193+
b: int = attr.ib()
1194+
1195+
[builtins fixtures/list.pyi]

test-data/unit/check-dataclasses.test

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,3 +746,64 @@ class C(B):
746746

747747
a = C(None, 'abc')
748748
[builtins fixtures/bool.pyi]
749+
750+
[case testDataclassesDefaultsIncremental]
751+
# flags: --python-version 3.6
752+
import a
753+
754+
[file a.py]
755+
from dataclasses import dataclass
756+
from b import Person
757+
758+
@dataclass
759+
class Asdf(Person):
760+
c: str = 'test'
761+
762+
[file a.py.2]
763+
from dataclasses import dataclass
764+
from b import Person
765+
766+
@dataclass
767+
class Asdf(Person):
768+
c: str = 'test'
769+
770+
# asdf
771+
772+
[file b.py]
773+
from dataclasses import dataclass
774+
775+
@dataclass
776+
class Person:
777+
b: int
778+
a: str = 'test'
779+
780+
[builtins fixtures/list.pyi]
781+
782+
[case testDataclassesDefaultsMroOtherFile]
783+
# flags: --python-version 3.6
784+
import a
785+
786+
[file a.py]
787+
from dataclasses import dataclass
788+
from b import A1, A2
789+
790+
@dataclass
791+
class Asdf(A1, A2): # E: Attributes without a default cannot follow attributes with one
792+
pass
793+
794+
[file b.py]
795+
from dataclasses import dataclass
796+
797+
# a bunch of blank lines to make sure the error doesn't accidentally line up...
798+
799+
800+
801+
@dataclass
802+
class A1:
803+
a: int
804+
805+
@dataclass
806+
class A2:
807+
b: str = 'test'
808+
809+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy