Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ Release date: TBA



What's New in astroid 2.12.14?
==============================
Release date: TBA

* Handle the effect of properties on the ``__init__`` of a dataclass correctly.

Closes PyCQA/pylint#5225


What's New in astroid 2.12.13?
==============================
Release date: 2022-11-19
Expand Down
19 changes: 19 additions & 0 deletions astroid/brain/brain_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ def _generate_dataclass_init(
name, annotation, value = assign.target.name, assign.annotation, assign.value
assign_names.append(name)

# Check whether this assign is overriden by a property assignment
property_node: nodes.FunctionDef | None = None
for additional_assign in node.locals[name]:
if not isinstance(additional_assign, nodes.FunctionDef):
continue
if not additional_assign.decorators:
continue
if "builtins.property" in additional_assign.decoratornames():
property_node = additional_assign
break

if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None
init_var = True
if isinstance(annotation, nodes.Subscript):
Expand Down Expand Up @@ -277,6 +288,14 @@ def _generate_dataclass_init(
)
else:
param_str += f" = {value.as_string()}"
elif property_node:
# We set the result of the property call as default
# This hides the fact that this would normally be a 'property object'
# But we can't represent those as string
try:
param_str += f" = {next(property_node.infer_call_result()).as_string()}"
except (InferenceError, StopIteration):
pass

params.append(param_str)
if not init_var:
Expand Down
69 changes: 69 additions & 0 deletions tests/unittest_brain_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,3 +1114,72 @@ def __init__(self, ef: int = 3):
third_init: bases.UnboundMethod = next(third.infer())
assert [a.name for a in third_init.args.args] == ["self", "ef"]
assert [a.value for a in third_init.args.defaults] == [3]


def test_dataclass_with_properties() -> None:
"""Tests for __init__ creation for dataclasses that use properties."""
first, second, third = astroid.extract_node(
"""
from dataclasses import dataclass

@dataclass
class Dataclass:
attr: int

@property
def attr(self) -> int:
return 1

@attr.setter
def attr(self, value: int) -> None:
pass

class ParentOne(Dataclass):
'''Docstring'''

@dataclass
class ParentTwo(Dataclass):
'''Docstring'''

Dataclass.__init__ #@
ParentOne.__init__ #@
ParentTwo.__init__ #@
"""
)

first_init: bases.UnboundMethod = next(first.infer())
assert [a.name for a in first_init.args.args] == ["self", "attr"]
assert [a.value for a in first_init.args.defaults] == [1]

second_init: bases.UnboundMethod = next(second.infer())
assert [a.name for a in second_init.args.args] == ["self", "attr"]
assert [a.value for a in second_init.args.defaults] == [1]

third_init: bases.UnboundMethod = next(third.infer())
assert [a.name for a in third_init.args.args] == ["self", "attr"]
assert [a.value for a in third_init.args.defaults] == [1]

fourth = astroid.extract_node(
"""
from dataclasses import dataclass

@dataclass
class Dataclass:
other_attr: str
attr: str

@property
def attr(self) -> str:
return self.other_attr[-1]

@attr.setter
def attr(self, value: int) -> None:
pass

Dataclass.__init__ #@
"""
)

fourth_init: bases.UnboundMethod = next(fourth.infer())
assert [a.name for a in fourth_init.args.args] == ["self", "other_attr", "attr"]
assert [a.name for a in fourth_init.args.defaults] == ["Uninferable"]