src/eric7/PipInterface/pipdeptree.py

branch
eric7
changeset 9849
99782ca569ed
parent 9653
e67609152c5e
child 10026
617290a049f0
--- a/src/eric7/PipInterface/pipdeptree.py	Sat Mar 04 18:09:08 2023 +0100
+++ b/src/eric7/PipInterface/pipdeptree.py	Sun Mar 05 12:26:12 2023 +0100
@@ -46,6 +46,7 @@
 from collections.abc import Mapping
 from importlib import import_module
 from itertools import chain
+from textwrap import dedent
 
 from pip._vendor import pkg_resources
 
@@ -55,7 +56,7 @@
     from pip import FrozenRequirement
 
 
-__version__ = '2.3.3'  # eric-ide modification: from version.py
+__version__ = '2.5.2'  # eric-ide modification: from version.py
 
 
 flatten = chain.from_iterable
@@ -590,6 +591,91 @@
     return json.dumps([aux(p) for p in nodes], indent=indent)
 
 
+def render_mermaid(tree) -> str:
+    """Produce a Mermaid flowchart from the dependency graph.
+
+    :param dict tree: dependency graph
+    """
+    # List of reserved keywords in Mermaid that cannot be used as node names.
+    # See: https://github.com/mermaid-js/mermaid/issues/4182#issuecomment-1454787806
+    reserved_ids: set[str] = {
+        "C4Component",
+        "C4Container",
+        "C4Deployment",
+        "C4Dynamic",
+        "_blank",
+        "_parent",
+        "_self",
+        "_top",
+        "call",
+        "class",
+        "classDef",
+        "click",
+        "end",
+        "flowchart",
+        "flowchart-v2",
+        "graph",
+        "interpolate",
+        "linkStyle",
+        "style",
+        "subgraph",
+    }
+    node_ids_map: dict[str:str] = {}
+
+    def mermaid_id(key: str) -> str:
+        """Returns a valid Mermaid node ID from a string."""
+        # If we have already seen this key, return the canonical ID.
+        canonical_id = node_ids_map.get(key)
+        if canonical_id is not None:
+            return canonical_id
+        # If the key is not a reserved keyword, return it as is, and update the map.
+        if key not in reserved_ids:
+            node_ids_map[key] = key
+            return key
+        # If the key is a reserved keyword, append a number to it.
+        number = 0
+        while True:
+            new_id = f"{key}_{number}"
+            if new_id not in node_ids_map:
+                node_ids_map[key] = new_id
+                return new_id
+            number += 1
+
+    # Use a sets to avoid duplicate entries.
+    nodes: set[str] = set()
+    edges: set[str] = set()
+
+    for pkg, deps in tree.items():
+        pkg_label = f"{pkg.project_name}\\n{pkg.version}"
+        pkg_key = mermaid_id(pkg.key)
+        nodes.add(f'{pkg_key}["{pkg_label}"]')
+        for dep in deps:
+            edge_label = dep.version_spec or "any"
+            dep_key = mermaid_id(dep.key)
+            if dep.is_missing:
+                dep_label = f"{dep.project_name}\\n(missing)"
+                nodes.add(f'{dep_key}["{dep_label}"]:::missing')
+                edges.add(f"{pkg_key} -.-> {dep_key}")
+            else:
+                edges.add(f'{pkg_key} -- "{edge_label}" --> {dep_key}')
+
+    # Produce the Mermaid Markdown.
+    indent = " " * 4
+    output = dedent(
+        f"""\
+        flowchart TD
+        {indent}classDef missing stroke-dasharray: 5
+        """
+    )
+    # Sort the nodes and edges to make the output deterministic.
+    output += indent
+    output += f"\n{indent}".join(node for node in sorted(nodes))
+    output += "\n" + indent
+    output += f"\n{indent}".join(edge for edge in sorted(edges))
+    output += "\n"
+    return output
+
+
 def dump_graphviz(tree, output_format="dot", is_reverse=False):
     """Output dependency graph as one of the supported GraphViz output formats.
 
@@ -652,7 +738,11 @@
 
     # Allow output of dot format, even if GraphViz isn't installed.
     if output_format == "dot":
-        return graph.source
+        # Emulates graphviz.dot.Dot.__iter__() to force the sorting of graph.body.
+        # Fixes https://github.com/tox-dev/pipdeptree/issues/188
+        # That way we can guarantee the output of the dot format is deterministic
+        # and stable.
+        return "".join([tuple(graph)[0]] + sorted(graph.body) + [graph._tail])
 
     # As it's unknown if the selected output format is binary or not, try to
     # decode it as UTF8 and only print it out in binary if that's not possible.
@@ -812,6 +902,12 @@
         ),
     )
     parser.add_argument(
+        "--mermaid",
+        action="store_true",
+        default=False,
+        help=("Display dependency tree as a Maermaid graph. " "This option overrides all other options."),
+    )
+    parser.add_argument(
         "--graph-output",
         dest="output_format",
         help=(
@@ -920,6 +1016,8 @@
         print(render_json(tree, indent=4))
     elif args.json_tree:
         print(render_json_tree(tree, indent=4))
+    elif args.mermaid:
+        print(render_mermaid(tree))
     elif args.output_format:
         output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse)
         print_graphviz(output)

eric ide

mercurial