github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/pip/bindata/pipdeptree.py (about) 1 # This file was taken from [pipdeptree](https://github.com/naiquevin/pipdeptree) 2 # at commit ee5eaf86ed0f49ea97601475e048d81e5b381902. 3 # 4 # It is licensed under the MIT License: 5 # 6 # Copyright (c) 2015 Vineet Naik (naikvin@gmail.com) 7 # 8 # Permission is hereby granted, free of charge, to any person obtaining 9 # a copy of this software and associated documentation files (the 10 # "Software"), to deal in the Software without restriction, including 11 # without limitation the rights to use, copy, modify, merge, publish, 12 # distribute, sublicense, and/or sell copies of the Software, and to 13 # permit persons to whom the Software is furnished to do so, subject to 14 # the following conditions: 15 # 16 # The above copyright notice and this permission notice shall be 17 # included in all copies or substantial portions of the Software. 18 # 19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 from __future__ import print_function 27 import os 28 import sys 29 from itertools import chain 30 from collections import defaultdict 31 import argparse 32 from operator import attrgetter 33 import json 34 from importlib import import_module 35 36 try: 37 from collections import OrderedDict 38 except ImportError: 39 from ordereddict import OrderedDict 40 41 try: 42 from pip._internal import get_installed_distributions 43 from pip._internal.operations.freeze import FrozenRequirement 44 except ImportError: 45 from pip import get_installed_distributions, FrozenRequirement 46 47 import pkg_resources 48 # inline: 49 # from graphviz import backend, Digraph 50 51 52 __version__ = '0.12.1' 53 54 55 flatten = chain.from_iterable 56 57 58 def build_dist_index(pkgs): 59 """Build an index pkgs by their key as a dict. 60 61 :param list pkgs: list of pkg_resources.Distribution instances 62 :returns: index of the pkgs by the pkg key 63 :rtype: dict 64 65 """ 66 return dict((p.key, DistPackage(p)) for p in pkgs) 67 68 69 def construct_tree(index): 70 """Construct tree representation of the pkgs from the index. 71 72 The keys of the dict representing the tree will be objects of type 73 DistPackage and the values will be list of ReqPackage objects. 74 75 :param dict index: dist index ie. index of pkgs by their keys 76 :returns: tree of pkgs and their dependencies 77 :rtype: dict 78 79 """ 80 return dict((p, [ReqPackage(r, index.get(r.key)) 81 for r in p.requires()]) 82 for p in index.values()) 83 84 85 def sorted_tree(tree): 86 """Sorts the dict representation of the tree 87 88 The root packages as well as the intermediate packages are sorted 89 in the alphabetical order of the package names. 90 91 :param dict tree: the pkg dependency tree obtained by calling 92 `construct_tree` function 93 :returns: sorted tree 94 :rtype: collections.OrderedDict 95 96 """ 97 return OrderedDict(sorted([(k, sorted(v, key=attrgetter('key'))) 98 for k, v in tree.items()], 99 key=lambda kv: kv[0].key)) 100 101 102 def find_tree_root(tree, key): 103 """Find a root in a tree by it's key 104 105 :param dict tree: the pkg dependency tree obtained by calling 106 `construct_tree` function 107 :param str key: key of the root node to find 108 :returns: a root node if found else None 109 :rtype: mixed 110 111 """ 112 result = [p for p in tree.keys() if p.key == key] 113 assert len(result) in [0, 1] 114 return None if len(result) == 0 else result[0] 115 116 117 def reverse_tree(tree): 118 """Reverse the dependency tree. 119 120 ie. the keys of the resulting dict are objects of type 121 ReqPackage and the values are lists of DistPackage objects. 122 123 :param dict tree: the pkg dependency tree obtained by calling 124 `construct_tree` function 125 :returns: reversed tree 126 :rtype: dict 127 128 """ 129 rtree = defaultdict(list) 130 child_keys = set(c.key for c in flatten(tree.values())) 131 for k, vs in tree.items(): 132 for v in vs: 133 node = find_tree_root(rtree, v.key) or v 134 rtree[node].append(k.as_required_by(v)) 135 if k.key not in child_keys: 136 rtree[k.as_requirement()] = [] 137 return rtree 138 139 140 def guess_version(pkg_key, default='?'): 141 """Guess the version of a pkg when pip doesn't provide it 142 143 :param str pkg_key: key of the package 144 :param str default: default version to return if unable to find 145 :returns: version 146 :rtype: string 147 148 """ 149 try: 150 m = import_module(pkg_key) 151 except ImportError: 152 return default 153 else: 154 return getattr(m, '__version__', default) 155 156 157 class Package(object): 158 """Abstract class for wrappers around objects that pip returns. 159 160 This class needs to be subclassed with implementations for 161 `render_as_root` and `render_as_branch` methods. 162 163 """ 164 165 def __init__(self, obj): 166 self._obj = obj 167 self.project_name = obj.project_name 168 self.key = obj.key 169 170 def render_as_root(self, frozen): 171 return NotImplementedError 172 173 def render_as_branch(self, frozen): 174 return NotImplementedError 175 176 def render(self, parent=None, frozen=False): 177 if not parent: 178 return self.render_as_root(frozen) 179 else: 180 return self.render_as_branch(frozen) 181 182 @staticmethod 183 def frozen_repr(obj): 184 fr = FrozenRequirement.from_dist(obj, []) 185 return str(fr).strip() 186 187 def __getattr__(self, key): 188 return getattr(self._obj, key) 189 190 def __repr__(self): 191 return '<{0}("{1}")>'.format(self.__class__.__name__, self.key) 192 193 194 class DistPackage(Package): 195 """Wrapper class for pkg_resources.Distribution instances 196 197 :param obj: pkg_resources.Distribution to wrap over 198 :param req: optional ReqPackage object to associate this 199 DistPackage with. This is useful for displaying the 200 tree in reverse 201 """ 202 203 def __init__(self, obj, req=None): 204 super(DistPackage, self).__init__(obj) 205 self.version_spec = None 206 self.req = req 207 208 def render_as_root(self, frozen): 209 if not frozen: 210 return '{0}=={1}'.format(self.project_name, self.version) 211 else: 212 return self.__class__.frozen_repr(self._obj) 213 214 def render_as_branch(self, frozen): 215 assert self.req is not None 216 if not frozen: 217 parent_ver_spec = self.req.version_spec 218 parent_str = self.req.project_name 219 if parent_ver_spec: 220 parent_str += parent_ver_spec 221 return ( 222 '{0}=={1} [requires: {2}]' 223 ).format(self.project_name, self.version, parent_str) 224 else: 225 return self.render_as_root(frozen) 226 227 def as_requirement(self): 228 """Return a ReqPackage representation of this DistPackage""" 229 return ReqPackage(self._obj.as_requirement(), dist=self) 230 231 def as_required_by(self, req): 232 """Return a DistPackage instance associated to a requirement 233 234 This association is necessary for displaying the tree in 235 reverse. 236 237 :param ReqPackage req: the requirement to associate with 238 :returns: DistPackage instance 239 240 """ 241 return self.__class__(self._obj, req) 242 243 def as_dict(self): 244 return {'key': self.key, 245 'package_name': self.project_name, 246 'installed_version': self.version} 247 248 249 class ReqPackage(Package): 250 """Wrapper class for Requirements instance 251 252 :param obj: The `Requirements` instance to wrap over 253 :param dist: optional `pkg_resources.Distribution` instance for 254 this requirement 255 """ 256 257 UNKNOWN_VERSION = '?' 258 259 def __init__(self, obj, dist=None): 260 super(ReqPackage, self).__init__(obj) 261 self.dist = dist 262 263 @property 264 def version_spec(self): 265 specs = sorted(self._obj.specs, reverse=True) # `reverse` makes '>' prior to '<' 266 return ','.join([''.join(sp) for sp in specs]) if specs else None 267 268 @property 269 def installed_version(self): 270 if not self.dist: 271 return guess_version(self.key, self.UNKNOWN_VERSION) 272 return self.dist.version 273 274 def is_conflicting(self): 275 """If installed version conflicts with required version""" 276 # unknown installed version is also considered conflicting 277 if self.installed_version == self.UNKNOWN_VERSION: 278 return True 279 ver_spec = (self.version_spec if self.version_spec else '') 280 req_version_str = '{0}{1}'.format(self.project_name, ver_spec) 281 req_obj = pkg_resources.Requirement.parse(req_version_str) 282 return self.installed_version not in req_obj 283 284 def render_as_root(self, frozen): 285 if not frozen: 286 return '{0}=={1}'.format(self.project_name, self.installed_version) 287 elif self.dist: 288 return self.__class__.frozen_repr(self.dist._obj) 289 else: 290 return self.project_name 291 292 def render_as_branch(self, frozen): 293 if not frozen: 294 req_ver = self.version_spec if self.version_spec else 'Any' 295 return ( 296 '{0} [required: {1}, installed: {2}]' 297 ).format(self.project_name, req_ver, self.installed_version) 298 else: 299 return self.render_as_root(frozen) 300 301 def as_dict(self): 302 return {'key': self.key, 303 'package_name': self.project_name, 304 'installed_version': self.installed_version, 305 'required_version': self.version_spec} 306 307 308 def render_tree(tree, list_all=True, show_only=None, frozen=False, exclude=None): 309 """Convert tree to string representation 310 311 :param dict tree: the package tree 312 :param bool list_all: whether to list all the pgks at the root 313 level or only those that are the 314 sub-dependencies 315 :param set show_only: set of select packages to be shown in the 316 output. This is optional arg, default: None. 317 :param bool frozen: whether or not show the names of the pkgs in 318 the output that's favourable to pip --freeze 319 :param set exclude: set of select packages to be excluded from the 320 output. This is optional arg, default: None. 321 :returns: string representation of the tree 322 :rtype: str 323 324 """ 325 tree = sorted_tree(tree) 326 branch_keys = set(r.key for r in flatten(tree.values())) 327 nodes = tree.keys() 328 use_bullets = not frozen 329 330 key_tree = dict((k.key, v) for k, v in tree.items()) 331 get_children = lambda n: key_tree.get(n.key, []) 332 333 if show_only: 334 nodes = [p for p in nodes 335 if p.key in show_only or p.project_name in show_only] 336 elif not list_all: 337 nodes = [p for p in nodes if p.key not in branch_keys] 338 339 def aux(node, parent=None, indent=0, chain=None): 340 if exclude and (node.key in exclude or node.project_name in exclude): 341 return [] 342 if chain is None: 343 chain = [node.project_name] 344 node_str = node.render(parent, frozen) 345 if parent: 346 prefix = ' '*indent + ('- ' if use_bullets else '') 347 node_str = prefix + node_str 348 result = [node_str] 349 children = [aux(c, node, indent=indent+2, 350 chain=chain+[c.project_name]) 351 for c in get_children(node) 352 if c.project_name not in chain] 353 result += list(flatten(children)) 354 return result 355 356 lines = flatten([aux(p) for p in nodes]) 357 return '\n'.join(lines) 358 359 360 def render_json(tree, indent): 361 """Converts the tree into a flat json representation. 362 363 The json repr will be a list of hashes, each hash having 2 fields: 364 - package 365 - dependencies: list of dependencies 366 367 :param dict tree: dependency tree 368 :param int indent: no. of spaces to indent json 369 :returns: json representation of the tree 370 :rtype: str 371 372 """ 373 return json.dumps([{'package': k.as_dict(), 374 'dependencies': [v.as_dict() for v in vs]} 375 for k, vs in tree.items()], 376 indent=indent) 377 378 379 def render_json_tree(tree, indent): 380 """Converts the tree into a nested json representation. 381 382 The json repr will be a list of hashes, each hash having the following fields: 383 - package_name 384 - key 385 - required_version 386 - installed_version 387 - dependencies: list of dependencies 388 389 :param dict tree: dependency tree 390 :param int indent: no. of spaces to indent json 391 :returns: json representation of the tree 392 :rtype: str 393 394 """ 395 tree = sorted_tree(tree) 396 branch_keys = set(r.key for r in flatten(tree.values())) 397 nodes = [p for p in tree.keys() if p.key not in branch_keys] 398 key_tree = dict((k.key, v) for k, v in tree.items()) 399 get_children = lambda n: key_tree.get(n.key, []) 400 401 def aux(node, parent=None, chain=None): 402 if chain is None: 403 chain = [node.project_name] 404 405 d = node.as_dict() 406 if parent: 407 d['required_version'] = node.version_spec if node.version_spec else 'Any' 408 else: 409 d['required_version'] = d['installed_version'] 410 411 d['dependencies'] = [ 412 aux(c, parent=node, chain=chain+[c.project_name]) 413 for c in get_children(node) 414 if c.project_name not in chain 415 ] 416 417 return d 418 419 return json.dumps([aux(p) for p in nodes], indent=indent) 420 421 422 def dump_graphviz(tree, output_format='dot'): 423 """Output dependency graph as one of the supported GraphViz output formats. 424 425 :param dict tree: dependency graph 426 :param string output_format: output format 427 :returns: representation of tree in the specified output format 428 :rtype: str or binary representation depending on the output format 429 430 """ 431 try: 432 from graphviz import backend, Digraph 433 except ImportError: 434 print('graphviz is not available, but necessary for the output ' 435 'option. Please install it.', file=sys.stderr) 436 sys.exit(1) 437 438 if output_format not in backend.FORMATS: 439 print('{0} is not a supported output format.'.format(output_format), 440 file=sys.stderr) 441 print('Supported formats are: {0}'.format( 442 ', '.join(sorted(backend.FORMATS))), file=sys.stderr) 443 sys.exit(1) 444 445 graph = Digraph(format=output_format) 446 for package, deps in tree.items(): 447 project_name = package.project_name 448 label = '{0}\n{1}'.format(project_name, package.version) 449 graph.node(project_name, label=label) 450 for dep in deps: 451 label = dep.version_spec 452 if not label: 453 label = 'any' 454 graph.edge(project_name, dep.project_name, label=label) 455 456 # Allow output of dot format, even if GraphViz isn't installed. 457 if output_format == 'dot': 458 return graph.source 459 460 # As it's unknown if the selected output format is binary or not, try to 461 # decode it as UTF8 and only print it out in binary if that's not possible. 462 try: 463 return graph.pipe().decode('utf-8') 464 except UnicodeDecodeError: 465 return graph.pipe() 466 467 468 def print_graphviz(dump_output): 469 """Dump the data generated by GraphViz to stdout. 470 471 :param dump_output: The output from dump_graphviz 472 """ 473 if hasattr(dump_output, 'encode'): 474 print(dump_output) 475 else: 476 with os.fdopen(sys.stdout.fileno(), 'wb') as bytestream: 477 bytestream.write(dump_output) 478 479 480 def conflicting_deps(tree): 481 """Returns dependencies which are not present or conflict with the 482 requirements of other packages. 483 484 e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed 485 486 :param tree: the requirements tree (dict) 487 :returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage 488 :rtype: dict 489 490 """ 491 conflicting = defaultdict(list) 492 for p, rs in tree.items(): 493 for req in rs: 494 if req.is_conflicting(): 495 conflicting[p].append(req) 496 return conflicting 497 498 499 def cyclic_deps(tree): 500 """Return cyclic dependencies as list of tuples 501 502 :param list pkgs: pkg_resources.Distribution instances 503 :param dict pkg_index: mapping of pkgs with their respective keys 504 :returns: list of tuples representing cyclic dependencies 505 :rtype: generator 506 507 """ 508 key_tree = dict((k.key, v) for k, v in tree.items()) 509 get_children = lambda n: key_tree.get(n.key, []) 510 cyclic = [] 511 for p, rs in tree.items(): 512 for req in rs: 513 if p.key in map(attrgetter('key'), get_children(req)): 514 cyclic.append((p, req, p)) 515 return cyclic 516 517 518 def get_parser(): 519 parser = argparse.ArgumentParser(description=( 520 'Dependency tree of the installed python packages' 521 )) 522 parser.add_argument('-v', '--version', action='version', 523 version='{0}'.format(__version__)) 524 parser.add_argument('-f', '--freeze', action='store_true', 525 help='Print names so as to write freeze files') 526 parser.add_argument('-a', '--all', action='store_true', 527 help='list all deps at top level') 528 parser.add_argument('-l', '--local-only', 529 action='store_true', help=( 530 'If in a virtualenv that has global access ' 531 'do not show globally installed packages' 532 )) 533 parser.add_argument('-u', '--user-only', action='store_true', 534 help=( 535 'Only show installations in the user site dir' 536 )) 537 parser.add_argument('-w', '--warn', action='store', dest='warn', 538 nargs='?', default='suppress', 539 choices=('silence', 'suppress', 'fail'), 540 help=( 541 'Warning control. "suppress" will show warnings ' 542 'but return 0 whether or not they are present. ' 543 '"silence" will not show warnings at all and ' 544 'always return 0. "fail" will show warnings and ' 545 'return 1 if any are present. The default is ' 546 '"suppress".' 547 )) 548 parser.add_argument('-r', '--reverse', action='store_true', 549 default=False, help=( 550 'Shows the dependency tree in the reverse fashion ' 551 'ie. the sub-dependencies are listed with the ' 552 'list of packages that need them under them.' 553 )) 554 parser.add_argument('-p', '--packages', 555 help=( 556 'Comma separated list of select packages to show ' 557 'in the output. If set, --all will be ignored.' 558 )) 559 parser.add_argument('-e', '--exclude', 560 help=( 561 'Comma separated list of select packages to exclude ' 562 'from the output. If set, --all will be ignored.' 563 ), metavar='PACKAGES') 564 parser.add_argument('-j', '--json', action='store_true', default=False, 565 help=( 566 'Display dependency tree as json. This will yield ' 567 '"raw" output that may be used by external tools. ' 568 'This option overrides all other options.' 569 )) 570 parser.add_argument('--json-tree', action='store_true', default=False, 571 help=( 572 'Display dependency tree as json which is nested ' 573 'the same way as the plain text output printed by default. ' 574 'This option overrides all other options (except --json).' 575 )) 576 parser.add_argument('--graph-output', dest='output_format', 577 help=( 578 'Print a dependency graph in the specified output ' 579 'format. Available are all formats supported by ' 580 'GraphViz, e.g.: dot, jpeg, pdf, png, svg' 581 )) 582 return parser 583 584 585 def _get_args(): 586 parser = get_parser() 587 return parser.parse_args() 588 589 590 def main(): 591 args = _get_args() 592 pkgs = get_installed_distributions(local_only=args.local_only, 593 user_only=args.user_only) 594 595 dist_index = build_dist_index(pkgs) 596 tree = construct_tree(dist_index) 597 598 if args.json: 599 print(render_json(tree, indent=4)) 600 return 0 601 elif args.json_tree: 602 print(render_json_tree(tree, indent=4)) 603 return 0 604 elif args.output_format: 605 output = dump_graphviz(tree, output_format=args.output_format) 606 print_graphviz(output) 607 return 0 608 609 return_code = 0 610 611 # show warnings about possibly conflicting deps if found and 612 # warnings are enabled 613 if args.warn != 'silence': 614 conflicting = conflicting_deps(tree) 615 if conflicting: 616 print('Warning!!! Possibly conflicting dependencies found:', 617 file=sys.stderr) 618 for p, reqs in conflicting.items(): 619 pkg = p.render_as_root(False) 620 print('* {}'.format(pkg), file=sys.stderr) 621 for req in reqs: 622 req_str = req.render_as_branch(False) 623 print(' - {}'.format(req_str), file=sys.stderr) 624 print('-'*72, file=sys.stderr) 625 626 cyclic = cyclic_deps(tree) 627 if cyclic: 628 print('Warning!! Cyclic dependencies found:', file=sys.stderr) 629 for a, b, c in cyclic: 630 print('* {0} => {1} => {2}'.format(a.project_name, 631 b.project_name, 632 c.project_name), 633 file=sys.stderr) 634 print('-'*72, file=sys.stderr) 635 636 if args.warn == 'fail' and (conflicting or cyclic): 637 return_code = 1 638 639 show_only = set(args.packages.split(',')) if args.packages else None 640 exclude = set(args.exclude.split(',')) if args.exclude else None 641 642 if show_only and exclude and (show_only & exclude): 643 print('Conflicting packages found in --packages and --exclude lists.', file=sys.stderr) 644 sys.exit(1) 645 646 tree = render_tree(tree if not args.reverse else reverse_tree(tree), 647 list_all=args.all, show_only=show_only, 648 frozen=args.freeze, exclude=exclude) 649 print(tree) 650 return return_code 651 652 653 if __name__ == '__main__': 654 sys.exit(main())