github.com/jfrog/build-info-go@v1.9.26/utils/pythonutils/pipdeptree/pipdeptree.py (about) 1 import argparse 2 import inspect 3 import json 4 import os 5 import shutil 6 import subprocess 7 import sys 8 import tempfile 9 from collections import defaultdict, deque 10 from collections.abc import Mapping 11 from importlib import import_module 12 from itertools import chain 13 14 from pip._vendor import pkg_resources 15 16 __version__ = '2.2.3' 17 18 try: 19 from pip._internal.operations.freeze import FrozenRequirement 20 except ImportError: 21 from pip import FrozenRequirement 22 23 24 def sorted_tree(tree): 25 """ 26 Sorts the dict representation of the tree. The root packages as well as the intermediate packages are sorted in the 27 alphabetical order of the package names. 28 29 :param dict tree: the pkg dependency tree obtained by calling `construct_tree` function 30 :returns: sorted tree 31 :rtype: dict 32 """ 33 return {k: sorted(v) for k, v in sorted(tree.items())} 34 35 36 def guess_version(pkg_key, default="?"): 37 """Guess the version of a pkg when pip doesn't provide it 38 39 :param str pkg_key: key of the package 40 :param str default: default version to return if unable to find 41 :returns: version 42 :rtype: string 43 """ 44 try: 45 if sys.version_info >= (3, 8): # pragma: >=3.8 cover 46 import importlib.metadata as importlib_metadata 47 else: # pragma: <3.8 cover 48 import importlib_metadata 49 return importlib_metadata.version(pkg_key) 50 except ImportError: 51 pass 52 # Avoid AssertionError with setuptools, see https://github.com/tox-dev/pipdeptree/issues/162 53 if pkg_key in {"setuptools"}: 54 return default 55 try: 56 m = import_module(pkg_key) 57 except ImportError: 58 return default 59 else: 60 v = getattr(m, "__version__", default) 61 if inspect.ismodule(v): 62 return getattr(v, "__version__", default) 63 else: 64 return v 65 66 67 def frozen_req_from_dist(dist): 68 # The `pip._internal.metadata` modules were introduced in 21.1.1 69 # and the `pip._internal.operations.freeze.FrozenRequirement` 70 # class now expects dist to be a subclass of 71 # `pip._internal.metadata.BaseDistribution`, however the 72 # `pip._internal.utils.misc.get_installed_distributions` continues 73 # to return objects of type 74 # pip._vendor.pkg_resources.DistInfoDistribution. 75 # 76 # This is a hacky backward compatible (with older versions of pip) 77 # fix. 78 try: 79 from pip._internal import metadata 80 except ImportError: 81 pass 82 else: 83 dist = metadata.pkg_resources.Distribution(dist) 84 85 try: 86 return FrozenRequirement.from_dist(dist) 87 except TypeError: 88 return FrozenRequirement.from_dist(dist, []) 89 90 91 class Package: 92 """ 93 Abstract class for wrappers around objects that pip returns. This class needs to be subclassed with implementations 94 for `render_as_root` and `render_as_branch` methods. 95 """ 96 97 def __init__(self, obj): 98 self._obj = obj 99 self.project_name = obj.project_name 100 self.key = obj.key 101 102 def render_as_root(self, frozen): # noqa: U100 103 return NotImplementedError 104 105 def render_as_branch(self, frozen): # noqa: U100 106 return NotImplementedError 107 108 def render(self, parent=None, frozen=False): 109 if not parent: 110 return self.render_as_root(frozen) 111 else: 112 return self.render_as_branch(frozen) 113 114 @staticmethod 115 def frozen_repr(obj): 116 fr = frozen_req_from_dist(obj) 117 return str(fr).strip() 118 119 def __getattr__(self, key): 120 return getattr(self._obj, key) 121 122 def __repr__(self): 123 return f'<{self.__class__.__name__}("{self.key}")>' 124 125 def __lt__(self, rhs): 126 return self.key < rhs.key 127 128 129 class DistPackage(Package): 130 """ 131 Wrapper class for pkg_resources.Distribution instances 132 133 :param obj: pkg_resources.Distribution to wrap over 134 :param req: optional ReqPackage object to associate this DistPackage with. This is useful for displaying the tree 135 in reverse 136 """ 137 138 def __init__(self, obj, req=None): 139 super().__init__(obj) 140 self.version_spec = None 141 self.req = req 142 143 def render_as_root(self, frozen): 144 if not frozen: 145 return f"{self.project_name}=={self.version}" 146 else: 147 return self.__class__.frozen_repr(self._obj) 148 149 def render_as_branch(self, frozen): 150 assert self.req is not None 151 if not frozen: 152 parent_ver_spec = self.req.version_spec 153 parent_str = self.req.project_name 154 if parent_ver_spec: 155 parent_str += parent_ver_spec 156 return f"{self.project_name}=={self.version} [requires: {parent_str}]" 157 else: 158 return self.render_as_root(frozen) 159 160 def as_requirement(self): 161 """Return a ReqPackage representation of this DistPackage""" 162 return ReqPackage(self._obj.as_requirement(), dist=self) 163 164 def as_parent_of(self, req): 165 """ 166 Return a DistPackage instance associated to a requirement. This association is necessary for reversing the 167 PackageDAG. 168 169 If `req` is None, and the `req` attribute of the current instance is also None, then the same instance will be 170 returned. 171 172 :param ReqPackage req: the requirement to associate with 173 :returns: DistPackage instance 174 """ 175 if req is None and self.req is None: 176 return self 177 return self.__class__(self._obj, req) 178 179 def as_dict(self): 180 return {"key": self.key, "package_name": self.project_name, "installed_version": self.version} 181 182 183 class ReqPackage(Package): 184 """ 185 Wrapper class for Requirements instance 186 187 :param obj: The `Requirements` instance to wrap over 188 :param dist: optional `pkg_resources.Distribution` instance for this requirement 189 """ 190 191 UNKNOWN_VERSION = "?" 192 193 def __init__(self, obj, dist=None): 194 super().__init__(obj) 195 self.dist = dist 196 197 @property 198 def version_spec(self): 199 specs = sorted(self._obj.specs, reverse=True) # `reverse` makes '>' prior to '<' 200 return ",".join(["".join(sp) for sp in specs]) if specs else None 201 202 @property 203 def installed_version(self): 204 if not self.dist: 205 return guess_version(self.key, self.UNKNOWN_VERSION) 206 return self.dist.version 207 208 @property 209 def is_missing(self): 210 return self.installed_version == self.UNKNOWN_VERSION 211 212 def is_conflicting(self): 213 """If installed version conflicts with required version""" 214 # unknown installed version is also considered conflicting 215 if self.installed_version == self.UNKNOWN_VERSION: 216 return True 217 ver_spec = self.version_spec if self.version_spec else "" 218 req_version_str = f"{self.project_name}{ver_spec}" 219 req_obj = pkg_resources.Requirement.parse(req_version_str) 220 return self.installed_version not in req_obj 221 222 def render_as_root(self, frozen): 223 if not frozen: 224 return f"{self.project_name}=={self.installed_version}" 225 elif self.dist: 226 return self.__class__.frozen_repr(self.dist._obj) 227 else: 228 return self.project_name 229 230 def render_as_branch(self, frozen): 231 if not frozen: 232 req_ver = self.version_spec if self.version_spec else "Any" 233 return f"{self.project_name} [required: {req_ver}, installed: {self.installed_version}]" 234 else: 235 return self.render_as_root(frozen) 236 237 def as_dict(self): 238 return { 239 "key": self.key, 240 "package_name": self.project_name, 241 "installed_version": self.installed_version, 242 "required_version": self.version_spec, 243 } 244 245 246 class PackageDAG(Mapping): 247 """ 248 Representation of Package dependencies as directed acyclic graph using a dict (Mapping) as the underlying 249 datastructure. 250 251 The nodes and their relationships (edges) are internally stored using a map as follows, 252 253 {a: [b, c], 254 b: [d], 255 c: [d, e], 256 d: [e], 257 e: [], 258 f: [b], 259 g: [e, f]} 260 261 Here, node `a` has 2 children nodes `b` and `c`. Consider edge direction from `a` -> `b` and `a` -> `c` 262 respectively. 263 264 A node is expected to be an instance of a subclass of `Package`. The keys are must be of class `DistPackage` and 265 each item in values must be of class `ReqPackage`. (See also ReversedPackageDAG where the key and value types are 266 interchanged). 267 """ 268 269 @classmethod 270 def from_pkgs(cls, pkgs): 271 pkgs = [DistPackage(p) for p in pkgs] 272 idx = {p.key: p for p in pkgs} 273 m = {p: [ReqPackage(r, idx.get(r.key)) for r in p.requires()] for p in pkgs} 274 return cls(m) 275 276 def __init__(self, m): 277 """Initialize the PackageDAG object 278 279 :param dict m: dict of node objects (refer class docstring) 280 :returns: None 281 :rtype: NoneType 282 283 """ 284 self._obj = m 285 self._index = {p.key: p for p in list(self._obj)} 286 287 def get_node_as_parent(self, node_key): 288 """ 289 Get the node from the keys of the dict representing the DAG. 290 291 This method is useful if the dict representing the DAG contains different kind of objects in keys and values. 292 Use this method to look up a node obj as a parent (from the keys of the dict) given a node key. 293 294 :param node_key: identifier corresponding to key attr of node obj 295 :returns: node obj (as present in the keys of the dict) 296 :rtype: Object 297 """ 298 try: 299 return self._index[node_key] 300 except KeyError: 301 return None 302 303 def get_children(self, node_key): 304 """ 305 Get child nodes for a node by its key 306 307 :param str node_key: key of the node to get children of 308 :returns: list of child nodes 309 :rtype: ReqPackage[] 310 """ 311 node = self.get_node_as_parent(node_key) 312 return self._obj[node] if node else [] 313 314 def filter(self, include, exclude): 315 """ 316 Filters nodes in a graph by given parameters 317 318 If a node is included, then all it's children are also included. 319 320 :param set include: set of node keys to include (or None) 321 :param set exclude: set of node keys to exclude (or None) 322 :returns: filtered version of the graph 323 :rtype: PackageDAG 324 """ 325 # If neither of the filters are specified, short circuit 326 if include is None and exclude is None: 327 return self 328 329 # Note: In following comparisons, we use lower cased values so 330 # that user may specify `key` or `project_name`. As per the 331 # documentation, `key` is simply 332 # `project_name.lower()`. Refer: 333 # https://setuptools.readthedocs.io/en/latest/pkg_resources.html#distribution-objects 334 if include: 335 include = {s.lower() for s in include} 336 if exclude: 337 exclude = {s.lower() for s in exclude} 338 else: 339 exclude = set() 340 341 # Check for mutual exclusion of show_only and exclude sets 342 # after normalizing the values to lowercase 343 if include and exclude: 344 assert not (include & exclude) 345 346 # Traverse the graph in a depth first manner and filter the 347 # nodes according to `show_only` and `exclude` sets 348 stack = deque() 349 m = {} 350 seen = set() 351 for node in self._obj.keys(): 352 if node.key in exclude: 353 continue 354 if include is None or node.key in include: 355 stack.append(node) 356 while True: 357 if len(stack) > 0: 358 n = stack.pop() 359 cldn = [c for c in self._obj[n] if c.key not in exclude] 360 m[n] = cldn 361 seen.add(n.key) 362 for c in cldn: 363 if c.key not in seen: 364 cld_node = self.get_node_as_parent(c.key) 365 if cld_node: 366 stack.append(cld_node) 367 else: 368 # It means there's no root node corresponding to the child node i.e. 369 # a dependency is missing 370 continue 371 else: 372 break 373 374 return self.__class__(m) 375 376 def reverse(self): 377 """ 378 Reverse the DAG, or turn it upside-down. 379 380 In other words, the directions of edges of the nodes in the DAG will be reversed. 381 382 Note that this function purely works on the nodes in the graph. This implies that to perform a combination of 383 filtering and reversing, the order in which `filter` and `reverse` methods should be applied is important. For 384 e.g., if reverse is called on a filtered graph, then only the filtered nodes and it's children will be 385 considered when reversing. On the other hand, if filter is called on reversed DAG, then the definition of 386 "child" nodes is as per the reversed DAG. 387 388 :returns: DAG in the reversed form 389 :rtype: ReversedPackageDAG 390 """ 391 m = defaultdict(list) 392 child_keys = {r.key for r in chain.from_iterable(self._obj.values())} 393 for k, vs in self._obj.items(): 394 for v in vs: 395 # if v is already added to the dict, then ensure that 396 # we are using the same object. This check is required 397 # as we're using array mutation 398 try: 399 node = [p for p in m.keys() if p.key == v.key][0] 400 except IndexError: 401 node = v 402 m[node].append(k.as_parent_of(v)) 403 if k.key not in child_keys: 404 m[k.as_requirement()] = [] 405 return ReversedPackageDAG(dict(m)) 406 407 def sort(self): 408 """ 409 Return sorted tree in which the underlying _obj dict is an dict, sorted alphabetically by the keys. 410 411 :returns: Instance of same class with dict 412 """ 413 return self.__class__(sorted_tree(self._obj)) 414 415 # Methods required by the abstract base class Mapping 416 def __getitem__(self, *args): 417 return self._obj.get(*args) 418 419 def __iter__(self): 420 return self._obj.__iter__() 421 422 def __len__(self): 423 return len(self._obj) 424 425 426 class ReversedPackageDAG(PackageDAG): 427 """Representation of Package dependencies in the reverse order. 428 429 Similar to it's super class `PackageDAG`, the underlying datastructure is a dict, but here the keys are expected to 430 be of type `ReqPackage` and each item in the values of type `DistPackage`. 431 432 Typically, this object will be obtained by calling `PackageDAG.reverse`. 433 """ 434 435 def reverse(self): 436 """ 437 Reverse the already reversed DAG to get the PackageDAG again 438 439 :returns: reverse of the reversed DAG 440 :rtype: PackageDAG 441 """ 442 m = defaultdict(list) 443 child_keys = {r.key for r in chain.from_iterable(self._obj.values())} 444 for k, vs in self._obj.items(): 445 for v in vs: 446 try: 447 node = [p for p in m.keys() if p.key == v.key][0] 448 except IndexError: 449 node = v.as_parent_of(None) 450 m[node].append(k) 451 if k.key not in child_keys: 452 m[k.dist] = [] 453 return PackageDAG(dict(m)) 454 455 456 def render_text(tree, list_all=True, frozen=False): 457 """Print tree as text on console 458 459 :param dict tree: the package tree 460 :param bool list_all: whether to list all the pgks at the root level or only those that are the sub-dependencies 461 :param bool frozen: show the names of the pkgs in the output that's favourable to pip --freeze 462 :returns: None 463 464 """ 465 tree = tree.sort() 466 nodes = tree.keys() 467 branch_keys = {r.key for r in chain.from_iterable(tree.values())} 468 use_bullets = not frozen 469 470 if not list_all: 471 nodes = [p for p in nodes if p.key not in branch_keys] 472 473 def aux(node, parent=None, indent=0, cur_chain=None): 474 cur_chain = cur_chain or [] 475 node_str = node.render(parent, frozen) 476 if parent: 477 prefix = " " * indent + ("- " if use_bullets else "") 478 node_str = prefix + node_str 479 result = [node_str] 480 children = [ 481 aux(c, node, indent=indent + 2, cur_chain=cur_chain + [c.project_name]) 482 for c in tree.get_children(node.key) 483 if c.project_name not in cur_chain 484 ] 485 result += list(chain.from_iterable(children)) 486 return result 487 488 lines = chain.from_iterable([aux(p) for p in nodes]) 489 print("\n".join(lines)) 490 491 492 def render_json(tree, indent): 493 """ 494 Converts the tree into a flat json representation. 495 496 The json repr will be a list of hashes, each hash having 2 fields: 497 - package 498 - dependencies: list of dependencies 499 500 :param dict tree: dependency tree 501 :param int indent: no. of spaces to indent json 502 :returns: json representation of the tree 503 :rtype: str 504 """ 505 tree = tree.sort() 506 return json.dumps( 507 [{"package": k.as_dict(), "dependencies": [v.as_dict() for v in vs]} for k, vs in tree.items()], indent=indent 508 ) 509 510 511 def render_json_tree(tree, indent): 512 """ 513 Converts the tree into a nested json representation. 514 515 The json repr will be a list of hashes, each hash having the following fields: 516 517 - package_name 518 - key 519 - required_version 520 - installed_version 521 - dependencies: list of dependencies 522 523 :param dict tree: dependency tree 524 :param int indent: no. of spaces to indent json 525 :returns: json representation of the tree 526 :rtype: str 527 """ 528 tree = tree.sort() 529 branch_keys = {r.key for r in chain.from_iterable(tree.values())} 530 nodes = [p for p in tree.keys() if p.key not in branch_keys] 531 532 def aux(node, parent=None, cur_chain=None): 533 if cur_chain is None: 534 cur_chain = [node.project_name] 535 536 d = node.as_dict() 537 if parent: 538 d["required_version"] = node.version_spec if node.version_spec else "Any" 539 else: 540 d["required_version"] = d["installed_version"] 541 542 d["dependencies"] = [ 543 aux(c, parent=node, cur_chain=cur_chain + [c.project_name]) 544 for c in tree.get_children(node.key) 545 if c.project_name not in cur_chain 546 ] 547 548 return d 549 550 return json.dumps([aux(p) for p in nodes], indent=indent) 551 552 553 def dump_graphviz(tree, output_format="dot", is_reverse=False): 554 """Output dependency graph as one of the supported GraphViz output formats. 555 556 :param dict tree: dependency graph 557 :param string output_format: output format 558 :param bool is_reverse: reverse or not 559 :returns: representation of tree in the specified output format 560 :rtype: str or binary representation depending on the output format 561 562 """ 563 try: 564 from graphviz import Digraph 565 except ImportError: 566 print("graphviz is not available, but necessary for the output " "option. Please install it.", file=sys.stderr) 567 sys.exit(1) 568 569 try: 570 from graphviz import parameters 571 except ImportError: 572 from graphviz import backend 573 574 valid_formats = backend.FORMATS 575 print( 576 "Deprecation warning! Please upgrade graphviz to version >=0.18.0 " 577 "Support for older versions will be removed in upcoming release", 578 file=sys.stderr, 579 ) 580 else: 581 valid_formats = parameters.FORMATS 582 583 if output_format not in valid_formats: 584 print(f"{output_format} is not a supported output format.", file=sys.stderr) 585 print(f"Supported formats are: {', '.join(sorted(valid_formats))}", file=sys.stderr) 586 sys.exit(1) 587 588 graph = Digraph(format=output_format) 589 590 if not is_reverse: 591 for pkg, deps in tree.items(): 592 pkg_label = f"{pkg.project_name}\\n{pkg.version}" 593 graph.node(pkg.key, label=pkg_label) 594 for dep in deps: 595 edge_label = dep.version_spec or "any" 596 if dep.is_missing: 597 dep_label = f"{dep.project_name}\\n(missing)" 598 graph.node(dep.key, label=dep_label, style="dashed") 599 graph.edge(pkg.key, dep.key, style="dashed") 600 else: 601 graph.edge(pkg.key, dep.key, label=edge_label) 602 else: 603 for dep, parents in tree.items(): 604 dep_label = f"{dep.project_name}\\n{dep.installed_version}" 605 graph.node(dep.key, label=dep_label) 606 for parent in parents: 607 # req reference of the dep associated with this 608 # particular parent package 609 req_ref = parent.req 610 edge_label = req_ref.version_spec or "any" 611 graph.edge(dep.key, parent.key, label=edge_label) 612 613 # Allow output of dot format, even if GraphViz isn't installed. 614 if output_format == "dot": 615 return graph.source 616 617 # As it's unknown if the selected output format is binary or not, try to 618 # decode it as UTF8 and only print it out in binary if that's not possible. 619 try: 620 return graph.pipe().decode("utf-8") 621 except UnicodeDecodeError: 622 return graph.pipe() 623 624 625 def print_graphviz(dump_output): 626 """ 627 Dump the data generated by GraphViz to stdout. 628 629 :param dump_output: The output from dump_graphviz 630 """ 631 if hasattr(dump_output, "encode"): 632 print(dump_output) 633 else: 634 with os.fdopen(sys.stdout.fileno(), "wb") as bytestream: 635 bytestream.write(dump_output) 636 637 638 def conflicting_deps(tree): 639 """ 640 Returns dependencies which are not present or conflict with the requirements of other packages. 641 642 e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed 643 644 :param tree: the requirements tree (dict) 645 :returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage 646 :rtype: dict 647 """ 648 conflicting = defaultdict(list) 649 for p, rs in tree.items(): 650 for req in rs: 651 if req.is_conflicting(): 652 conflicting[p].append(req) 653 return conflicting 654 655 656 def render_conflicts_text(conflicts): 657 if conflicts: 658 print("Warning!!! Possibly conflicting dependencies found:", file=sys.stderr) 659 # Enforce alphabetical order when listing conflicts 660 pkgs = sorted(conflicts.keys()) 661 for p in pkgs: 662 pkg = p.render_as_root(False) 663 print(f"* {pkg}", file=sys.stderr) 664 for req in conflicts[p]: 665 req_str = req.render_as_branch(False) 666 print(f" - {req_str}", file=sys.stderr) 667 668 669 def cyclic_deps(tree): 670 """ 671 Return cyclic dependencies as list of tuples 672 673 :param PackageDAG tree: package tree/dag 674 :returns: list of tuples representing cyclic dependencies 675 :rtype: list 676 """ 677 index = {p.key: {r.key for r in rs} for p, rs in tree.items()} 678 cyclic = [] 679 for p, rs in tree.items(): 680 for r in rs: 681 if p.key in index.get(r.key, []): 682 p_as_dep_of_r = [x for x in tree.get(tree.get_node_as_parent(r.key)) if x.key == p.key][0] 683 cyclic.append((p, r, p_as_dep_of_r)) 684 return cyclic 685 686 687 def render_cycles_text(cycles): 688 if cycles: 689 print("Warning!! Cyclic dependencies found:", file=sys.stderr) 690 # List in alphabetical order of the dependency that's cycling 691 # (2nd item in the tuple) 692 cycles = sorted(cycles, key=lambda xs: xs[1].key) 693 for a, b, c in cycles: 694 print(f"* {a.project_name} => {b.project_name} => {c.project_name}", file=sys.stderr) 695 696 697 def get_parser(): 698 parser = argparse.ArgumentParser(description="Dependency tree of the installed python packages") 699 parser.add_argument("-v", "--version", action="version", version=f"{__version__}") 700 parser.add_argument("-f", "--freeze", action="store_true", help="Print names so as to write freeze files") 701 parser.add_argument( 702 "--python", 703 default=sys.executable, 704 help="Python to use to look for packages in it (default: where" " installed)", 705 ) 706 parser.add_argument("-a", "--all", action="store_true", help="list all deps at top level") 707 parser.add_argument( 708 "-l", 709 "--local-only", 710 action="store_true", 711 help="If in a virtualenv that has global access " "do not show globally installed packages", 712 ) 713 parser.add_argument("-u", "--user-only", action="store_true", help="Only show installations in the user site dir") 714 parser.add_argument( 715 "-w", 716 "--warn", 717 action="store", 718 dest="warn", 719 nargs="?", 720 default="suppress", 721 choices=("silence", "suppress", "fail"), 722 help=( 723 'Warning control. "suppress" will show warnings ' 724 "but return 0 whether or not they are present. " 725 '"silence" will not show warnings at all and ' 726 'always return 0. "fail" will show warnings and ' 727 "return 1 if any are present. The default is " 728 '"suppress".' 729 ), 730 ) 731 parser.add_argument( 732 "-r", 733 "--reverse", 734 action="store_true", 735 default=False, 736 help=( 737 "Shows the dependency tree in the reverse fashion " 738 "ie. the sub-dependencies are listed with the " 739 "list of packages that need them under them." 740 ), 741 ) 742 parser.add_argument( 743 "-p", 744 "--packages", 745 help="Comma separated list of select packages to show " "in the output. If set, --all will be ignored.", 746 ) 747 parser.add_argument( 748 "-e", 749 "--exclude", 750 help="Comma separated list of select packages to exclude " "from the output. If set, --all will be ignored.", 751 metavar="PACKAGES", 752 ) 753 parser.add_argument( 754 "-j", 755 "--json", 756 action="store_true", 757 default=False, 758 help=( 759 "Display dependency tree as json. This will yield " 760 '"raw" output that may be used by external tools. ' 761 "This option overrides all other options." 762 ), 763 ) 764 parser.add_argument( 765 "--json-tree", 766 action="store_true", 767 default=False, 768 help=( 769 "Display dependency tree as json which is nested " 770 "the same way as the plain text output printed by default. " 771 "This option overrides all other options (except --json)." 772 ), 773 ) 774 parser.add_argument( 775 "--graph-output", 776 dest="output_format", 777 help=( 778 "Print a dependency graph in the specified output " 779 "format. Available are all formats supported by " 780 "GraphViz, e.g.: dot, jpeg, pdf, png, svg" 781 ), 782 ) 783 return parser 784 785 786 def _get_args(): 787 parser = get_parser() 788 return parser.parse_args() 789 790 791 def handle_non_host_target(args): 792 of_python = os.path.abspath(args.python) 793 # if target is not current python re-invoke it under the actual host 794 if of_python != os.path.abspath(sys.executable): 795 # there's no way to guarantee that graphviz is available, so refuse 796 if args.output_format: 797 print("graphviz functionality is not supported when querying" " non-host python", file=sys.stderr) 798 raise SystemExit(1) 799 argv = sys.argv[1:] # remove current python executable 800 for py_at, value in enumerate(argv): 801 if value == "--python": 802 del argv[py_at] 803 del argv[py_at] 804 elif value.startswith("--python"): 805 del argv[py_at] 806 807 main_file = inspect.getsourcefile(sys.modules[__name__]) 808 with tempfile.TemporaryDirectory() as project: 809 dest = os.path.join(project, "pipdeptree") 810 shutil.copytree(os.path.dirname(main_file), dest) 811 # invoke from an empty folder to avoid cwd altering sys.path 812 env = os.environ.copy() 813 env["PYTHONPATH"] = project 814 cmd = [of_python, "-m", "pipdeptree"] 815 cmd.extend(argv) 816 return subprocess.call(cmd, cwd=project, env=env) 817 return None 818 819 820 def get_installed_distributions(local_only=False, user_only=False): 821 try: 822 from pip._internal.metadata import pkg_resources 823 except ImportError: 824 # For backward compatibility with python ver. 2.7 and pip 825 # version 20.3.4 (the latest pip version that works with python 826 # version 2.7) 827 from pip._internal.utils import misc 828 829 return misc.get_installed_distributions(local_only=local_only, user_only=user_only) 830 else: 831 dists = pkg_resources.Environment.from_paths(None).iter_installed_distributions( 832 local_only=local_only, skip=(), user_only=user_only 833 ) 834 return [d._dist for d in dists] 835 836 837 def main(): 838 args = _get_args() 839 result = handle_non_host_target(args) 840 if result is not None: 841 return result 842 843 pkgs = get_installed_distributions(local_only=args.local_only, user_only=args.user_only) 844 845 tree = PackageDAG.from_pkgs(pkgs) 846 847 is_text_output = not any([args.json, args.json_tree, args.output_format]) 848 849 return_code = 0 850 851 # Before any reversing or filtering, show warnings to console 852 # about possibly conflicting or cyclic deps if found and warnings 853 # are enabled (i.e. only if output is to be printed to console) 854 if is_text_output and args.warn != "silence": 855 conflicts = conflicting_deps(tree) 856 if conflicts: 857 render_conflicts_text(conflicts) 858 print("-" * 72, file=sys.stderr) 859 860 cycles = cyclic_deps(tree) 861 if cycles: 862 render_cycles_text(cycles) 863 print("-" * 72, file=sys.stderr) 864 865 if args.warn == "fail" and (conflicts or cycles): 866 return_code = 1 867 868 # Reverse the tree (if applicable) before filtering, thus ensuring 869 # that the filter will be applied on ReverseTree 870 if args.reverse: 871 tree = tree.reverse() 872 873 show_only = set(args.packages.split(",")) if args.packages else None 874 exclude = set(args.exclude.split(",")) if args.exclude else None 875 876 if show_only is not None or exclude is not None: 877 tree = tree.filter(show_only, exclude) 878 879 if args.json: 880 print(render_json(tree, indent=4)) 881 elif args.json_tree: 882 print(render_json_tree(tree, indent=4)) 883 elif args.output_format: 884 output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse) 885 print_graphviz(output) 886 else: 887 render_text(tree, args.all, args.freeze) 888 889 return return_code 890 891 892 if __name__ == "__main__": 893 sys.exit(main())