Skip to content

Commit aaf9828

Browse files
Allow attrs kw_only arguments at any position (#8803)
Fixes #7489.
1 parent 8748e69 commit aaf9828

File tree

3 files changed

+15
-35
lines changed

3 files changed

+15
-35
lines changed

mypy/plugins/attrs.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,15 +358,13 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
358358
# Check the init args for correct default-ness. Note: This has to be done after all the
359359
# attributes for all classes have been read, because subclasses can override parents.
360360
last_default = False
361-
last_kw_only = False
362361

363362
for i, attribute in enumerate(attributes):
364363
if not attribute.init:
365364
continue
366365

367366
if attribute.kw_only:
368367
# Keyword-only attributes don't care whether they are default or not.
369-
last_kw_only = True
370368
continue
371369

372370
# If the issue comes from merging different classes, report it
@@ -377,11 +375,6 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
377375
ctx.api.fail(
378376
"Non-default attributes not allowed after default attributes.",
379377
context)
380-
if last_kw_only:
381-
ctx.api.fail(
382-
"Non keyword-only attributes are not allowed after a keyword-only attribute.",
383-
context
384-
)
385378
last_default |= attribute.has_default
386379

387380
return attributes
@@ -626,7 +619,18 @@ def _make_frozen(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute]
626619
def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute],
627620
adder: 'MethodAdder') -> None:
628621
"""Generate an __init__ method for the attributes and add it to the class."""
629-
args = [attribute.argument(ctx) for attribute in attributes if attribute.init]
622+
# Convert attributes to arguments with kw_only arguments at the end of
623+
# the argument list
624+
pos_args = []
625+
kw_only_args = []
626+
for attribute in attributes:
627+
if not attribute.init:
628+
continue
629+
if attribute.kw_only:
630+
kw_only_args.append(attribute.argument(ctx))
631+
else:
632+
pos_args.append(attribute.argument(ctx))
633+
args = pos_args + kw_only_args
630634
if all(
631635
# We use getattr rather than instance checks because the variable.type
632636
# might be wrapped into a Union or some other type, but even non-Any

test-data/unit/check-attr.test

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,11 +1070,12 @@ class A:
10701070
a = attr.ib(default=0)
10711071
@attr.s
10721072
class B(A):
1073-
b = attr.ib() # E: Non keyword-only attributes are not allowed after a keyword-only attribute.
1073+
b = attr.ib()
10741074
@attr.s
10751075
class C:
10761076
a = attr.ib(kw_only=True)
1077-
b = attr.ib(15) # E: Non keyword-only attributes are not allowed after a keyword-only attribute.
1077+
b = attr.ib(15)
1078+
10781079
[builtins fixtures/attr.pyi]
10791080

10801081
[case testAttrsKwOnlyPy2]

test-data/unit/fine-grained.test

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,31 +1023,6 @@ class A:
10231023
==
10241024
main:2: error: Unsupported left operand type for < ("B")
10251025

1026-
[case testAttrsUpdateKwOnly]
1027-
[file a.py]
1028-
import attr
1029-
@attr.s(kw_only=True)
1030-
class A:
1031-
a = attr.ib(15) # type: int
1032-
[file b.py]
1033-
from a import A
1034-
import attr
1035-
@attr.s(kw_only=True)
1036-
class B(A):
1037-
b = attr.ib("16") # type: str
1038-
1039-
[file b.py.2]
1040-
from a import A
1041-
import attr
1042-
@attr.s()
1043-
class B(A):
1044-
b = attr.ib("16") # type: str
1045-
B(b="foo", a=7)
1046-
[builtins fixtures/attr.pyi]
1047-
[out]
1048-
==
1049-
b.py:5: error: Non keyword-only attributes are not allowed after a keyword-only attribute.
1050-
10511026
[case testAttrsUpdateBaseKwOnly]
10521027
from b import B
10531028
B(5)

0 commit comments

Comments
 (0)