From ceef8764dba2f386ca0c0ec2a5895dfeccd37f48 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 3 May 2025 04:00:11 +0200 Subject: [PATCH 1/4] Kwonly attributes are moved to the end in all cases, this behaviour does not depend on parent classes... --- mypy/plugins/dataclasses.py | 11 ++++++----- test-data/unit/check-dataclasses.test | 25 +++++++++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 2b4982a36bb6..316b1368c3dc 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -263,7 +263,11 @@ def transform(self) -> bool: args = [ attr.to_argument(info, of="__init__") for attr in attributes - if attr.is_in_init and not self._is_kw_only_type(attr.type) + if attr.is_in_init and not self._is_kw_only_type(attr.type) and not attr.kw_only + ] + [ + attr.to_argument(info, of="__init__") + for attr in attributes + if attr.is_in_init and not self._is_kw_only_type(attr.type) and attr.kw_only ] if info.fallback_to_any: @@ -546,7 +550,6 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # in the parent. We can implement this via a dict without disrupting the attr order # because dicts preserve insertion order in Python 3.7+. found_attrs: dict[str, DataclassAttribute] = {} - found_dataclass_supertype = False for info in reversed(cls.info.mro[1:-1]): if "dataclass_tag" in info.metadata and "dataclass" not in info.metadata: # We haven't processed the base class yet. Need another pass. @@ -556,7 +559,6 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # Each class depends on the set of attributes in its dataclass ancestors. self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname)) - found_dataclass_supertype = True for data in info.metadata["dataclass"]["attributes"]: name: str = data["name"] @@ -720,8 +722,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: ) all_attrs = list(found_attrs.values()) - if found_dataclass_supertype: - all_attrs.sort(key=lambda a: a.kw_only) + all_attrs.sort(key=lambda a: a.kw_only) # Third, ensure that arguments without a default don't follow # arguments that have a default and that the KW_ONLY sentinel diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index dbcb4c82072c..df6e1db9066e 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -460,14 +460,16 @@ 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) Application(name='name', rating=5) -Application() # E: Missing positional argument "name" in call to "Application" -Application('name') # E: Too many positional arguments for "Application" # E: Too few arguments for "Application" -Application('name', 123) # E: Too many positional arguments for "Application" -Application('name', rating=123) # E: Too many positional arguments for "Application" - +Application() # E: Missing positional argument "rating" in call to "Application" +Application(123) +Application('name') # E: Argument 1 to "Application" has incompatible type "str"; expected "int" +Application('name', 123) # E: Too many positional arguments for "Application" \ + # E: Argument 1 to "Application" has incompatible type "str"; expected "int" \ + # E: Argument 2 to "Application" has incompatible type "int"; expected "str" +Application(123, rating=123) # E: "Application" gets multiple values for keyword argument "rating" [builtins fixtures/dataclasses.pyi] [case testDataclassesOrderingKwOnlyWithSentinelAndSubclass] @@ -2610,3 +2612,14 @@ class B2(B1): # E: A NamedTuple cannot be a dataclass pass [builtins fixtures/tuple.pyi] + +[case testDataclassKwOnlyArgsLast] +from dataclasses import dataclass, field + +@dataclass +class User: + id: int = field(kw_only=True) + name: str + +User("Foo", id=0) +[builtins fixtures/tuple.pyi] From 63edf493c36def7edbee1510d4b5071c796b8788 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 3 May 2025 04:01:40 +0200 Subject: [PATCH 2/4] Add regression test for another issue --- test-data/unit/check-dataclasses.test | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index df6e1db9066e..b978a2e8428d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2623,3 +2623,19 @@ class User: User("Foo", id=0) [builtins fixtures/tuple.pyi] + +[case testDataclassKwOnlyArgsDefaultAllowedNonLast] +from dataclasses import dataclass, field + +@dataclass +class User: + id: int = field(kw_only=True, default=0) + name: str + +User() # E: Missing positional argument "name" in call to "User" +User("") +User(0) # E: Argument 1 to "User" has incompatible type "int"; expected "str" +User("", 0) # E: Too many positional arguments for "User" +User("", id=0) +User("", name="") # E: "User" gets multiple values for keyword argument "name" +[builtins fixtures/tuple.pyi] From 39138a70c960a3ecf647b3f37d4ad59eb582cd62 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 3 May 2025 04:04:32 +0200 Subject: [PATCH 3/4] Revert previous attempt... --- mypy/plugins/dataclasses.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 316b1368c3dc..99d4ef56a540 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -263,11 +263,7 @@ def transform(self) -> bool: args = [ attr.to_argument(info, of="__init__") for attr in attributes - if attr.is_in_init and not self._is_kw_only_type(attr.type) and not attr.kw_only - ] + [ - attr.to_argument(info, of="__init__") - for attr in attributes - if attr.is_in_init and not self._is_kw_only_type(attr.type) and attr.kw_only + if attr.is_in_init and not self._is_kw_only_type(attr.type) ] if info.fallback_to_any: From b9503ad39fd87f9b0dc69a7d84f379a0a53fd213 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 3 May 2025 04:30:57 +0200 Subject: [PATCH 4/4] Yes, this test was also wrong --- test-data/unit/check-dataclass-transform.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 8213f8df282a..9cc9c03448d6 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -265,7 +265,7 @@ class Foo: Foo(a=5, b_=1) # E: Unexpected keyword argument "a" for "Foo" Foo(a_=1, b_=1, noinit=1) # E: Unexpected keyword argument "noinit" for "Foo" -Foo(1, 2, 3) # E: Too many positional arguments for "Foo" +Foo(1, 2, 3) # (a, b, unused1) foo = Foo(1, 2, kwonly=3) reveal_type(foo.noinit) # N: Revealed type is "builtins.int" reveal_type(foo.unused1) # N: Revealed type is "builtins.int"