diff --git a/ChangeLog b/ChangeLog index 4ea98fedfd..e71e0a8a2e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,15 @@ Release date: TBA Refs PyCQA/pylint#2567 +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 diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 5d3c346101..a84805d2da 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -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): @@ -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: diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index a65a8dec0e..9df5993f80 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -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"]