Skip to content

Commit c2615b7

Browse files
committed
Handle init=False with defaults in dataclasses (#1898)
(cherry picked from commit a178353)
1 parent c7fea76 commit c2615b7

File tree

3 files changed

+105
-19
lines changed

3 files changed

+105
-19
lines changed

ChangeLog

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

1818
Closes PyCQA/pylint#5225
1919

20+
* Handle the effect of ``init=False`` in dataclass fields correctly.
21+
22+
Closes PyCQA/pylint#7291
23+
2024
* Fix crash if ``numpy`` module doesn't have ``version`` attribute.
2125

2226
Refs PyCQA/pylint#7868

astroid/brain/brain_dataclasses.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -115,33 +115,23 @@ def _get_dataclass_attributes(
115115
) -> Iterator[nodes.AnnAssign]:
116116
"""Yield the AnnAssign nodes of dataclass attributes for the node.
117117
118-
If init is True, also include InitVars, but exclude attributes from calls to
119-
field where init=False.
118+
If init is True, also include InitVars.
120119
"""
121120
for assign_node in node.body:
122121
if not isinstance(assign_node, nodes.AnnAssign) or not isinstance(
123122
assign_node.target, nodes.AssignName
124123
):
125124
continue
126125

127-
if _is_class_var(assign_node.annotation): # type: ignore[arg-type] # annotation is never None
126+
# Annotation is never None
127+
if _is_class_var(assign_node.annotation): # type: ignore[arg-type]
128128
continue
129129

130130
if _is_keyword_only_sentinel(assign_node.annotation):
131131
continue
132132

133-
if init:
134-
value = assign_node.value
135-
if (
136-
isinstance(value, nodes.Call)
137-
and _looks_like_dataclass_field_call(value, check_scope=False)
138-
and any(
139-
keyword.arg == "init" and not keyword.value.bool_value()
140-
for keyword in value.keywords
141-
)
142-
):
143-
continue
144-
elif _is_init_var(assign_node.annotation): # type: ignore[arg-type] # annotation is never None
133+
# Annotation is never None
134+
if not init and _is_init_var(assign_node.annotation): # type: ignore[arg-type]
145135
continue
146136

147137
yield assign_node
@@ -231,6 +221,25 @@ def _find_arguments_from_base_classes(
231221
return pos_only, kw_only
232222

233223

224+
def _get_previous_field_default(node: nodes.ClassDef, name: str) -> nodes.NodeNG | None:
225+
"""Get the default value of a previously defined field."""
226+
for base in reversed(node.mro()):
227+
if not base.is_dataclass:
228+
continue
229+
if name in base.locals:
230+
for assign in base.locals[name]:
231+
if (
232+
isinstance(assign.parent, nodes.AnnAssign)
233+
and assign.parent.value
234+
and isinstance(assign.parent.value, nodes.Call)
235+
and _looks_like_dataclass_field_call(assign.parent.value)
236+
):
237+
default = _get_field_default(assign.parent.value)
238+
if default:
239+
return default[1]
240+
return None
241+
242+
234243
def _generate_dataclass_init(
235244
node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool
236245
) -> str:
@@ -254,6 +263,18 @@ def _generate_dataclass_init(
254263
property_node = additional_assign
255264
break
256265

266+
is_field = isinstance(value, nodes.Call) and _looks_like_dataclass_field_call(
267+
value, check_scope=False
268+
)
269+
270+
if is_field:
271+
# Skip any fields that have `init=False`
272+
if any(
273+
keyword.arg == "init" and not keyword.value.bool_value()
274+
for keyword in value.keywords # type: ignore[union-attr] # value is never None
275+
):
276+
continue
277+
257278
if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None
258279
init_var = True
259280
if isinstance(annotation, nodes.Subscript):
@@ -272,10 +293,8 @@ def _generate_dataclass_init(
272293
param_str = name
273294

274295
if value:
275-
if isinstance(value, nodes.Call) and _looks_like_dataclass_field_call(
276-
value, check_scope=False
277-
):
278-
result = _get_field_default(value)
296+
if is_field:
297+
result = _get_field_default(value) # type: ignore[arg-type]
279298
if result:
280299
default_type, default_node = result
281300
if default_type == "default":
@@ -296,6 +315,13 @@ def _generate_dataclass_init(
296315
param_str += f" = {next(property_node.infer_call_result()).as_string()}"
297316
except (InferenceError, StopIteration):
298317
pass
318+
else:
319+
# Even with `init=False` the default value still can be propogated to
320+
# later assignments. Creating weird signatures like:
321+
# (self, a: str = 1) -> None
322+
previous_default = _get_previous_field_default(node, name)
323+
if previous_default:
324+
param_str += f" = {previous_default.as_string()}"
299325

300326
params.append(param_str)
301327
if not init_var:

tests/unittest_brain_dataclasses.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,62 @@ class ImpossibleGrandChild(FirstChild, SecondChild, ThirdChild):
10621062
assert next(impossible.infer()) is Uninferable
10631063

10641064

1065+
def test_dataclass_with_field_init_is_false() -> None:
1066+
"""When init=False it shouldn't end up in the __init__."""
1067+
first, second, second_child, third_child, third = astroid.extract_node(
1068+
"""
1069+
from dataclasses import dataclass, field
1070+
1071+
1072+
@dataclass
1073+
class First:
1074+
a: int
1075+
1076+
@dataclass
1077+
class Second(First):
1078+
a: int = field(init=False, default=1)
1079+
1080+
@dataclass
1081+
class SecondChild(Second):
1082+
a: float
1083+
1084+
@dataclass
1085+
class ThirdChild(SecondChild):
1086+
a: str
1087+
1088+
@dataclass
1089+
class Third(First):
1090+
a: str
1091+
1092+
First.__init__ #@
1093+
Second.__init__ #@
1094+
SecondChild.__init__ #@
1095+
ThirdChild.__init__ #@
1096+
Third.__init__ #@
1097+
"""
1098+
)
1099+
1100+
first_init: bases.UnboundMethod = next(first.infer())
1101+
assert [a.name for a in first_init.args.args] == ["self", "a"]
1102+
assert [a.value for a in first_init.args.defaults] == []
1103+
1104+
second_init: bases.UnboundMethod = next(second.infer())
1105+
assert [a.name for a in second_init.args.args] == ["self"]
1106+
assert [a.value for a in second_init.args.defaults] == []
1107+
1108+
second_child_init: bases.UnboundMethod = next(second_child.infer())
1109+
assert [a.name for a in second_child_init.args.args] == ["self", "a"]
1110+
assert [a.value for a in second_child_init.args.defaults] == [1]
1111+
1112+
third_child_init: bases.UnboundMethod = next(third_child.infer())
1113+
assert [a.name for a in third_child_init.args.args] == ["self", "a"]
1114+
assert [a.value for a in third_child_init.args.defaults] == [1]
1115+
1116+
third_init: bases.UnboundMethod = next(third.infer())
1117+
assert [a.name for a in third_init.args.args] == ["self", "a"]
1118+
assert [a.value for a in third_init.args.defaults] == []
1119+
1120+
10651121
def test_dataclass_inits_of_non_dataclasses() -> None:
10661122
"""Regression test for __init__ mangling for non dataclasses.
10671123

0 commit comments

Comments
 (0)