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