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