src/eric7/PipInterface/pipdeptree.py

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

eric ide

mercurial