Skip to content

Commit 1abd975

Browse files
Julfriedpre-commit-ci[bot]DudeNr33
authored
Refactor pyreverse Association Logic (#10397)
* Correct the test output to follow UML semantics * Introduce composition * Update all the printers to emit the right arrow types * Update docstring * Remove type annotations from test output * Avoid processing duplicate relationships * Update expected files again -> defaults to association * change arrowhead for dot language for association relationsships * Update comment * Update comments in test file * rename test folders for better clarity * Enhance composition and aggregation handling in AST node processing so that the differentation between aggregation and composition is correct according to UMLEnhance composition and aggregation handling in AST node processing so that the differentation between aggregation and composition is correct according to UML * Update relationship extraction to avoid duplicate entries * Correctly infer the node type for Composition * Update the functional test for comprehensions aswell * Instead of checking not call node check for name node ==> more explicit * Remove redundant checks in AggregationHandler * Add todo note because infering type in Aggregation comprehensions is unreliable for now * Enhance type resolution in AssociationsHandler and add utility functions for extracting element types and resolving class definitionsEnhance type resolution in AssociationsHandler and add utility functions for extracting element types and resolving class definitions * Fix order so that tests pass * Update the functional test files for attribute annotation, because now pyreverse correctly extracts Dummy as Association * Use the new utility function for the other handlers aswell * Fix regression that did not correctly detect Composition in fields.py * Revert functional test for comprehension ==> this now works with new util functions * Add correct arrow type for dot printer * Update functional tests to include all file formats * Fix diadefs tests (DoNothing now is correctly detected as Composition instead of assoiciation) * Remove TODO since this now works * Rename functional test files to relationships for better clarity * rename to compositionshandler for better clarity * Rename association-related classes to relationship aswell for improved clarity * Try to fix the failing tests by processing instance attributes and class level attributes separate * Fix diadefs_test again (cls_member is now also correctly identified as composition instead of association) * Also consider composition when when filtering for --no-standalone ==> fixes last failing test * Add newsfragment * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add functional tests * Update newsfragement * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestions from code review Co-authored-by: Andreas Finkler <[email protected]> * Update fields test files * Update comprehensions test file * Reverse arrows in attribute annotation test files aswell * Reverse arrows in writer --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Andreas Finkler <[email protected]>
1 parent 4f98c18 commit 1abd975

40 files changed

+498
-133
lines changed

doc/whatsnew/fragments/9045.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Enhanced pyreverse to properly distinguish between UML relationship types (association, aggregation, composition) based on object ownership semantics. Type annotations without assignment are now treated as associations, parameter assignments as aggregations, and object instantiation as compositions.
2+
3+
Closes #9045
4+
Closes #9267

pylint/pyreverse/diagrams.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ def extract_relationships(self) -> None:
226226
obj.attrs = self.get_attrs(node)
227227
obj.methods = self.get_methods(node)
228228
obj.shape = "class"
229+
229230
# inheritance link
230231
for par_node in node.ancestors(recurs=False):
231232
try:
@@ -234,27 +235,41 @@ def extract_relationships(self) -> None:
234235
except KeyError:
235236
continue
236237

237-
# associations & aggregations links
238-
for name, values in list(node.aggregations_type.items()):
238+
# Track processed attributes to avoid duplicates
239+
processed_attrs = set()
240+
241+
# Process in priority order: Composition > Aggregation > Association
242+
243+
# 1. Composition links (highest priority)
244+
for name, values in list(node.compositions_type.items()):
245+
if not self.show_attr(name):
246+
continue
239247
for value in values:
240-
if not self.show_attr(name):
241-
continue
248+
self.assign_association_relationship(
249+
value, obj, name, "composition"
250+
)
251+
processed_attrs.add(name)
242252

253+
# 2. Aggregation links (medium priority)
254+
for name, values in list(node.aggregations_type.items()):
255+
if not self.show_attr(name) or name in processed_attrs:
256+
continue
257+
for value in values:
243258
self.assign_association_relationship(
244259
value, obj, name, "aggregation"
245260
)
261+
processed_attrs.add(name)
246262

263+
# 3. Association links (lowest priority)
247264
associations = node.associations_type.copy()
248-
249265
for name, values in node.locals_type.items():
250266
if name not in associations:
251267
associations[name] = values
252268

253269
for name, values in associations.items():
270+
if not self.show_attr(name) or name in processed_attrs:
271+
continue
254272
for value in values:
255-
if not self.show_attr(name):
256-
continue
257-
258273
self.assign_association_relationship(
259274
value, obj, name, "association"
260275
)

pylint/pyreverse/dot_printer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ class HTMLLabels(Enum):
3030
# pylint: disable-next=consider-using-namedtuple-or-dataclass
3131
ARROWS: dict[EdgeType, dict[str, str]] = {
3232
EdgeType.INHERITS: {"arrowtail": "none", "arrowhead": "empty"},
33-
EdgeType.ASSOCIATION: {
33+
EdgeType.COMPOSITION: {
3434
"fontcolor": "green",
3535
"arrowtail": "none",
3636
"arrowhead": "diamond",
3737
"style": "solid",
3838
},
39+
EdgeType.ASSOCIATION: {
40+
"fontcolor": "green",
41+
"arrowtail": "none",
42+
"arrowhead": "vee",
43+
"style": "solid",
44+
},
3945
EdgeType.AGGREGATION: {
4046
"fontcolor": "green",
4147
"arrowtail": "none",

0 commit comments

Comments
 (0)