diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py index 8825363fa7..fee1e1048e 100644 --- a/pylint/pyreverse/inspector.py +++ b/pylint/pyreverse/inspector.py @@ -21,6 +21,7 @@ from pylint import constants from pylint.pyreverse import utils +from pylint.pyreverse.utils import get_annotation_label _WrapperFuncT = Callable[ [Callable[[str], nodes.Module], str, bool], Optional[nodes.Module] @@ -174,6 +175,51 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: if not isinstance(assignattr, nodes.Unknown): self.associations_handler.handle(assignattr, node) self.handle_assignattr_type(assignattr, node) + # resolve class properties + properties = [] + for name, member_list in node.locals.items(): + for member in member_list: + if isinstance(member, nodes.FunctionDef): + # Check if this function is decorated with @property + if any( + isinstance(decorator, nodes.Name) + and decorator.name == "property" + for decorator in ( + member.decorators.nodes if member.decorators else [] + ) + ): + properties.append(member) + + # Extract return type directly from the function definition + if member.returns: + # Use get_annotation_label to extract the type name + annotation_label = get_annotation_label(member.returns) + inferred_type = annotation_label + + # Special handling for enum types ==> name property is always a string + elif ( + any(base.name == "Enum" for base in node.ancestors()) + and name == "name" + ): + inferred_type = "str" + + # Fallback to inference (which may not always be perfect) + else: + inferred_nodes = utils.infer_node(member) + inferred_type = ( + ", ".join(str(inf) for inf in inferred_nodes if inf) + if inferred_nodes + else "Unknown" + ) + + # Assign to instance attributes with the inferred or annotated type + node.instance_attrs_type[name] = [inferred_type] + print( + f"Property {name} in class {node.name} has type {inferred_type}" + ) + + # Add the detected properties to a new attribute for the class definition + node.properties = properties def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Visit an astroid.Function node. diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index 093c459598..4168a975db 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -169,10 +169,27 @@ def get_package_properties(self, obj: PackageEntity) -> NodeProperties: def get_class_properties(self, obj: ClassEntity) -> NodeProperties: """Get label and shape for classes.""" + attrs = obj.attrs if not self.config.only_classnames else None + methods = obj.methods if not self.config.only_classnames else None + + if attrs and hasattr(obj.node, "properties"): + formatted_attrs = [] + property_names = {prop.name for prop in obj.node.properties} + + for attr in attrs: + name = attr.split(":")[0].strip() + if name in property_names: + # Get type from instance_attrs_type + prop_type = obj.node.instance_attrs_type.get(name, ["Unknown"])[0] + formatted_attrs.append(f"{name} «property»: {prop_type}") + else: + formatted_attrs.append(attr) + attrs = formatted_attrs + properties = NodeProperties( label=obj.title, - attrs=obj.attrs if not self.config.only_classnames else None, - methods=obj.methods if not self.config.only_classnames else None, + attrs=attrs, + methods=methods, fontcolor="red" if is_exception(obj.node) else "black", color=self.get_shape_color(obj) if self.config.colorized else "black", )