github.com/jfrog/build-info-go@v1.9.26/utils/pythonutils/pipdeptree/pipdeptree.py (about)

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