Skip to content

Enhanced support for @Property decorator in pyreverse #10057

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

Merged
merged 9 commits into from
Mar 10, 2025
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
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10057.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Enhanced support for @property decorator in pyreverse to correctly display return types of annotated properties when generating class diagrams.

Closes #10057
24 changes: 20 additions & 4 deletions pylint/pyreverse/diagrams.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from astroid import nodes, util

from pylint.checkers.utils import decorated_with_property, in_type_checking_block
from pylint.pyreverse.utils import FilterMixIn
from pylint.pyreverse.utils import FilterMixIn, get_annotation_label


class Figure:
Expand Down Expand Up @@ -121,12 +121,16 @@ def get_relationship(
def get_attrs(self, node: nodes.ClassDef) -> list[str]:
"""Return visible attributes, possibly with class name."""
attrs = []

# Collect functions decorated with @property
properties = {
local_name: local_node
for local_name, local_node in node.items()
if isinstance(local_node, nodes.FunctionDef)
and decorated_with_property(local_node)
}

# Add instance attributes to properties
for attr_name, attr_type in list(node.locals_type.items()) + list(
node.instance_attrs_type.items()
):
Expand All @@ -136,9 +140,21 @@ def get_attrs(self, node: nodes.ClassDef) -> list[str]:
for node_name, associated_nodes in properties.items():
if not self.show_attr(node_name):
continue
names = self.class_names(associated_nodes)
if names:
node_name = f"{node_name} : {', '.join(names)}"

# Handle property methods differently to correctly extract return type
if isinstance(
associated_nodes, nodes.FunctionDef
) and decorated_with_property(associated_nodes):
if associated_nodes.returns:
type_annotation = get_annotation_label(associated_nodes.returns)
node_name = f"{node_name} : {type_annotation}"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not that experienced, but do you think it is possible to infer the return type using astroid in case the property is not annotated? In essence add an else branch in here

Copy link
Member

Choose a reason for hiding this comment

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

In pylint we infer only and do not trust the typing (because there was no typing when pylint was created), so yes. Not sure what the philosophy in pyreverse should be. Probably that trusting the typing first then inferring makes sense (even if changing the philosophy in pylint would be humongous work and probably make pylint inconsistent).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this philosophy makes sense. Also assumed that pylint works that way, this is why I tried to come up with a solution using node.infer_call_result() however I always got UNINFERABLE. Probalby it is an error on my side, because I have not that much experience with astroid :)

Copy link
Member

Choose a reason for hiding this comment

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

I'm not surprised you're getting uninferable, property inferring in pylint is not very good. Making property inference in astroid better would help in both pyreverse and pylint but no one worked on it as of today.

Copy link
Contributor Author

@Julfried Julfried Mar 6, 2025

Choose a reason for hiding this comment

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

Makes a lot of sense. So I guess for now relying on the annotations for the Properties is fine. Unfortunately I currently dont have the time to work on that myself. Maybe something to work on in a follow up PR


# Handle regular attributes
else:
names = self.class_names(associated_nodes)
if names:
node_name = f"{node_name} : {', '.join(names)}"

attrs.append(node_name)
return sorted(attrs)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
classDiagram
class AnnotatedPropertyTest {
x : int
}
class NonAnnotatedPropertyTest {
x
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# property_decorator.py
class AnnotatedPropertyTest:
"""Test class for property decorators with annotated return type"""
def __init__(self):
self._x = 0

@property
def x(self) -> int:
"""This is a getter for x"""
return self._x

@x.setter
def x(self, value):
"""This is a setter for x"""
self._x = value

@x.deleter
def x(self):
"""This is a deleter for x"""
del self._x


class NonAnnotatedPropertyTest:
"""Test class for property decorators without annotated return type"""
def __init__(self):
self._x = 0

@property
def x(self):
"""This is a getter for x"""
return self._x

@x.setter
def x(self, value):
"""This is a setter for x"""
self._x = value

@x.deleter
def x(self):
"""This is a deleter for x"""
del self._x
Loading