src/eric7/PipInterface/pipdeptree.py

branch
eric7
changeset 9218
71cf3979a6c9
child 9589
09218eb3ae21
diff -r 0c34da0d7b76 -r 71cf3979a6c9 src/eric7/PipInterface/pipdeptree.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/PipInterface/pipdeptree.py	Tue Jul 12 16:26:22 2022 +0200
@@ -0,0 +1,960 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Copyright (c) 2015 Vineet Naik (naikvin@gmail.com)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+
+#
+# Modified to be used within the eric-ide project.
+#
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+from __future__ import print_function
+import os
+import inspect
+import sys
+import subprocess
+from itertools import chain
+from collections import defaultdict, deque
+import argparse
+import json
+from importlib import import_module
+import tempfile
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    from ordereddict import OrderedDict
+
+try:
+    from collections.abc import Mapping
+except ImportError:
+    from collections import Mapping
+
+from pip._vendor import pkg_resources
+try:
+    from pip._internal.operations.freeze import FrozenRequirement
+except ImportError:
+    from pip import FrozenRequirement
+
+
+__version__ = '2.2.1'
+
+
+flatten = chain.from_iterable
+
+
+def sorted_tree(tree):
+    """Sorts the dict representation of the tree
+
+    The root packages as well as the intermediate packages are sorted
+    in the alphabetical order of the package names.
+
+    :param dict tree: the pkg dependency tree obtained by calling
+                     `construct_tree` function
+    :returns: sorted tree
+    :rtype: collections.OrderedDict
+
+    """
+    return OrderedDict([(k, sorted(v)) for k, v in sorted(tree.items())])
+
+
+def guess_version(pkg_key, default='?'):
+    """Guess the version of a pkg when pip doesn't provide it
+
+    :param str pkg_key: key of the package
+    :param str default: default version to return if unable to find
+    :returns: version
+    :rtype: string
+
+    """
+    try:
+        m = import_module(pkg_key)
+    except ImportError:
+        return default
+    else:
+        v = getattr(m, '__version__', default)
+        if inspect.ismodule(v):
+            return getattr(v, '__version__', default)
+        else:
+            return v
+
+
+def frozen_req_from_dist(dist):
+    # The `pip._internal.metadata` modules were introduced in 21.1.1
+    # and the `pip._internal.operations.freeze.FrozenRequirement`
+    # class now expects dist to be a subclass of
+    # `pip._internal.metadata.BaseDistribution`, however the
+    # `pip._internal.utils.misc.get_installed_distributions` continues
+    # to return objects of type
+    # pip._vendor.pkg_resources.DistInfoDistribution.
+    #
+    # This is a hacky backward compatible (with older versions of pip)
+    # fix.
+    try:
+        from pip._internal import metadata
+    except ImportError:
+        pass
+    else:
+        dist = metadata.pkg_resources.Distribution(dist)
+
+    try:
+        return FrozenRequirement.from_dist(dist)
+    except TypeError:
+        return FrozenRequirement.from_dist(dist, [])
+
+
+class Package(object):
+    """Abstract class for wrappers around objects that pip returns.
+
+    This class needs to be subclassed with implementations for
+    `render_as_root` and `render_as_branch` methods.
+
+    """
+
+    def __init__(self, obj):
+        self._obj = obj
+        self.project_name = obj.project_name
+        self.key = obj.key
+
+    def render_as_root(self, frozen):
+        return NotImplementedError
+
+    def render_as_branch(self, frozen):
+        return NotImplementedError
+
+    def render(self, parent=None, frozen=False):
+        if not parent:
+            return self.render_as_root(frozen)
+        else:
+            return self.render_as_branch(frozen)
+
+    @staticmethod
+    def frozen_repr(obj):
+        fr = frozen_req_from_dist(obj)
+        return str(fr).strip()
+
+    def __getattr__(self, key):
+        return getattr(self._obj, key)
+
+    def __repr__(self):
+        return '<{0}("{1}")>'.format(self.__class__.__name__, self.key)
+
+    def __lt__(self, rhs):
+        return self.key < rhs.key
+
+
+class DistPackage(Package):
+    """Wrapper class for pkg_resources.Distribution instances
+
+      :param obj: pkg_resources.Distribution to wrap over
+      :param req: optional ReqPackage object to associate this
+                  DistPackage with. This is useful for displaying the
+                  tree in reverse
+    """
+
+    def __init__(self, obj, req=None):
+        super(DistPackage, self).__init__(obj)
+        self.version_spec = None
+        self.req = req
+
+    def render_as_root(self, frozen):
+        if not frozen:
+            return '{0}=={1}'.format(self.project_name, self.version)
+        else:
+            return self.__class__.frozen_repr(self._obj)
+
+    def render_as_branch(self, frozen):
+        assert self.req is not None
+        if not frozen:
+            parent_ver_spec = self.req.version_spec
+            parent_str = self.req.project_name
+            if parent_ver_spec:
+                parent_str += parent_ver_spec
+            return (
+                '{0}=={1} [requires: {2}]'
+            ).format(self.project_name, self.version, parent_str)
+        else:
+            return self.render_as_root(frozen)
+
+    def as_requirement(self):
+        """Return a ReqPackage representation of this DistPackage"""
+        return ReqPackage(self._obj.as_requirement(), dist=self)
+
+    def as_parent_of(self, req):
+        """Return a DistPackage instance associated to a requirement
+
+        This association is necessary for reversing the PackageDAG.
+
+        If `req` is None, and the `req` attribute of the current
+        instance is also None, then the same instance will be
+        returned.
+
+        :param ReqPackage req: the requirement to associate with
+        :returns: DistPackage instance
+
+        """
+        if req is None and self.req is None:
+            return self
+        return self.__class__(self._obj, req)
+
+    def as_dict(self):
+        return {'key': self.key,
+                'package_name': self.project_name,
+                'installed_version': self.version}
+
+
+class ReqPackage(Package):
+    """Wrapper class for Requirements instance
+
+      :param obj: The `Requirements` instance to wrap over
+      :param dist: optional `pkg_resources.Distribution` instance for
+                   this requirement
+    """
+
+    UNKNOWN_VERSION = '?'
+
+    def __init__(self, obj, dist=None):
+        super(ReqPackage, self).__init__(obj)
+        self.dist = dist
+
+    @property
+    def version_spec(self):
+        specs = sorted(self._obj.specs, reverse=True)  # `reverse` makes '>' prior to '<'
+        return ','.join([''.join(sp) for sp in specs]) if specs else None
+
+    @property
+    def installed_version(self):
+        if not self.dist:
+            return guess_version(self.key, self.UNKNOWN_VERSION)
+        return self.dist.version
+
+    @property
+    def is_missing(self):
+        return self.installed_version == self.UNKNOWN_VERSION
+
+    def is_conflicting(self):
+        """If installed version conflicts with required version"""
+        # unknown installed version is also considered conflicting
+        if self.installed_version == self.UNKNOWN_VERSION:
+            return True
+        ver_spec = (self.version_spec if self.version_spec else '')
+        req_version_str = '{0}{1}'.format(self.project_name, ver_spec)
+        req_obj = pkg_resources.Requirement.parse(req_version_str)
+        return self.installed_version not in req_obj
+
+    def render_as_root(self, frozen):
+        if not frozen:
+            return '{0}=={1}'.format(self.project_name, self.installed_version)
+        elif self.dist:
+            return self.__class__.frozen_repr(self.dist._obj)
+        else:
+            return self.project_name
+
+    def render_as_branch(self, frozen):
+        if not frozen:
+            req_ver = self.version_spec if self.version_spec else 'Any'
+            return (
+                '{0} [required: {1}, installed: {2}]'
+                ).format(self.project_name, req_ver, self.installed_version)
+        else:
+            return self.render_as_root(frozen)
+
+    def as_dict(self):
+        return {'key': self.key,
+                'package_name': self.project_name,
+                'installed_version': self.installed_version,
+                'required_version': self.version_spec}
+
+
+class PackageDAG(Mapping):
+    """Representation of Package dependencies as directed acyclic graph
+    using a dict (Mapping) as the underlying datastructure.
+
+    The nodes and their relationships (edges) are internally
+    stored using a map as follows,
+
+    {a: [b, c],
+     b: [d],
+     c: [d, e],
+     d: [e],
+     e: [],
+     f: [b],
+     g: [e, f]}
+
+    Here, node `a` has 2 children nodes `b` and `c`. Consider edge
+    direction from `a` -> `b` and `a` -> `c` respectively.
+
+    A node is expected to be an instance of a subclass of
+    `Package`. The keys are must be of class `DistPackage` and each
+    item in values must be of class `ReqPackage`. (See also
+    ReversedPackageDAG where the key and value types are
+    interchanged).
+
+    """
+
+    @classmethod
+    def from_pkgs(cls, pkgs):
+        pkgs = [DistPackage(p) for p in pkgs]
+        idx = {p.key: p for p in pkgs}
+        m = {p: [ReqPackage(r, idx.get(r.key))
+                 for r in p.requires()]
+             for p in pkgs}
+        return cls(m)
+
+    def __init__(self, m):
+        """Initialize the PackageDAG object
+
+        :param dict m: dict of node objects (refer class docstring)
+        :returns: None
+        :rtype: NoneType
+
+        """
+        self._obj = m
+        self._index = {p.key: p for p in list(self._obj)}
+
+    def get_node_as_parent(self, node_key):
+        """Get the node from the keys of the dict representing the DAG.
+
+        This method is useful if the dict representing the DAG
+        contains different kind of objects in keys and values. Use
+        this method to lookup a node obj as a parent (from the keys of
+        the dict) given a node key.
+
+        :param node_key: identifier corresponding to key attr of node obj
+        :returns: node obj (as present in the keys of the dict)
+        :rtype: Object
+
+        """
+        try:
+            return self._index[node_key]
+        except KeyError:
+            return None
+
+    def get_children(self, node_key):
+        """Get child nodes for a node by it's key
+
+        :param str node_key: key of the node to get children of
+        :returns: list of child nodes
+        :rtype: ReqPackage[]
+
+        """
+        node = self.get_node_as_parent(node_key)
+        return self._obj[node] if node else []
+
+    def filter(self, include, exclude):
+        """Filters nodes in a graph by given parameters
+
+        If a node is included, then all it's children are also
+        included.
+
+        :param set include: set of node keys to include (or None)
+        :param set exclude: set of node keys to exclude (or None)
+        :returns: filtered version of the graph
+        :rtype: PackageDAG
+
+        """
+        # If neither of the filters are specified, short circuit
+        if include is None and exclude is None:
+            return self
+
+        # Note: In following comparisons, we use lower cased values so
+        # that user may specify `key` or `project_name`. As per the
+        # documentation, `key` is simply
+        # `project_name.lower()`. Refer:
+        # https://setuptools.readthedocs.io/en/latest/pkg_resources.html#distribution-objects
+        if include:
+            include = set([s.lower() for s in include])
+        if exclude:
+            exclude = set([s.lower() for s in exclude])
+        else:
+            exclude = set([])
+
+        # Check for mutual exclusion of show_only and exclude sets
+        # after normalizing the values to lowercase
+        if include and exclude:
+            assert not (include & exclude)
+
+        # Traverse the graph in a depth first manner and filter the
+        # nodes according to `show_only` and `exclude` sets
+        stack = deque()
+        m = {}
+        seen = set([])
+        for node in self._obj.keys():
+            if node.key in exclude:
+                continue
+            if include is None or node.key in include:
+                stack.append(node)
+            while True:
+                if len(stack) > 0:
+                    n = stack.pop()
+                    cldn = [c for c in self._obj[n]
+                            if c.key not in exclude]
+                    m[n] = cldn
+                    seen.add(n.key)
+                    for c in cldn:
+                        if c.key not in seen:
+                            cld_node = self.get_node_as_parent(c.key)
+                            if cld_node:
+                                stack.append(cld_node)
+                            else:
+                                # It means there's no root node
+                                # corresponding to the child node
+                                # ie. a dependency is missing
+                                continue
+                else:
+                    break
+
+        return self.__class__(m)
+
+    def reverse(self):
+        """Reverse the DAG, or turn it upside-down
+
+        In other words, the directions of edges of the nodes in the
+        DAG will be reversed.
+
+        Note that this function purely works on the nodes in the
+        graph. This implies that to perform a combination of filtering
+        and reversing, the order in which `filter` and `reverse`
+        methods should be applied is important. For eg. if reverse is
+        called on a filtered graph, then only the filtered nodes and
+        it's children will be considered when reversing. On the other
+        hand, if filter is called on reversed DAG, then the definition
+        of "child" nodes is as per the reversed DAG.
+
+        :returns: DAG in the reversed form
+        :rtype: ReversedPackageDAG
+
+        """
+        m = defaultdict(list)
+        child_keys = set(r.key for r in flatten(self._obj.values()))
+        for k, vs in self._obj.items():
+            for v in vs:
+                # if v is already added to the dict, then ensure that
+                # we are using the same object. This check is required
+                # as we're using array mutation
+                try:
+                    node = [p for p in m.keys() if p.key == v.key][0]
+                except IndexError:
+                    node = v
+                m[node].append(k.as_parent_of(v))
+            if k.key not in child_keys:
+                m[k.as_requirement()] = []
+        return ReversedPackageDAG(dict(m))
+
+    def sort(self):
+        """Return sorted tree in which the underlying _obj dict is an
+        OrderedDict, sorted alphabetically by the keys
+
+        :returns: Instance of same class with OrderedDict
+
+        """
+        return self.__class__(sorted_tree(self._obj))
+
+    # Methods required by the abstract base class Mapping
+    def __getitem__(self, *args):
+        return self._obj.get(*args)
+
+    def __iter__(self):
+        return self._obj.__iter__()
+
+    def __len__(self):
+        return len(self._obj)
+
+
+class ReversedPackageDAG(PackageDAG):
+    """Representation of Package dependencies in the reverse
+    order.
+
+    Similar to it's super class `PackageDAG`, the underlying
+    datastructure is a dict, but here the keys are expected to be of
+    type `ReqPackage` and each item in the values of type
+    `DistPackage`.
+
+    Typically, this object will be obtained by calling
+    `PackageDAG.reverse`.
+
+    """
+
+    def reverse(self):
+        """Reverse the already reversed DAG to get the PackageDAG again
+
+        :returns: reverse of the reversed DAG
+        :rtype: PackageDAG
+
+        """
+        m = defaultdict(list)
+        child_keys = set(r.key for r in flatten(self._obj.values()))
+        for k, vs in self._obj.items():
+            for v in vs:
+                try:
+                    node = [p for p in m.keys() if p.key == v.key][0]
+                except IndexError:
+                    node = v.as_parent_of(None)
+                m[node].append(k)
+            if k.key not in child_keys:
+                m[k.dist] = []
+        return PackageDAG(dict(m))
+
+
+def render_text(tree, list_all=True, frozen=False):
+    """Print tree as text on console
+
+    :param dict tree: the package tree
+    :param bool list_all: whether to list all the pgks at the root
+                          level or only those that are the
+                          sub-dependencies
+    :param bool frozen: whether or not show the names of the pkgs in
+                        the output that's favourable to pip --freeze
+    :returns: None
+
+    """
+    tree = tree.sort()
+    nodes = tree.keys()
+    branch_keys = set(r.key for r in flatten(tree.values()))
+    use_bullets = not frozen
+
+    if not list_all:
+        nodes = [p for p in nodes if p.key not in branch_keys]
+
+    def aux(node, parent=None, indent=0, chain=None):
+        chain = chain or []
+        node_str = node.render(parent, frozen)
+        if parent:
+            prefix = ' '*indent + ('- ' if use_bullets else '')
+            node_str = prefix + node_str
+        result = [node_str]
+        children = [aux(c, node, indent=indent+2,
+                        chain=chain+[c.project_name])
+                    for c in tree.get_children(node.key)
+                    if c.project_name not in chain]
+        result += list(flatten(children))
+        return result
+
+    lines = flatten([aux(p) for p in nodes])
+    print('\n'.join(lines))
+
+
+def render_json(tree, indent):
+    """Converts the tree into a flat json representation.
+
+    The json repr will be a list of hashes, each hash having 2 fields:
+      - package
+      - dependencies: list of dependencies
+
+    :param dict tree: dependency tree
+    :param int indent: no. of spaces to indent json
+    :returns: json representation of the tree
+    :rtype: str
+
+    """
+    tree = tree.sort()
+    return json.dumps([{'package': k.as_dict(),
+                        'dependencies': [v.as_dict() for v in vs]}
+                       for k, vs in tree.items()],
+                      indent=indent)
+
+
+def render_json_tree(tree, indent):
+    """Converts the tree into a nested json representation.
+
+    The json repr will be a list of hashes, each hash having the following fields:
+      - package_name
+      - key
+      - required_version
+      - installed_version
+      - dependencies: list of dependencies
+
+    :param dict tree: dependency tree
+    :param int indent: no. of spaces to indent json
+    :returns: json representation of the tree
+    :rtype: str
+
+    """
+    tree = tree.sort()
+    branch_keys = set(r.key for r in flatten(tree.values()))
+    nodes = [p for p in tree.keys() if p.key not in branch_keys]
+
+    def aux(node, parent=None, chain=None):
+        if chain is None:
+            chain = [node.project_name]
+
+        d = node.as_dict()
+        if parent:
+            d['required_version'] = node.version_spec if node.version_spec else 'Any'
+        else:
+            d['required_version'] = d['installed_version']
+
+        d['dependencies'] = [
+            aux(c, parent=node, chain=chain+[c.project_name])
+            for c in tree.get_children(node.key)
+            if c.project_name not in chain
+        ]
+
+        return d
+
+    return json.dumps([aux(p) for p in nodes], indent=indent)
+
+
+def dump_graphviz(tree, output_format='dot', is_reverse=False):
+    """Output dependency graph as one of the supported GraphViz output formats.
+
+    :param dict tree: dependency graph
+    :param string output_format: output format
+    :returns: representation of tree in the specified output format
+    :rtype: str or binary representation depending on the output format
+
+    """
+    try:
+        from graphviz import Digraph
+    except ImportError:
+        print('graphviz is not available, but necessary for the output '
+              'option. Please install it.', file=sys.stderr)
+        sys.exit(1)
+
+    try:
+        from graphviz import parameters
+    except ImportError:
+        from graphviz import backend
+        valid_formats = backend.FORMATS
+        print('Deprecation warning! Please upgrade graphviz to version >=0.18.0 '
+              'Support for older versions will be removed in upcoming release',
+              file=sys.stderr)
+    else:
+        valid_formats = parameters.FORMATS
+
+    if output_format not in valid_formats:
+        print('{0} is not a supported output format.'.format(output_format),
+              file=sys.stderr)
+        print('Supported formats are: {0}'.format(
+            ', '.join(sorted(valid_formats))), file=sys.stderr)
+        sys.exit(1)
+
+    graph = Digraph(format=output_format)
+
+    if not is_reverse:
+        for pkg, deps in tree.items():
+            pkg_label = '{0}\\n{1}'.format(pkg.project_name, pkg.version)
+            graph.node(pkg.key, label=pkg_label)
+            for dep in deps:
+                edge_label = dep.version_spec or 'any'
+                if dep.is_missing:
+                    dep_label = '{0}\\n(missing)'.format(dep.project_name)
+                    graph.node(dep.key, label=dep_label, style='dashed')
+                    graph.edge(pkg.key, dep.key, style='dashed')
+                else:
+                    graph.edge(pkg.key, dep.key, label=edge_label)
+    else:
+        for dep, parents in tree.items():
+            dep_label = '{0}\\n{1}'.format(dep.project_name,
+                                          dep.installed_version)
+            graph.node(dep.key, label=dep_label)
+            for parent in parents:
+                # req reference of the dep associated with this
+                # particular parent package
+                req_ref = parent.req
+                edge_label = req_ref.version_spec or 'any'
+                graph.edge(dep.key, parent.key, label=edge_label)
+
+    # Allow output of dot format, even if GraphViz isn't installed.
+    if output_format == 'dot':
+        return graph.source
+
+    # As it's unknown if the selected output format is binary or not, try to
+    # decode it as UTF8 and only print it out in binary if that's not possible.
+    try:
+        return graph.pipe().decode('utf-8')
+    except UnicodeDecodeError:
+        return graph.pipe()
+
+
+def print_graphviz(dump_output):
+    """Dump the data generated by GraphViz to stdout.
+
+    :param dump_output: The output from dump_graphviz
+    """
+    if hasattr(dump_output, 'encode'):
+        print(dump_output)
+    else:
+        with os.fdopen(sys.stdout.fileno(), 'wb') as bytestream:
+            bytestream.write(dump_output)
+
+
+def conflicting_deps(tree):
+    """Returns dependencies which are not present or conflict with the
+    requirements of other packages.
+
+    e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed
+
+    :param tree: the requirements tree (dict)
+    :returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage
+    :rtype: dict
+
+    """
+    conflicting = defaultdict(list)
+    for p, rs in tree.items():
+        for req in rs:
+            if req.is_conflicting():
+                conflicting[p].append(req)
+    return conflicting
+
+
+def render_conflicts_text(conflicts):
+    if conflicts:
+        print('Warning!!! Possibly conflicting dependencies found:',
+              file=sys.stderr)
+        # Enforce alphabetical order when listing conflicts
+        pkgs = sorted(conflicts.keys())
+        for p in pkgs:
+            pkg = p.render_as_root(False)
+            print('* {}'.format(pkg), file=sys.stderr)
+            for req in conflicts[p]:
+                req_str = req.render_as_branch(False)
+                print(' - {}'.format(req_str), file=sys.stderr)
+
+
+def cyclic_deps(tree):
+    """Return cyclic dependencies as list of tuples
+
+    :param PackageDAG pkgs: package tree/dag
+    :returns: list of tuples representing cyclic dependencies
+    :rtype: list
+
+    """
+    index = {p.key: set([r.key for r in rs]) for p, rs in tree.items()}
+    cyclic = []
+    for p, rs in tree.items():
+        for r in rs:
+            if p.key in index.get(r.key, []):
+                p_as_dep_of_r = [x for x
+                                 in tree.get(tree.get_node_as_parent(r.key))
+                                 if x.key == p.key][0]
+                cyclic.append((p, r, p_as_dep_of_r))
+    return cyclic
+
+
+def render_cycles_text(cycles):
+    if cycles:
+        print('Warning!! Cyclic dependencies found:', file=sys.stderr)
+        # List in alphabetical order of the dependency that's cycling
+        # (2nd item in the tuple)
+        cycles = sorted(cycles, key=lambda xs: xs[1].key)
+        for a, b, c in cycles:
+            print('* {0} => {1} => {2}'.format(a.project_name,
+                                               b.project_name,
+                                               c.project_name),
+                  file=sys.stderr)
+
+
+def get_parser():
+    parser = argparse.ArgumentParser(description=(
+        'Dependency tree of the installed python packages'
+    ))
+    parser.add_argument('-v', '--version', action='version',
+                        version='{0}'.format(__version__))
+    parser.add_argument('-f', '--freeze', action='store_true',
+                        help='Print names so as to write freeze files')
+    parser.add_argument('--python', default=sys.executable,
+                        help='Python to use to look for packages in it (default: where'
+                             ' installed)')
+    parser.add_argument('-a', '--all', action='store_true',
+                        help='list all deps at top level')
+    parser.add_argument('-l', '--local-only',
+                        action='store_true', help=(
+                            'If in a virtualenv that has global access '
+                            'do not show globally installed packages'
+                        ))
+    parser.add_argument('-u', '--user-only', action='store_true',
+                        help=(
+                            'Only show installations in the user site dir'
+                        ))
+    parser.add_argument('-w', '--warn', action='store', dest='warn',
+                        nargs='?', default='suppress',
+                        choices=('silence', 'suppress', 'fail'),
+                        help=(
+                            'Warning control. "suppress" will show warnings '
+                            'but return 0 whether or not they are present. '
+                            '"silence" will not show warnings at all and '
+                            'always return 0. "fail" will show warnings and '
+                            'return 1 if any are present. The default is '
+                            '"suppress".'
+                        ))
+    parser.add_argument('-r', '--reverse', action='store_true',
+                        default=False, help=(
+                            'Shows the dependency tree in the reverse fashion '
+                            'ie. the sub-dependencies are listed with the '
+                            'list of packages that need them under them.'
+                        ))
+    parser.add_argument('-p', '--packages',
+                        help=(
+                            'Comma separated list of select packages to show '
+                            'in the output. If set, --all will be ignored.'
+                        ))
+    parser.add_argument('-e', '--exclude',
+                        help=(
+                            'Comma separated list of select packages to exclude '
+                            'from the output. If set, --all will be ignored.'
+                        ), metavar='PACKAGES')
+    parser.add_argument('-j', '--json', action='store_true', default=False,
+                        help=(
+                            'Display dependency tree as json. This will yield '
+                            '"raw" output that may be used by external tools. '
+                            'This option overrides all other options.'
+                        ))
+    parser.add_argument('--json-tree', action='store_true', default=False,
+                        help=(
+                            'Display dependency tree as json which is nested '
+                            'the same way as the plain text output printed by default. '
+                            'This option overrides all other options (except --json).'
+                        ))
+    parser.add_argument('--graph-output', dest='output_format',
+                        help=(
+                            'Print a dependency graph in the specified output '
+                            'format. Available are all formats supported by '
+                            'GraphViz, e.g.: dot, jpeg, pdf, png, svg'
+                        ))
+    return parser
+
+
+def _get_args():
+    parser = get_parser()
+    return parser.parse_args()
+
+
+def handle_non_host_target(args):
+    of_python = os.path.abspath(args.python)
+    # if target is not current python re-invoke it under the actual host
+    if of_python != os.path.abspath(sys.executable):
+        # there's no way to guarantee that graphviz is available, so refuse
+        if args.output_format:
+            print("graphviz functionality is not supported when querying"
+                  " non-host python", file=sys.stderr)
+            raise SystemExit(1)
+        argv = sys.argv[1:]  # remove current python executable
+        for py_at, value in enumerate(argv):
+            if value == "--python":
+                del argv[py_at]
+                del argv[py_at]
+            elif value.startswith("--python"):
+                del argv[py_at]
+        # feed the file as argument, instead of file
+        # to avoid adding the file path to sys.path, that can affect result
+        file_path = inspect.getsourcefile(sys.modules[__name__])
+        with open(file_path, 'rt') as file_handler:
+            content = file_handler.read()
+        cmd = [of_python, "-c", content]
+        cmd.extend(argv)
+        # invoke from an empty folder to avoid cwd altering sys.path
+        cwd = tempfile.mkdtemp()
+        try:
+            return subprocess.call(cmd, cwd=cwd)
+        finally:
+            os.removedirs(cwd)
+    return None
+
+
+def get_installed_distributions(local_only=False, user_only=False):
+    try:
+        from pip._internal.metadata import get_environment
+    except ImportError:
+        # For backward compatibility with python ver. 2.7 and pip
+        # version 20.3.4 (latest pip version that works with python
+        # version 2.7)
+        from pip._internal.utils import misc
+        return misc.get_installed_distributions(
+            local_only=local_only,
+            user_only=user_only
+        )
+    else:
+        dists = get_environment(None).iter_installed_distributions(
+            local_only=local_only,
+            skip=(),
+            user_only=user_only
+        )
+        return [d._dist for d in dists]
+
+
+def main():
+    os.environ["_PIP_USE_IMPORTLIB_METADATA"] = "False"
+    # patched for 3.11+ compatibility
+    
+    args = _get_args()
+    result = handle_non_host_target(args)
+    if result is not None:
+        return result
+
+    pkgs = get_installed_distributions(local_only=args.local_only,
+                                       user_only=args.user_only)
+
+    tree = PackageDAG.from_pkgs(pkgs)
+
+    is_text_output = not any([args.json, args.json_tree, args.output_format])
+
+    return_code = 0
+
+    # Before any reversing or filtering, show warnings to console
+    # about possibly conflicting or cyclic deps if found and warnings
+    # are enabled (ie. only if output is to be printed to console)
+    if is_text_output and args.warn != 'silence':
+        conflicts = conflicting_deps(tree)
+        if conflicts:
+            render_conflicts_text(conflicts)
+            print('-'*72, file=sys.stderr)
+
+        cycles = cyclic_deps(tree)
+        if cycles:
+            render_cycles_text(cycles)
+            print('-'*72, file=sys.stderr)
+
+        if args.warn == 'fail' and (conflicts or cycles):
+            return_code = 1
+
+    # Reverse the tree (if applicable) before filtering, thus ensuring
+    # that the filter will be applied on ReverseTree
+    if args.reverse:
+        tree = tree.reverse()
+
+    show_only = set(args.packages.split(',')) if args.packages else None
+    exclude = set(args.exclude.split(',')) if args.exclude else None
+
+    if show_only is not None or exclude is not None:
+        tree = tree.filter(show_only, exclude)
+
+    if args.json:
+        print(render_json(tree, indent=4))
+    elif args.json_tree:
+        print(render_json_tree(tree, indent=4))
+    elif args.output_format:
+        output = dump_graphviz(tree,
+                               output_format=args.output_format,
+                               is_reverse=args.reverse)
+        print_graphviz(output)
+    else:
+        render_text(tree, args.all, args.freeze)
+
+    return return_code
+
+
+if __name__ == '__main__':
+    sys.exit(main())

eric ide

mercurial