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