Skip to content

Commit 406dfbc

Browse files
committed
Handle kw_only=True in dataclass fields
1 parent a178353 commit 406dfbc

File tree

3 files changed

+72
-14
lines changed

3 files changed

+72
-14
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Release date: TBA
2424

2525
Closes PyCQA/pylint#5225
2626

27+
* Handle the effect of ``kw_only=True`` in dataclass fields correctly.
28+
29+
Closes PyCQA/pylint#7623
30+
2731
* Handle the effect of ``init=False`` in dataclass fields correctly.
2832

2933
Closes PyCQA/pylint#7291

astroid/brain/brain_dataclasses.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,12 @@ def _get_previous_field_default(node: nodes.ClassDef, name: str) -> nodes.NodeNG
240240
return None
241241

242242

243-
def _generate_dataclass_init(
243+
def _generate_dataclass_init( # pylint: disable=too-many-locals
244244
node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool
245245
) -> str:
246246
"""Return an init method for a dataclass given the targets."""
247247
params: list[str] = []
248+
kw_only_params: list[str] = []
248249
assignments: list[str] = []
249250
assign_names: list[str] = []
250251

@@ -323,7 +324,22 @@ def _generate_dataclass_init(
323324
if previous_default:
324325
param_str += f" = {previous_default.as_string()}"
325326

326-
params.append(param_str)
327+
# If the field is a kw_only field, we need to add it to the kw_only_params
328+
# This overwrites whether or not the class is kw_only decorated
329+
if is_field:
330+
kw_only = [k for k in value.keywords if k.arg == "kw_only"] # type: ignore[union-attr]
331+
if kw_only:
332+
if kw_only[0].value.bool_value():
333+
kw_only_params.append(param_str)
334+
else:
335+
params.append(param_str)
336+
continue
337+
# If kw_only decorated, we need to add all parameters to the kw_only_params
338+
if kw_only_decorated:
339+
kw_only_params.append(param_str)
340+
else:
341+
params.append(param_str)
342+
327343
if not init_var:
328344
assignments.append(assignment_str)
329345

@@ -332,21 +348,16 @@ def _generate_dataclass_init(
332348
)
333349

334350
# Construct the new init method paramter string
335-
params_string = "self, "
336-
if prev_pos_only:
337-
params_string += prev_pos_only
338-
if not kw_only_decorated:
339-
params_string += ", ".join(params)
340-
351+
# First we do the positional only parameters, making sure to add the
352+
# the self parameter and the comma to allow adding keyword only parameters
353+
params_string = f"self, {prev_pos_only}{', '.join(params)}"
341354
if not params_string.endswith(", "):
342355
params_string += ", "
343356

344-
if prev_kw_only:
345-
params_string += "*, " + prev_kw_only
346-
if kw_only_decorated:
347-
params_string += ", ".join(params) + ", "
348-
elif kw_only_decorated:
349-
params_string += "*, " + ", ".join(params) + ", "
357+
# Then we add the keyword only parameters
358+
if prev_kw_only or kw_only_params:
359+
params_string += "*, "
360+
params_string += f"{prev_kw_only}{', '.join(kw_only_params)}"
350361

351362
assignments_string = "\n ".join(assignments) if assignments else "pass"
352363
return f"def __init__({params_string}) -> None:\n {assignments_string}"

tests/unittest_brain_dataclasses.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,49 @@ class Dee(Cee):
826826
assert [a.name for a in dee_init.args.kwonlyargs] == []
827827

828828

829+
def test_kw_only_in_field_call() -> None:
830+
"""Test that keyword only fields get correctly put at the end of the __init__."""
831+
832+
first, second, third = astroid.extract_node(
833+
"""
834+
from dataclasses import dataclass, field
835+
836+
@dataclass
837+
class Parent:
838+
p1: int = field(kw_only=True, default=0)
839+
840+
@dataclass
841+
class Child(Parent):
842+
c1: str
843+
844+
@dataclass(kw_only=True)
845+
class GrandChild(Child):
846+
p2: int = field(kw_only=False, default=1)
847+
p3: int = field(kw_only=True, default=2)
848+
849+
Parent.__init__ #@
850+
Child.__init__ #@
851+
GrandChild.__init__ #@
852+
"""
853+
)
854+
855+
first_init: bases.UnboundMethod = next(first.infer())
856+
assert [a.name for a in first_init.args.args] == ["self"]
857+
assert [a.name for a in first_init.args.kwonlyargs] == ["p1"]
858+
assert [d.value for d in first_init.args.kw_defaults] == [0]
859+
860+
second_init: bases.UnboundMethod = next(second.infer())
861+
assert [a.name for a in second_init.args.args] == ["self", "c1"]
862+
assert [a.name for a in second_init.args.kwonlyargs] == ["p1"]
863+
assert [d.value for d in second_init.args.kw_defaults] == [0]
864+
865+
third_init: bases.UnboundMethod = next(third.infer())
866+
assert [a.name for a in third_init.args.args] == ["self", "c1", "p2"]
867+
assert [a.name for a in third_init.args.kwonlyargs] == ["p1", "p3"]
868+
assert [d.value for d in third_init.args.defaults] == [1]
869+
assert [d.value for d in third_init.args.kw_defaults] == [0, 2]
870+
871+
829872
def test_dataclass_with_unknown_base() -> None:
830873
"""Regression test for dataclasses with unknown base classes.
831874

0 commit comments

Comments
 (0)