src/eric7/PipInterface/pipdeptree.py

branch
eric7
changeset 9589
09218eb3ae21
parent 9218
71cf3979a6c9
child 9653
e67609152c5e
equal deleted inserted replaced
9588:268570e5b65f 9589:09218eb3ae21
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 #
5 # eric-ide modification: Copyright from file "LICENSE"
6 #
7
4 """ 8 """
5 Copyright (c) 2015 Vineet Naik (naikvin@gmail.com) 9 Copyright (c) The pipdeptree developers
6 10
7 Permission is hereby granted, free of charge, to any person obtaining 11 Permission is hereby granted, free of charge, to any person obtaining
8 a copy of this software and associated documentation files (the 12 a copy of this software and associated documentation files (the
9 "Software"), to deal in the Software without restriction, including 13 "Software"), to deal in the Software without restriction, including
10 without limitation the rights to use, copy, modify, merge, publish, 14 without limitation the rights to use, copy, modify, merge, publish,
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):
123 return FrozenRequirement.from_dist(dist) 126 return FrozenRequirement.from_dist(dist)
124 except TypeError: 127 except TypeError:
125 return FrozenRequirement.from_dist(dist, []) 128 return FrozenRequirement.from_dist(dist, [])
126 129
127 130
128 class Package(object): 131 class Package:
129 """Abstract class for wrappers around objects that pip returns. 132 """
130 133 Abstract class for wrappers around objects that pip returns. This class needs to be subclassed with implementations
131 This class needs to be subclassed with implementations for 134 for `render_as_root` and `render_as_branch` methods.
132 `render_as_root` and `render_as_branch` methods.
133
134 """ 135 """
135 136
136 def __init__(self, obj): 137 def __init__(self, obj):
137 self._obj = obj 138 self._obj = obj
138 self.project_name = obj.project_name 139 self.project_name = obj.project_name
139 self.key = obj.key 140 self.key = obj.key
140 141
141 def render_as_root(self, frozen): 142 def render_as_root(self, frozen): # noqa: U100
142 return NotImplementedError 143 return NotImplementedError
143 144
144 def render_as_branch(self, frozen): 145 def render_as_branch(self, frozen): # noqa: U100
145 return NotImplementedError 146 return NotImplementedError
146 147
147 def render(self, parent=None, frozen=False): 148 def render(self, parent=None, frozen=False):
148 if not parent: 149 if not parent:
149 return self.render_as_root(frozen) 150 return self.render_as_root(frozen)
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
462 if k.key not in child_keys: 443 if k.key not in child_keys:
463 m[k.as_requirement()] = [] 444 m[k.as_requirement()] = []
464 return ReversedPackageDAG(dict(m)) 445 return ReversedPackageDAG(dict(m))
465 446
466 def sort(self): 447 def sort(self):
467 """Return sorted tree in which the underlying _obj dict is an 448 """
468 OrderedDict, sorted alphabetically by the keys 449 Return sorted tree in which the underlying _obj dict is an dict, sorted alphabetically by the keys.
469 450
470 :returns: Instance of same class with OrderedDict 451 :returns: Instance of same class with dict
471
472 """ 452 """
473 return self.__class__(sorted_tree(self._obj)) 453 return self.__class__(sorted_tree(self._obj))
474 454
475 # Methods required by the abstract base class Mapping 455 # Methods required by the abstract base class Mapping
476 def __getitem__(self, *args): 456 def __getitem__(self, *args):
482 def __len__(self): 462 def __len__(self):
483 return len(self._obj) 463 return len(self._obj)
484 464
485 465
486 class ReversedPackageDAG(PackageDAG): 466 class ReversedPackageDAG(PackageDAG):
487 """Representation of Package dependencies in the reverse 467 """Representation of Package dependencies in the reverse order.
488 order. 468
489 469 Similar to it's super class `PackageDAG`, the underlying datastructure is a dict, but here the keys are expected to
490 Similar to it's super class `PackageDAG`, the underlying 470 be of type `ReqPackage` and each item in the values of type `DistPackage`.
491 datastructure is a dict, but here the keys are expected to be of 471
492 type `ReqPackage` and each item in the values of type 472 Typically, this object will be obtained by calling `PackageDAG.reverse`.
493 `DistPackage`.
494
495 Typically, this object will be obtained by calling
496 `PackageDAG.reverse`.
497
498 """ 473 """
499 474
500 def reverse(self): 475 def reverse(self):
501 """Reverse the already reversed DAG to get the PackageDAG again 476 """
477 Reverse the already reversed DAG to get the PackageDAG again
502 478
503 :returns: reverse of the reversed DAG 479 :returns: reverse of the reversed DAG
504 :rtype: PackageDAG 480 :rtype: PackageDAG
505
506 """ 481 """
507 m = defaultdict(list) 482 m = defaultdict(list)
508 child_keys = set(r.key for r in flatten(self._obj.values())) 483 child_keys = {r.key for r in chain.from_iterable(self._obj.values())}
509 for k, vs in self._obj.items(): 484 for k, vs in self._obj.items():
510 for v in vs: 485 for v in vs:
511 try: 486 try:
512 node = [p for p in m.keys() if p.key == v.key][0] 487 node = [p for p in m.keys() if p.key == v.key][0]
513 except IndexError: 488 except IndexError:
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())

eric ide

mercurial