Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle properties in dataclasses correctly #1897

Merged
merged 1 commit into from
Dec 7, 2022
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 @@ -16,6 +16,15 @@ Release date: TBA

Refs PyCQA/pylint#2567

What's New in astroid 2.12.14?
==============================
Release date: TBA
Comment on lines +19 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Let's release this in 2.13.0. #1189 is almost done.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be okay with me doing a patch release myself? I created quite a bit of regressions (and fixes) with the dataclass refactor. I'd like to get these out quickly as I depend on them in my day-to-day.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, might be the occasion to add the backporting github action to astroid.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that decision to you. This one should be fairly trivial to backport.


* 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"]