--- 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)