diff --git a/doc/whatsnew/fragments/10057.feature b/doc/whatsnew/fragments/10057.feature new file mode 100644 index 0000000000..41fd94e5b7 --- /dev/null +++ b/doc/whatsnew/fragments/10057.feature @@ -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 diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py index 278102cab8..6db0563003 100644 --- a/pylint/pyreverse/diagrams.py +++ b/pylint/pyreverse/diagrams.py @@ -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: @@ -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() ): @@ -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}" + + # 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) diff --git a/tests/pyreverse/functional/class_diagrams/property_decorator/property_decorator.mmd b/tests/pyreverse/functional/class_diagrams/property_decorator/property_decorator.mmd new file mode 100644 index 0000000000..af55f86849 --- /dev/null +++ b/tests/pyreverse/functional/class_diagrams/property_decorator/property_decorator.mmd @@ -0,0 +1,7 @@ +classDiagram + class AnnotatedPropertyTest { + x : int + } + class NonAnnotatedPropertyTest { + x + } diff --git a/tests/pyreverse/functional/class_diagrams/property_decorator/property_decorator.py b/tests/pyreverse/functional/class_diagrams/property_decorator/property_decorator.py new file mode 100644 index 0000000000..7a87e6b5b9 --- /dev/null +++ b/tests/pyreverse/functional/class_diagrams/property_decorator/property_decorator.py @@ -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