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