Skip to content

Commit a0c9c32

Browse files
Implements Mermaid output (#195)
* Implements mermaid output. Closes #129. * Fix linting issues. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 96a2341 commit a0c9c32

File tree

2 files changed

+91
-3
lines changed

2 files changed

+91
-3
lines changed

src/pipdeptree/__init__.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections.abc import Mapping
1111
from importlib import import_module
1212
from itertools import chain
13+
from textwrap import dedent
1314

1415
from pip._vendor import pkg_resources
1516

@@ -550,6 +551,44 @@ def aux(node, parent=None, cur_chain=None):
550551
return json.dumps([aux(p) for p in nodes], indent=indent)
551552

552553

554+
def render_mermaid(tree) -> str:
555+
"""Produce a Mermaid flowchart from the dependency graph.
556+
557+
:param dict tree: dependency graph
558+
"""
559+
# Use a sets to avoid duplicate entries.
560+
nodes: set[str] = set()
561+
edges: set[str] = set()
562+
563+
for pkg, deps in tree.items():
564+
pkg_label = f"{pkg.project_name}\\n{pkg.version}"
565+
nodes.add(f"{pkg.key}[{pkg_label}]")
566+
for dep in deps:
567+
edge_label = dep.version_spec or "any"
568+
if dep.is_missing:
569+
dep_label = f"{dep.project_name}\\n(missing)"
570+
nodes.add(f"{dep.key}[{dep_label}]:::missing")
571+
edges.add(f"{pkg.key} -.-> {dep.key}")
572+
else:
573+
edges.add(f"{pkg.key} -- {edge_label} --> {dep.key}")
574+
575+
# Produce the Mermaid Markdown.
576+
indent = " " * 4
577+
output = dedent(
578+
f"""\
579+
flowchart TD
580+
{indent}classDef missing stroke-dasharray: 5
581+
"""
582+
)
583+
# Sort the nodes and edges to make the output deterministic.
584+
output += indent
585+
output += f"\n{indent}".join(node for node in sorted(nodes))
586+
output += "\n" + indent
587+
output += f"\n{indent}".join(edge for edge in sorted(edges))
588+
output += "\n"
589+
return output
590+
591+
553592
def dump_graphviz(tree, output_format="dot", is_reverse=False):
554593
"""Output dependency graph as one of the supported GraphViz output formats.
555594
@@ -775,6 +814,12 @@ def get_parser():
775814
"This option overrides all other options (except --json)."
776815
),
777816
)
817+
parser.add_argument(
818+
"--mermaid",
819+
action="store_true",
820+
default=False,
821+
help=("Display dependency tree as a Maermaid graph. " "This option overrides all other options."),
822+
)
778823
parser.add_argument(
779824
"--graph-output",
780825
dest="output_format",
@@ -884,6 +929,8 @@ def main():
884929
print(render_json(tree, indent=4))
885930
elif args.json_tree:
886931
print(render_json_tree(tree, indent=4))
932+
elif args.output_format:
933+
print(render_mermaid(tree))
887934
elif args.output_format:
888935
output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse)
889936
print_graphviz(output)

tests/test_pipdeptree.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,9 @@ def test_render_text(capsys, list_all, reverse, expected_output):
306306
# Tests for graph outputs
307307

308308

309-
def test_render_dot(capsys):
310-
# Extract the dependency graph from the package tree fixture and randomize it.
309+
def randomized_dag_copy(t):
310+
"""Returns a copy of the package tree fixture with dependencies in randomized order."""
311+
# Extract the dependency graph from the package tree and randomize it.
311312
randomized_graph = {}
312313
randomized_nodes = list(t._obj.keys())
313314
random.shuffle(randomized_nodes)
@@ -320,10 +321,42 @@ def test_render_dot(capsys):
320321
# Create a randomized package tree.
321322
randomized_dag = p.PackageDAG(randomized_graph)
322323
assert len(t) == len(randomized_dag)
324+
return randomized_dag
325+
326+
327+
def test_render_mermaid():
328+
# Check both the sorted and randomized package tree produces the same sorted
329+
# Mermaid output.
330+
for package_tree in (t, randomized_dag_copy(t)):
331+
output = p.render_mermaid(package_tree)
332+
assert output == dedent(
333+
"""\
334+
flowchart TD
335+
classDef missing stroke-dasharray: 5
336+
a[a\\n3.4.0]
337+
b[b\\n2.3.1]
338+
c[c\\n5.10.0]
339+
d[d\\n2.35]
340+
e[e\\n0.12.1]
341+
f[f\\n3.1]
342+
g[g\\n6.8.3rc1]
343+
a -- >=2.0.0 --> b
344+
a -- >=5.7.1 --> c
345+
b -- >=2.30,<2.42 --> d
346+
c -- >=0.12.1 --> e
347+
c -- >=2.30 --> d
348+
d -- >=0.9.0 --> e
349+
f -- >=2.1.0 --> b
350+
g -- >=0.9.0 --> e
351+
g -- >=3.0.0 --> f
352+
"""
353+
)
323354

355+
356+
def test_render_dot(capsys):
324357
# Check both the sorted and randomized package tree produces the same sorted
325358
# graphviz output.
326-
for package_tree in (t, randomized_dag):
359+
for package_tree in (t, randomized_dag_copy(t)):
327360
output = p.dump_graphviz(package_tree, output_format="dot")
328361
p.print_graphviz(output)
329362
out, _ = capsys.readouterr()
@@ -499,6 +532,14 @@ def test_parser_json_tree():
499532
assert args.output_format is None
500533

501534

535+
def test_parser_mermaid():
536+
parser = p.get_parser()
537+
args = parser.parse_args(["--mermaid"])
538+
assert args.mermaid
539+
assert not args.json
540+
assert args.output_format is None
541+
542+
502543
def test_parser_pdf():
503544
parser = p.get_parser()
504545
args = parser.parse_args(["--graph-output", "pdf"])

0 commit comments

Comments
 (0)