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

Enhanced support for @Property decorator in pyreverse #10057

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
46 changes: 46 additions & 0 deletions pylint/pyreverse/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 19 additions & 2 deletions pylint/pyreverse/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
Loading