src/eric7/PipInterface/pipdeptree.py

branch
eric7
changeset 9589
09218eb3ae21
parent 9218
71cf3979a6c9
child 9653
e67609152c5e
--- a/src/eric7/PipInterface/pipdeptree.py	Wed Dec 07 13:32:38 2022 +0100
+++ b/src/eric7/PipInterface/pipdeptree.py	Thu Dec 08 10:38:38 2022 +0100
@@ -1,8 +1,12 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+#
+# eric-ide modification: Copyright from file "LICENSE"
+#
+
 """
-Copyright (c) 2015 Vineet Naik (naikvin@gmail.com)
+Copyright (c) The pipdeptree developers
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
@@ -25,78 +29,77 @@
 """
 
 #
-# Modified to be used within the eric-ide project.
+# Slightly modified to be used within the eric-ide project.
 #
 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
 #
 
-from __future__ import print_function
+import argparse
+import inspect
+import json
 import os
-import inspect
-import sys
+import shutil
 import subprocess
-from itertools import chain
-from collections import defaultdict, deque
-import argparse
-import json
-from importlib import import_module
+import sys
 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 collections import defaultdict, deque
+from collections.abc import Mapping
+from importlib import import_module
+from itertools import chain
 
 from pip._vendor import pkg_resources
+
 try:
     from pip._internal.operations.freeze import FrozenRequirement
 except ImportError:
     from pip import FrozenRequirement
 
 
-__version__ = '2.2.1'
+__version__ = '2.3.3'  # eric-ide modification: from version.py
 
 
 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.
+    """
+    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
+    :param dict tree: the pkg dependency tree obtained by calling `construct_tree` function
     :returns: sorted tree
-    :rtype: collections.OrderedDict
-
+    :rtype: dict
     """
-    return OrderedDict([(k, sorted(v)) for k, v in sorted(tree.items())])
+    return {k: sorted(v) for k, v in sorted(tree.items())}
 
 
-def guess_version(pkg_key, default='?'):
+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:
+        if sys.version_info >= (3, 8):  # pragma: >=3.8 cover
+            import importlib.metadata as importlib_metadata
+        else:  # pragma: <3.8 cover
+            import importlib_metadata
+        return importlib_metadata.version(pkg_key)
+    except ImportError:
+        pass
+    # Avoid AssertionError with setuptools, see https://github.com/tox-dev/pipdeptree/issues/162
+    if pkg_key in {"setuptools"}:
+        return default
+    try:
         m = import_module(pkg_key)
     except ImportError:
         return default
     else:
-        v = getattr(m, '__version__', default)
+        v = getattr(m, "__version__", default)
         if inspect.ismodule(v):
-            return getattr(v, '__version__', default)
+            return getattr(v, "__version__", default)
         else:
             return v
 
@@ -125,12 +128,10 @@
         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.
-
+class Package:
+    """
+    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):
@@ -138,10 +139,10 @@
         self.project_name = obj.project_name
         self.key = obj.key
 
-    def render_as_root(self, frozen):
+    def render_as_root(self, frozen):  # noqa: U100
         return NotImplementedError
 
-    def render_as_branch(self, frozen):
+    def render_as_branch(self, frozen):  # noqa: U100
         return NotImplementedError
 
     def render(self, parent=None, frozen=False):
@@ -159,29 +160,29 @@
         return getattr(self._obj, key)
 
     def __repr__(self):
-        return '<{0}("{1}")>'.format(self.__class__.__name__, self.key)
+        return f'<{self.__class__.__name__}("{self.key}")>'
 
     def __lt__(self, rhs):
         return self.key < rhs.key
 
 
 class DistPackage(Package):
-    """Wrapper class for pkg_resources.Distribution instances
+    """
+    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
+    :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)
+        super().__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)
+            return f"{self.project_name}=={self.version}"
         else:
             return self.__class__.frozen_repr(self._obj)
 
@@ -192,9 +193,7 @@
             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)
+            return f"{self.project_name}=={self.version} [requires: {parent_str}]"
         else:
             return self.render_as_root(frozen)
 
@@ -203,46 +202,42 @@
         return ReqPackage(self._obj.as_requirement(), dist=self)
 
     def as_parent_of(self, req):
-        """Return a DistPackage instance associated to a requirement
+        """
+        Return a DistPackage instance associated to a requirement. This association is necessary for reversing the
+        PackageDAG.
 
-        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
+        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}
+        return {"key": self.key, "package_name": self.project_name, "installed_version": self.version}
 
 
 class ReqPackage(Package):
-    """Wrapper class for Requirements instance
+    """
+    Wrapper class for Requirements instance
 
-      :param obj: The `Requirements` instance to wrap over
-      :param dist: optional `pkg_resources.Distribution` instance for
-                   this requirement
+    :param obj: The `Requirements` instance to wrap over
+    :param dist: optional `pkg_resources.Distribution` instance for this requirement
     """
 
-    UNKNOWN_VERSION = '?'
+    UNKNOWN_VERSION = "?"
 
     def __init__(self, obj, dist=None):
-        super(ReqPackage, self).__init__(obj)
+        super().__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
+        return ",".join(["".join(sp) for sp in specs]) if specs else None
 
     @property
     def installed_version(self):
@@ -259,14 +254,14 @@
         # 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)
+        ver_spec = self.version_spec if self.version_spec else ""
+        req_version_str = f"{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)
+            return f"{self.project_name}=={self.installed_version}"
         elif self.dist:
             return self.__class__.frozen_repr(self.dist._obj)
         else:
@@ -274,26 +269,26 @@
 
     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)
+            req_ver = self.version_spec if self.version_spec else "Any"
+            return f"{self.project_name} [required: {req_ver}, installed: {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}
+        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.
+    """
+    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,
+    The nodes and their relationships (edges) are internally stored using a map as follows,
 
     {a: [b, c],
      b: [d],
@@ -303,24 +298,19 @@
      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.
+    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
+    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}
+        m = {p: [ReqPackage(r, idx.get(r.key)) for r in p.requires()] for p in pkgs}
         return cls(m)
 
     def __init__(self, m):
@@ -335,17 +325,15 @@
         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.
+        """
+        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.
+        This method is useful if the dict representing the DAG contains different kind of objects in keys and values.
+        Use this method to look up 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]
@@ -353,27 +341,26 @@
             return None
 
     def get_children(self, node_key):
-        """Get child nodes for a node by it's key
+        """
+        Get child nodes for a node by its 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
+        """
+        Filters nodes in a graph by given parameters
 
-        If a node is included, then all it's children are also
-        included.
+        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:
@@ -385,11 +372,11 @@
         # `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])
+            include = {s.lower() for s in include}
         if exclude:
-            exclude = set([s.lower() for s in exclude])
+            exclude = {s.lower() for s in exclude}
         else:
-            exclude = set([])
+            exclude = set()
 
         # Check for mutual exclusion of show_only and exclude sets
         # after normalizing the values to lowercase
@@ -400,7 +387,7 @@
         # nodes according to `show_only` and `exclude` sets
         stack = deque()
         m = {}
-        seen = set([])
+        seen = set()
         for node in self._obj.keys():
             if node.key in exclude:
                 continue
@@ -409,8 +396,7 @@
             while True:
                 if len(stack) > 0:
                     n = stack.pop()
-                    cldn = [c for c in self._obj[n]
-                            if c.key not in exclude]
+                    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:
@@ -419,9 +405,8 @@
                             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
+                                # It means there's no root node corresponding to the child node i.e.
+                                # a dependency is missing
                                 continue
                 else:
                     break
@@ -429,26 +414,22 @@
         return self.__class__(m)
 
     def reverse(self):
-        """Reverse the DAG, or turn it upside-down
+        """
+        Reverse the DAG, or turn it upside-down.
 
-        In other words, the directions of edges of the nodes in the
-        DAG will be reversed.
+        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.
+        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
+        e.g., 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()))
+        child_keys = {r.key for r in chain.from_iterable(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
@@ -464,11 +445,10 @@
         return ReversedPackageDAG(dict(m))
 
     def sort(self):
-        """Return sorted tree in which the underlying _obj dict is an
-        OrderedDict, sorted alphabetically by the keys
+        """
+        Return sorted tree in which the underlying _obj dict is an dict, sorted alphabetically by the keys.
 
-        :returns: Instance of same class with OrderedDict
-
+        :returns: Instance of same class with dict
         """
         return self.__class__(sorted_tree(self._obj))
 
@@ -484,28 +464,23 @@
 
 
 class ReversedPackageDAG(PackageDAG):
-    """Representation of Package dependencies in the reverse
-    order.
+    """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`.
+    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`.
-
+    Typically, this object will be obtained by calling `PackageDAG.reverse`.
     """
 
     def reverse(self):
-        """Reverse the already reversed DAG to get the PackageDAG again
+        """
+        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()))
+        child_keys = {r.key for r in chain.from_iterable(self._obj.values())}
         for k, vs in self._obj.items():
             for v in vs:
                 try:
@@ -522,42 +497,41 @@
     """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
+    :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: 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()))
+    branch_keys = {r.key for r in chain.from_iterable(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 []
+    def aux(node, parent=None, indent=0, cur_chain=None):
+        cur_chain = cur_chain or []
         node_str = node.render(parent, frozen)
         if parent:
-            prefix = ' '*indent + ('- ' if use_bullets else '')
+            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))
+        children = [
+            aux(c, node, indent=indent + 2, cur_chain=cur_chain + [c.project_name])
+            for c in tree.get_children(node.key)
+            if c.project_name not in cur_chain
+        ]
+        result += list(chain.from_iterable(children))
         return result
 
-    lines = flatten([aux(p) for p in nodes])
-    print('\n'.join(lines))
+    lines = chain.from_iterable([aux(p) for p in nodes])
+    print("\n".join(lines))
 
 
 def render_json(tree, indent):
-    """Converts the tree into a flat json representation.
+    """
+    Converts the tree into a flat json representation.
 
     The json repr will be a list of hashes, each hash having 2 fields:
       - package
@@ -567,19 +541,19 @@
     :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)
+    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.
+    """
+    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
@@ -590,26 +564,25 @@
     :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()))
+    branch_keys = {r.key for r in chain.from_iterable(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]
+    def aux(node, parent=None, cur_chain=None):
+        if cur_chain is None:
+            cur_chain = [node.project_name]
 
         d = node.as_dict()
         if parent:
-            d['required_version'] = node.version_spec if node.version_spec else 'Any'
+            d["required_version"] = node.version_spec if node.version_spec else "Any"
         else:
-            d['required_version'] = d['installed_version']
+            d["required_version"] = d["installed_version"]
 
-        d['dependencies'] = [
-            aux(c, parent=node, chain=chain+[c.project_name])
+        d["dependencies"] = [
+            aux(c, parent=node, cur_chain=cur_chain + [c.project_name])
             for c in tree.get_children(node.key)
-            if c.project_name not in chain
+            if c.project_name not in cur_chain
         ]
 
         return d
@@ -617,11 +590,12 @@
     return json.dumps([aux(p) for p in nodes], indent=indent)
 
 
-def dump_graphviz(tree, output_format='dot', is_reverse=False):
+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
+    :param bool is_reverse: reverse or not
     :returns: representation of tree in the specified output format
     :rtype: str or binary representation depending on the output format
 
@@ -629,88 +603,87 @@
     try:
         from graphviz import Digraph
     except ImportError:
-        print('graphviz is not available, but necessary for the output '
-              'option. Please install it.', file=sys.stderr)
+        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)
+        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)
+        print(f"{output_format} is not a supported output format.", file=sys.stderr)
+        print(f"Supported formats are: {', '.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)
+            pkg_label = f"{pkg.project_name}\\n{pkg.version}"
             graph.node(pkg.key, label=pkg_label)
             for dep in deps:
-                edge_label = dep.version_spec or 'any'
+                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')
+                    dep_label = f"{dep.project_name}\\n(missing)"
+                    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)
+            dep_label = f"{dep.project_name}\\n{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'
+                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':
+    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')
+        return graph.pipe().decode("utf-8")
     except UnicodeDecodeError:
         return graph.pipe()
 
 
 def print_graphviz(dump_output):
-    """Dump the data generated by GraphViz to stdout.
+    """
+    Dump the data generated by GraphViz to stdout.
 
     :param dump_output: The output from dump_graphviz
     """
-    if hasattr(dump_output, 'encode'):
+    if hasattr(dump_output, "encode"):
         print(dump_output)
     else:
-        with os.fdopen(sys.stdout.fileno(), 'wb') as bytestream:
+        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.
+    """
+    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():
@@ -722,118 +695,131 @@
 
 def render_conflicts_text(conflicts):
     if conflicts:
-        print('Warning!!! Possibly conflicting dependencies found:',
-              file=sys.stderr)
+        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)
+            print(f"* {pkg}", file=sys.stderr)
             for req in conflicts[p]:
                 req_str = req.render_as_branch(False)
-                print(' - {}'.format(req_str), file=sys.stderr)
+                print(f" - {req_str}", file=sys.stderr)
 
 
 def cyclic_deps(tree):
-    """Return cyclic dependencies as list of tuples
+    """
+    Return cyclic dependencies as list of tuples
 
-    :param PackageDAG pkgs: package tree/dag
+    :param PackageDAG tree: 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()}
+    index = {p.key: {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]
+                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)
+        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)
+            print(f"* {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'
-                        ))
+    parser = argparse.ArgumentParser(description="Dependency tree of the installed python packages")
+    parser.add_argument("-v", "--version", action="version", version=f"{__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
 
 
@@ -848,8 +834,7 @@
     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)
+            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):
@@ -858,54 +843,44 @@
                 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)
+
+        main_file = inspect.getsourcefile(sys.modules[__name__])
+        with tempfile.TemporaryDirectory() as project:
+            dest = os.path.join(project, "pipdeptree")
+            shutil.copytree(os.path.dirname(main_file), dest)
+            # invoke from an empty folder to avoid cwd altering sys.path
+            env = os.environ.copy()
+            env["PYTHONPATH"] = project
+            cmd = [of_python, "-m", "pipdeptree"]
+            cmd.extend(argv)
+            return subprocess.call(cmd, cwd=project, env=env)
     return None
 
 
 def get_installed_distributions(local_only=False, user_only=False):
     try:
-        from pip._internal.metadata import get_environment
+        from pip._internal.metadata import pkg_resources
     except ImportError:
         # For backward compatibility with python ver. 2.7 and pip
-        # version 20.3.4 (latest pip version that works with python
+        # version 20.3.4 (the 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
-        )
+
+        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
+        dists = pkg_resources.Environment.from_paths(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)
+    pkgs = get_installed_distributions(local_only=args.local_only, user_only=args.user_only)
 
     tree = PackageDAG.from_pkgs(pkgs)
 
@@ -915,19 +890,19 @@
 
     # 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':
+    # are enabled (i.e. 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)
+            print("-" * 72, file=sys.stderr)
 
         cycles = cyclic_deps(tree)
         if cycles:
             render_cycles_text(cycles)
-            print('-'*72, file=sys.stderr)
+            print("-" * 72, file=sys.stderr)
 
-        if args.warn == 'fail' and (conflicts or cycles):
+        if args.warn == "fail" and (conflicts or cycles):
             return_code = 1
 
     # Reverse the tree (if applicable) before filtering, thus ensuring
@@ -935,8 +910,8 @@
     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
+    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)
@@ -946,15 +921,16 @@
     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)
+        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
 
+#
+# eric-ide modification: entry point to get one self-contained script
+#
 
 if __name__ == '__main__':
     sys.exit(main())

eric ide

mercurial