Skip to content

Commit b147d11

Browse files
authored
Move dataclass kw_only fields to the end of the signature (#19018)
Fixes #19017. Fixes #17731. This is a rather naive change: python does that at runtime. `kw_only` args can be in any order, and non-kwonly args should remain sorted as-is (stable sort). I don't understand why this was only done in presence of a parent dataclass - AFAIC kwonly fields work that way since `kw_only` was introduced in py3.10. The test I changed was invalid and asserted a false positive to the best of my knowledge.
1 parent 9ded5b1 commit b147d11

File tree

3 files changed

+37
-11
lines changed

3 files changed

+37
-11
lines changed

mypy/plugins/dataclasses.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,6 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
546546
# in the parent. We can implement this via a dict without disrupting the attr order
547547
# because dicts preserve insertion order in Python 3.7+.
548548
found_attrs: dict[str, DataclassAttribute] = {}
549-
found_dataclass_supertype = False
550549
for info in reversed(cls.info.mro[1:-1]):
551550
if "dataclass_tag" in info.metadata and "dataclass" not in info.metadata:
552551
# We haven't processed the base class yet. Need another pass.
@@ -556,7 +555,6 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
556555

557556
# Each class depends on the set of attributes in its dataclass ancestors.
558557
self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname))
559-
found_dataclass_supertype = True
560558

561559
for data in info.metadata["dataclass"]["attributes"]:
562560
name: str = data["name"]
@@ -720,8 +718,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
720718
)
721719

722720
all_attrs = list(found_attrs.values())
723-
if found_dataclass_supertype:
724-
all_attrs.sort(key=lambda a: a.kw_only)
721+
all_attrs.sort(key=lambda a: a.kw_only)
725722

726723
# Third, ensure that arguments without a default don't follow
727724
# arguments that have a default and that the KW_ONLY sentinel

test-data/unit/check-dataclass-transform.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class Foo:
265265

266266
Foo(a=5, b_=1) # E: Unexpected keyword argument "a" for "Foo"
267267
Foo(a_=1, b_=1, noinit=1) # E: Unexpected keyword argument "noinit" for "Foo"
268-
Foo(1, 2, 3) # E: Too many positional arguments for "Foo"
268+
Foo(1, 2, 3) # (a, b, unused1)
269269
foo = Foo(1, 2, kwonly=3)
270270
reveal_type(foo.noinit) # N: Revealed type is "builtins.int"
271271
reveal_type(foo.unused1) # N: Revealed type is "builtins.int"

test-data/unit/check-dataclasses.test

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -460,14 +460,16 @@ from dataclasses import dataclass, field, KW_ONLY
460460
class Application:
461461
_: KW_ONLY
462462
name: str = 'Unnamed'
463-
rating: int = field(kw_only=False) # E: Attributes without a default cannot follow attributes with one
463+
rating: int = field(kw_only=False)
464464

465465
Application(name='name', rating=5)
466-
Application() # E: Missing positional argument "name" in call to "Application"
467-
Application('name') # E: Too many positional arguments for "Application" # E: Too few arguments for "Application"
468-
Application('name', 123) # E: Too many positional arguments for "Application"
469-
Application('name', rating=123) # E: Too many positional arguments for "Application"
470-
466+
Application() # E: Missing positional argument "rating" in call to "Application"
467+
Application(123)
468+
Application('name') # E: Argument 1 to "Application" has incompatible type "str"; expected "int"
469+
Application('name', 123) # E: Too many positional arguments for "Application" \
470+
# E: Argument 1 to "Application" has incompatible type "str"; expected "int" \
471+
# E: Argument 2 to "Application" has incompatible type "int"; expected "str"
472+
Application(123, rating=123) # E: "Application" gets multiple values for keyword argument "rating"
471473
[builtins fixtures/dataclasses.pyi]
472474

473475
[case testDataclassesOrderingKwOnlyWithSentinelAndSubclass]
@@ -2618,3 +2620,30 @@ raw_target: object
26182620
if isinstance(raw_target, type) and dataclasses.is_dataclass(raw_target):
26192621
reveal_type(raw_target) # N: Revealed type is "type[dataclasses.DataclassInstance]"
26202622
[builtins fixtures/tuple.pyi]
2623+
2624+
[case testDataclassKwOnlyArgsLast]
2625+
from dataclasses import dataclass, field
2626+
2627+
@dataclass
2628+
class User:
2629+
id: int = field(kw_only=True)
2630+
name: str
2631+
2632+
User("Foo", id=0)
2633+
[builtins fixtures/tuple.pyi]
2634+
2635+
[case testDataclassKwOnlyArgsDefaultAllowedNonLast]
2636+
from dataclasses import dataclass, field
2637+
2638+
@dataclass
2639+
class User:
2640+
id: int = field(kw_only=True, default=0)
2641+
name: str
2642+
2643+
User() # E: Missing positional argument "name" in call to "User"
2644+
User("")
2645+
User(0) # E: Argument 1 to "User" has incompatible type "int"; expected "str"
2646+
User("", 0) # E: Too many positional arguments for "User"
2647+
User("", id=0)
2648+
User("", name="") # E: "User" gets multiple values for keyword argument "name"
2649+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)