44 import tempfile |
44 import tempfile |
45 from collections import defaultdict, deque |
45 from collections import defaultdict, deque |
46 from collections.abc import Mapping |
46 from collections.abc import Mapping |
47 from importlib import import_module |
47 from importlib import import_module |
48 from itertools import chain |
48 from itertools import chain |
|
49 from textwrap import dedent |
49 |
50 |
50 from pip._vendor import pkg_resources |
51 from pip._vendor import pkg_resources |
51 |
52 |
52 try: |
53 try: |
53 from pip._internal.operations.freeze import FrozenRequirement |
54 from pip._internal.operations.freeze import FrozenRequirement |
54 except ImportError: |
55 except ImportError: |
55 from pip import FrozenRequirement |
56 from pip import FrozenRequirement |
56 |
57 |
57 |
58 |
58 __version__ = '2.3.3' # eric-ide modification: from version.py |
59 __version__ = '2.5.2' # eric-ide modification: from version.py |
59 |
60 |
60 |
61 |
61 flatten = chain.from_iterable |
62 flatten = chain.from_iterable |
62 |
63 |
63 |
64 |
588 return d |
589 return d |
589 |
590 |
590 return json.dumps([aux(p) for p in nodes], indent=indent) |
591 return json.dumps([aux(p) for p in nodes], indent=indent) |
591 |
592 |
592 |
593 |
|
594 def render_mermaid(tree) -> str: |
|
595 """Produce a Mermaid flowchart from the dependency graph. |
|
596 |
|
597 :param dict tree: dependency graph |
|
598 """ |
|
599 # List of reserved keywords in Mermaid that cannot be used as node names. |
|
600 # See: https://github.com/mermaid-js/mermaid/issues/4182#issuecomment-1454787806 |
|
601 reserved_ids: set[str] = { |
|
602 "C4Component", |
|
603 "C4Container", |
|
604 "C4Deployment", |
|
605 "C4Dynamic", |
|
606 "_blank", |
|
607 "_parent", |
|
608 "_self", |
|
609 "_top", |
|
610 "call", |
|
611 "class", |
|
612 "classDef", |
|
613 "click", |
|
614 "end", |
|
615 "flowchart", |
|
616 "flowchart-v2", |
|
617 "graph", |
|
618 "interpolate", |
|
619 "linkStyle", |
|
620 "style", |
|
621 "subgraph", |
|
622 } |
|
623 node_ids_map: dict[str:str] = {} |
|
624 |
|
625 def mermaid_id(key: str) -> str: |
|
626 """Returns a valid Mermaid node ID from a string.""" |
|
627 # If we have already seen this key, return the canonical ID. |
|
628 canonical_id = node_ids_map.get(key) |
|
629 if canonical_id is not None: |
|
630 return canonical_id |
|
631 # If the key is not a reserved keyword, return it as is, and update the map. |
|
632 if key not in reserved_ids: |
|
633 node_ids_map[key] = key |
|
634 return key |
|
635 # If the key is a reserved keyword, append a number to it. |
|
636 number = 0 |
|
637 while True: |
|
638 new_id = f"{key}_{number}" |
|
639 if new_id not in node_ids_map: |
|
640 node_ids_map[key] = new_id |
|
641 return new_id |
|
642 number += 1 |
|
643 |
|
644 # Use a sets to avoid duplicate entries. |
|
645 nodes: set[str] = set() |
|
646 edges: set[str] = set() |
|
647 |
|
648 for pkg, deps in tree.items(): |
|
649 pkg_label = f"{pkg.project_name}\\n{pkg.version}" |
|
650 pkg_key = mermaid_id(pkg.key) |
|
651 nodes.add(f'{pkg_key}["{pkg_label}"]') |
|
652 for dep in deps: |
|
653 edge_label = dep.version_spec or "any" |
|
654 dep_key = mermaid_id(dep.key) |
|
655 if dep.is_missing: |
|
656 dep_label = f"{dep.project_name}\\n(missing)" |
|
657 nodes.add(f'{dep_key}["{dep_label}"]:::missing') |
|
658 edges.add(f"{pkg_key} -.-> {dep_key}") |
|
659 else: |
|
660 edges.add(f'{pkg_key} -- "{edge_label}" --> {dep_key}') |
|
661 |
|
662 # Produce the Mermaid Markdown. |
|
663 indent = " " * 4 |
|
664 output = dedent( |
|
665 f"""\ |
|
666 flowchart TD |
|
667 {indent}classDef missing stroke-dasharray: 5 |
|
668 """ |
|
669 ) |
|
670 # Sort the nodes and edges to make the output deterministic. |
|
671 output += indent |
|
672 output += f"\n{indent}".join(node for node in sorted(nodes)) |
|
673 output += "\n" + indent |
|
674 output += f"\n{indent}".join(edge for edge in sorted(edges)) |
|
675 output += "\n" |
|
676 return output |
|
677 |
|
678 |
593 def dump_graphviz(tree, output_format="dot", is_reverse=False): |
679 def dump_graphviz(tree, output_format="dot", is_reverse=False): |
594 """Output dependency graph as one of the supported GraphViz output formats. |
680 """Output dependency graph as one of the supported GraphViz output formats. |
595 |
681 |
596 :param dict tree: dependency graph |
682 :param dict tree: dependency graph |
597 :param string output_format: output format |
683 :param string output_format: output format |
650 edge_label = req_ref.version_spec or "any" |
736 edge_label = req_ref.version_spec or "any" |
651 graph.edge(dep.key, parent.key, label=edge_label) |
737 graph.edge(dep.key, parent.key, label=edge_label) |
652 |
738 |
653 # Allow output of dot format, even if GraphViz isn't installed. |
739 # Allow output of dot format, even if GraphViz isn't installed. |
654 if output_format == "dot": |
740 if output_format == "dot": |
655 return graph.source |
741 # Emulates graphviz.dot.Dot.__iter__() to force the sorting of graph.body. |
|
742 # Fixes https://github.com/tox-dev/pipdeptree/issues/188 |
|
743 # That way we can guarantee the output of the dot format is deterministic |
|
744 # and stable. |
|
745 return "".join([tuple(graph)[0]] + sorted(graph.body) + [graph._tail]) |
656 |
746 |
657 # As it's unknown if the selected output format is binary or not, try to |
747 # As it's unknown if the selected output format is binary or not, try to |
658 # decode it as UTF8 and only print it out in binary if that's not possible. |
748 # decode it as UTF8 and only print it out in binary if that's not possible. |
659 try: |
749 try: |
660 return graph.pipe().decode("utf-8") |
750 return graph.pipe().decode("utf-8") |
810 "the same way as the plain text output printed by default. " |
900 "the same way as the plain text output printed by default. " |
811 "This option overrides all other options (except --json)." |
901 "This option overrides all other options (except --json)." |
812 ), |
902 ), |
813 ) |
903 ) |
814 parser.add_argument( |
904 parser.add_argument( |
|
905 "--mermaid", |
|
906 action="store_true", |
|
907 default=False, |
|
908 help=("Display dependency tree as a Maermaid graph. " "This option overrides all other options."), |
|
909 ) |
|
910 parser.add_argument( |
815 "--graph-output", |
911 "--graph-output", |
816 dest="output_format", |
912 dest="output_format", |
817 help=( |
913 help=( |
818 "Print a dependency graph in the specified output " |
914 "Print a dependency graph in the specified output " |
819 "format. Available are all formats supported by " |
915 "format. Available are all formats supported by " |
918 |
1014 |
919 if args.json: |
1015 if args.json: |
920 print(render_json(tree, indent=4)) |
1016 print(render_json(tree, indent=4)) |
921 elif args.json_tree: |
1017 elif args.json_tree: |
922 print(render_json_tree(tree, indent=4)) |
1018 print(render_json_tree(tree, indent=4)) |
|
1019 elif args.mermaid: |
|
1020 print(render_mermaid(tree)) |
923 elif args.output_format: |
1021 elif args.output_format: |
924 output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse) |
1022 output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse) |
925 print_graphviz(output) |
1023 print_graphviz(output) |
926 else: |
1024 else: |
927 render_text(tree, args.all, args.freeze) |
1025 render_text(tree, args.all, args.freeze) |