Skip to content

Commit 0bc160f

Browse files
committed
Handle properties in dataclasses correctly
1 parent c7c2bbc commit 0bc160f

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

ChangeLog

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ Release date: TBA
1616

1717
Refs PyCQA/pylint#2567
1818

19+
What's New in astroid 2.12.14?
20+
==============================
21+
Release date: TBA
22+
23+
* Handle the effect of properties on the ``__init__`` of a dataclass correctly.
24+
25+
Closes PyCQA/pylint#5225
26+
27+
1928
What's New in astroid 2.12.13?
2029
==============================
2130
Release date: 2022-11-19

astroid/brain/brain_dataclasses.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,17 @@ def _generate_dataclass_init(
243243
name, annotation, value = assign.target.name, assign.annotation, assign.value
244244
assign_names.append(name)
245245

246+
# Check whether this assign is overriden by a property assignment
247+
property_node: nodes.FunctionDef | None = None
248+
for additional_assign in node.locals[name]:
249+
if not isinstance(additional_assign, nodes.FunctionDef):
250+
continue
251+
if not additional_assign.decorators:
252+
continue
253+
if "builtins.property" in additional_assign.decoratornames():
254+
property_node = additional_assign
255+
break
256+
246257
if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None
247258
init_var = True
248259
if isinstance(annotation, nodes.Subscript):
@@ -277,6 +288,14 @@ def _generate_dataclass_init(
277288
)
278289
else:
279290
param_str += f" = {value.as_string()}"
291+
elif property_node:
292+
# We set the result of the property call as default
293+
# This hides the fact that this would normally be a 'property object'
294+
# But we can't represent those as string
295+
try:
296+
param_str += f" = {next(property_node.infer_call_result()).as_string()}"
297+
except (InferenceError, StopIteration):
298+
pass
280299

281300
params.append(param_str)
282301
if not init_var:

tests/unittest_brain_dataclasses.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,3 +1114,72 @@ def __init__(self, ef: int = 3):
11141114
third_init: bases.UnboundMethod = next(third.infer())
11151115
assert [a.name for a in third_init.args.args] == ["self", "ef"]
11161116
assert [a.value for a in third_init.args.defaults] == [3]
1117+
1118+
1119+
def test_dataclass_with_properties() -> None:
1120+
"""Tests for __init__ creation for dataclasses that use properties."""
1121+
first, second, third = astroid.extract_node(
1122+
"""
1123+
from dataclasses import dataclass
1124+
1125+
@dataclass
1126+
class Dataclass:
1127+
attr: int
1128+
1129+
@property
1130+
def attr(self) -> int:
1131+
return 1
1132+
1133+
@attr.setter
1134+
def attr(self, value: int) -> None:
1135+
pass
1136+
1137+
class ParentOne(Dataclass):
1138+
'''Docstring'''
1139+
1140+
@dataclass
1141+
class ParentTwo(Dataclass):
1142+
'''Docstring'''
1143+
1144+
Dataclass.__init__ #@
1145+
ParentOne.__init__ #@
1146+
ParentTwo.__init__ #@
1147+
"""
1148+
)
1149+
1150+
first_init: bases.UnboundMethod = next(first.infer())
1151+
assert [a.name for a in first_init.args.args] == ["self", "attr"]
1152+
assert [a.value for a in first_init.args.defaults] == [1]
1153+
1154+
second_init: bases.UnboundMethod = next(second.infer())
1155+
assert [a.name for a in second_init.args.args] == ["self", "attr"]
1156+
assert [a.value for a in second_init.args.defaults] == [1]
1157+
1158+
third_init: bases.UnboundMethod = next(third.infer())
1159+
assert [a.name for a in third_init.args.args] == ["self", "attr"]
1160+
assert [a.value for a in third_init.args.defaults] == [1]
1161+
1162+
fourth = astroid.extract_node(
1163+
"""
1164+
from dataclasses import dataclass
1165+
1166+
@dataclass
1167+
class Dataclass:
1168+
other_attr: str
1169+
attr: str
1170+
1171+
@property
1172+
def attr(self) -> str:
1173+
return self.other_attr[-1]
1174+
1175+
@attr.setter
1176+
def attr(self, value: int) -> None:
1177+
pass
1178+
1179+
Dataclass.__init__ #@
1180+
"""
1181+
)
1182+
1183+
fourth_init: bases.UnboundMethod = next(fourth.infer())
1184+
assert [a.name for a in fourth_init.args.args] == ["self", "other_attr", "attr"]
1185+
assert [a.name for a in fourth_init.args.defaults] == ["Uninferable"]

0 commit comments

Comments
 (0)