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

Add --max-depth option to control diagram complexity #10077

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions doc/additional_tools/pyreverse/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ Display Options
**Default:** ``2``


--max-depth
-----------
*Maximum depth in package/module hierarchy to display. A depth of 0 shows only top-level packages, 1 shows one level of subpackages, etc. If not specified, all packages/modules are shown.*

**Default:** ``None``


--module-names
--------------
*Include module name in the representation of classes.*
Expand Down
16 changes: 16 additions & 0 deletions pylint/pyreverse/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,22 @@
"help": "Use colored output. Classes/modules of the same package get the same color.",
},
),
(
"max-depth",
{
"dest": "max_depth",
"action": "store",
"default": None,
"metavar": "<depth>",
"type": "int",
"group": OPTIONS_GROUPS["DISPLAY"],
"help": (
"Maximum depth in package/module hierarchy to display. A depth of 0 shows only "
"top-level packages, 1 shows one level of subpackages, etc. If not specified, "
"all packages/modules are shown."
),
},
),
(
"max-color-depth",
{
Expand Down
59 changes: 59 additions & 0 deletions pylint/pyreverse/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
self.printer: Printer # defined in set_printer
self.file_name = "" # defined in set_printer
self.depth = self.config.max_color_depth
self.max_depth = self.config.max_depth
# default colors are an adaption of the seaborn colorblind palette
self.available_colors = itertools.cycle(self.config.color_palette)
self.used_colors: dict[str, str] = {}
Expand All @@ -53,6 +54,29 @@
self.write_classes(diagram)
self.save()

def should_show_node(self, qualified_name: str, is_class: bool = False) -> bool:
"""Determine if a node should be shown based on depth settings."""
# If no depth limit is set ==> show all nodes
if self.max_depth is None:
return True

# For classes, we want to measure depth from their containing module
if is_class:

Check warning on line 64 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L64

Added line #L64 was not covered by tests
# Get the module part (everything before the last dot)
last_dot = qualified_name.rfind(".")
if last_dot == -1:
module_path = ""

Check warning on line 68 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L66-L68

Added lines #L66 - L68 were not covered by tests
else:
module_path = qualified_name[:last_dot]

Check warning on line 70 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L70

Added line #L70 was not covered by tests

# Count module depth
module_depth = module_path.count(".")
return bool(module_depth <= self.max_depth)

Check warning on line 74 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L73-L74

Added lines #L73 - L74 were not covered by tests

# For packages/modules, count full depth
node_depth = qualified_name.count(".")
return bool(node_depth <= self.max_depth)

Check warning on line 78 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L77-L78

Added lines #L77 - L78 were not covered by tests

def write_packages(self, diagram: PackageDiagram) -> None:
"""Write a package diagram."""
module_info: dict[str, dict[str, int]] = {}
Expand All @@ -61,6 +85,10 @@
for module in sorted(diagram.modules(), key=lambda x: x.title):
module.fig_id = module.node.qname()

# Filter nodes based on depth
if not self.should_show_node(module.fig_id):
continue

Check warning on line 90 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L90

Added line #L90 was not covered by tests

if self.config.no_standalone and not any(
module in (rel.from_object, rel.to_object)
for rel in diagram.get_relationships("depends")
Expand All @@ -83,6 +111,10 @@
from_id = rel.from_object.fig_id
to_id = rel.to_object.fig_id

# Filter nodes based on depth ==> skip if either source or target nodes is beyond the max depth
if not self.should_show_node(from_id) or not self.should_show_node(to_id):
continue

Check warning on line 116 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L116

Added line #L116 was not covered by tests

self.printer.emit_edge(
from_id,
to_id,
Expand All @@ -96,6 +128,10 @@
from_id = rel.from_object.fig_id
to_id = rel.to_object.fig_id

# Filter nodes based on depth ==> skip if either source or target nodes is beyond the max depth
if not self.should_show_node(from_id) or not self.should_show_node(to_id):
continue

Check warning on line 133 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L133

Added line #L133 was not covered by tests

self.printer.emit_edge(
from_id,
to_id,
Expand All @@ -115,6 +151,11 @@
# sorted to get predictable (hence testable) results
for obj in sorted(diagram.objects, key=lambda x: x.title):
obj.fig_id = obj.node.qname()

# Filter class based on depth setting
if not self.should_show_node(obj.fig_id, is_class=True):
continue

Check warning on line 157 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L157

Added line #L157 was not covered by tests

if self.config.no_standalone and not any(
obj in (rel.from_object, rel.to_object)
for rel_type in ("specialization", "association", "aggregation")
Expand All @@ -129,6 +170,12 @@
)
# inheritance links
for rel in diagram.get_relationships("specialization"):
# Filter nodes based on depth setting
if not self.should_show_node(
rel.from_object.fig_id, is_class=True
) or not self.should_show_node(rel.to_object.fig_id, is_class=True):
continue

Check warning on line 177 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L177

Added line #L177 was not covered by tests

self.printer.emit_edge(
rel.from_object.fig_id,
rel.to_object.fig_id,
Expand All @@ -137,6 +184,12 @@
associations: dict[str, set[str]] = defaultdict(set)
# generate associations
for rel in diagram.get_relationships("association"):
# Filter nodes based on depth setting
if not self.should_show_node(
rel.from_object.fig_id, is_class=True
) or not self.should_show_node(rel.to_object.fig_id, is_class=True):
continue

Check warning on line 191 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L191

Added line #L191 was not covered by tests

associations[rel.from_object.fig_id].add(rel.to_object.fig_id)
self.printer.emit_edge(
rel.from_object.fig_id,
Expand All @@ -146,6 +199,12 @@
)
# generate aggregations
for rel in diagram.get_relationships("aggregation"):
# Filter nodes based on depth setting
if not self.should_show_node(
rel.from_object.fig_id, is_class=True
) or not self.should_show_node(rel.to_object.fig_id, is_class=True):
continue

Check warning on line 206 in pylint/pyreverse/writer.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/writer.py#L206

Added line #L206 was not covered by tests

if rel.to_object.fig_id in associations[rel.from_object.fig_id]:
continue
self.printer.emit_edge(
Expand Down
5 changes: 5 additions & 0 deletions pylint/testutils/pyreverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PyreverseConfig(
The default values correspond to the defaults of the options' parser.
"""

# pylint: disable=too-many-locals
def __init__(
self,
*,
Expand All @@ -40,6 +41,7 @@ def __init__(
output_format: str = "dot",
colorized: bool = False,
max_color_depth: int = 2,
max_depth: int | None = None,
color_palette: tuple[str, ...] = DEFAULT_COLOR_PALETTE,
ignore_list: tuple[str, ...] = tuple(),
project: str = "",
Expand All @@ -62,12 +64,15 @@ def __init__(
self.only_classnames = only_classnames
self.output_format = output_format
self.colorized = colorized
self.max_depth = max_depth
self.max_color_depth = max_color_depth
self.color_palette = color_palette
self.ignore_list = ignore_list
self.project = project
self.output_directory = output_directory

# pylint: enable=too-many-locals


class TestFileOptions(TypedDict):
source_roots: list[str]
Expand Down
Loading