Skip to content

Commit f45bc2c

Browse files
committed
Introduce composition
1 parent 37b4d98 commit f45bc2c

File tree

4 files changed

+66
-10
lines changed

4 files changed

+66
-10
lines changed

pylint/pyreverse/diagrams.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,15 +234,22 @@ def extract_relationships(self) -> None:
234234
except KeyError:
235235
continue
236236

237-
# associations & aggregations links
237+
# Composition links
238+
for name, values in list(node.compositions_type.items()):
239+
for value in values:
240+
self.assign_association_relationship(
241+
value, obj, name, "composition"
242+
)
243+
244+
# Aggregation links
238245
for name, values in list(node.aggregations_type.items()):
239246
for value in values:
240247
self.assign_association_relationship(
241248
value, obj, name, "aggregation"
242249
)
243250

251+
# Association links
244252
associations = node.associations_type.copy()
245-
246253
for name, values in node.locals_type.items():
247254
if name not in associations:
248255
associations[name] = values

pylint/pyreverse/inspector.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,14 @@ def __init__(self, project: Project, tag: bool = False) -> None:
122122
self.tag = tag
123123
# visited project
124124
self.project = project
125-
self.associations_handler = AggregationsHandler()
126-
self.associations_handler.set_next(OtherAssociationsHandler())
125+
126+
# Chain: Composition → Aggregation → Association
127+
self.associations_handler = CompositionsHandler()
128+
aggregation_handler = AggregationsHandler()
129+
association_handler = AssociationsHandler()
130+
131+
self.associations_handler.set_next(aggregation_handler)
132+
aggregation_handler.set_next(association_handler)
127133

128134
def visit_project(self, node: Project) -> None:
129135
"""Visit a pyreverse.utils.Project node.
@@ -167,6 +173,7 @@ def visit_classdef(self, node: nodes.ClassDef) -> None:
167173
specializations.append(node)
168174
baseobj.specializations = specializations
169175
# resolve instance attributes
176+
node.compositions_type = collections.defaultdict(list)
170177
node.instance_attrs_type = collections.defaultdict(list)
171178
node.aggregations_type = collections.defaultdict(list)
172179
node.associations_type = collections.defaultdict(list)
@@ -327,28 +334,50 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
327334
self._next_handler.handle(node, parent)
328335

329336

337+
class CompositionsHandler(AbstractAssociationHandler):
338+
"""Handle composition relationships where parent creates child objects."""
339+
340+
def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
341+
if not isinstance(node.parent, (nodes.AnnAssign, nodes.Assign)):
342+
super().handle(node, parent)
343+
return
344+
345+
value = node.parent.value
346+
347+
# Composition: parent creates child (self.x = P())
348+
if isinstance(value, nodes.Call):
349+
current = set(parent.compositions_type[node.attrname])
350+
parent.compositions_type[node.attrname] = list(
351+
current | utils.infer_node(node)
352+
)
353+
return
354+
355+
# Not a composition, pass to next handler
356+
super().handle(node, parent)
357+
358+
330359
class AggregationsHandler(AbstractAssociationHandler):
360+
"""Handle aggregation relationships where parent receives child objects."""
361+
331362
def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
332-
# Check if we're not in an assignment context
333363
if not isinstance(node.parent, (nodes.AnnAssign, nodes.Assign)):
334364
super().handle(node, parent)
335365
return
336366

337367
value = node.parent.value
338368

339-
# Handle direct name assignments
369+
# Aggregation: parent receives child (self.x = x)
340370
if isinstance(value, astroid.node_classes.Name):
341371
current = set(parent.aggregations_type[node.attrname])
342372
parent.aggregations_type[node.attrname] = list(
343373
current | utils.infer_node(node)
344374
)
345375
return
346376

347-
# Handle comprehensions
377+
# Aggregation: comprehensions (self.x = [P() for ...])
348378
if isinstance(
349379
value, (nodes.ListComp, nodes.DictComp, nodes.SetComp, nodes.GeneratorExp)
350380
):
351-
# Determine the type of the element in the comprehension
352381
if isinstance(value, nodes.DictComp):
353382
element_type = safe_infer(value.value)
354383
else:
@@ -358,12 +387,23 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
358387
parent.aggregations_type[node.attrname] = list(current | {element_type})
359388
return
360389

361-
# Fallback to parent handler
390+
# Type annotation only (x: P) defaults to aggregation
391+
if isinstance(node.parent, nodes.AnnAssign) and node.parent.value is None:
392+
current = set(parent.aggregations_type[node.attrname])
393+
parent.aggregations_type[node.attrname] = list(
394+
current | utils.infer_node(node)
395+
)
396+
return
397+
398+
# Not an aggregation, pass to next handler
362399
super().handle(node, parent)
363400

364401

365-
class OtherAssociationsHandler(AbstractAssociationHandler):
402+
class AssociationsHandler(AbstractAssociationHandler):
403+
"""Handle regular association relationships."""
404+
366405
def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
406+
# Everything else is a regular association
367407
current = set(parent.associations_type[node.attrname])
368408
parent.associations_type[node.attrname] = list(current | utils.infer_node(node))
369409

pylint/pyreverse/printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class NodeType(Enum):
2222

2323
class EdgeType(Enum):
2424
INHERITS = "inherits"
25+
COMPOSITION = "composition"
2526
ASSOCIATION = "association"
2627
AGGREGATION = "aggregation"
2728
USES = "uses"

pylint/pyreverse/writer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ def write_classes(self, diagram: ClassDiagram) -> None:
146146
label=rel.name,
147147
type_=EdgeType.ASSOCIATION,
148148
)
149+
# generate compositions
150+
for rel in diagram.get_relationships("composition"):
151+
self.printer.emit_edge(
152+
rel.from_object.fig_id,
153+
rel.to_object.fig_id,
154+
label=rel.name,
155+
type_=EdgeType.COMPOSITION,
156+
)
149157
# generate aggregations
150158
for rel in diagram.get_relationships("aggregation"):
151159
if rel.to_object.fig_id in associations[rel.from_object.fig_id]:

0 commit comments

Comments
 (0)