diff options
Diffstat (limited to 'venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/database.py')
-rw-r--r-- | venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/database.py | 1336 |
1 files changed, 0 insertions, 1336 deletions
diff --git a/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/database.py b/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/database.py deleted file mode 100644 index 54483e1..0000000 --- a/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/database.py +++ /dev/null | |||
@@ -1,1336 +0,0 @@ | |||
1 | # -*- coding: utf-8 -*- | ||
2 | # | ||
3 | # Copyright (C) 2012-2017 The Python Software Foundation. | ||
4 | # See LICENSE.txt and CONTRIBUTORS.txt. | ||
5 | # | ||
6 | """PEP 376 implementation.""" | ||
7 | |||
8 | from __future__ import unicode_literals | ||
9 | |||
10 | import base64 | ||
11 | import codecs | ||
12 | import contextlib | ||
13 | import hashlib | ||
14 | import logging | ||
15 | import os | ||
16 | import posixpath | ||
17 | import sys | ||
18 | import zipimport | ||
19 | |||
20 | from . import DistlibException, resources | ||
21 | from .compat import StringIO | ||
22 | from .version import get_scheme, UnsupportedVersionError | ||
23 | from .metadata import Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME | ||
24 | from .util import (parse_requirement, cached_property, parse_name_and_version, | ||
25 | read_exports, write_exports, CSVReader, CSVWriter) | ||
26 | |||
27 | |||
28 | __all__ = ['Distribution', 'BaseInstalledDistribution', | ||
29 | 'InstalledDistribution', 'EggInfoDistribution', | ||
30 | 'DistributionPath'] | ||
31 | |||
32 | |||
33 | logger = logging.getLogger(__name__) | ||
34 | |||
35 | EXPORTS_FILENAME = 'pydist-exports.json' | ||
36 | COMMANDS_FILENAME = 'pydist-commands.json' | ||
37 | |||
38 | DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED', | ||
39 | 'RESOURCES', EXPORTS_FILENAME, 'SHARED') | ||
40 | |||
41 | DISTINFO_EXT = '.dist-info' | ||
42 | |||
43 | |||
44 | class _Cache(object): | ||
45 | """ | ||
46 | A simple cache mapping names and .dist-info paths to distributions | ||
47 | """ | ||
48 | def __init__(self): | ||
49 | """ | ||
50 | Initialise an instance. There is normally one for each DistributionPath. | ||
51 | """ | ||
52 | self.name = {} | ||
53 | self.path = {} | ||
54 | self.generated = False | ||
55 | |||
56 | def clear(self): | ||
57 | """ | ||
58 | Clear the cache, setting it to its initial state. | ||
59 | """ | ||
60 | self.name.clear() | ||
61 | self.path.clear() | ||
62 | self.generated = False | ||
63 | |||
64 | def add(self, dist): | ||
65 | """ | ||
66 | Add a distribution to the cache. | ||
67 | :param dist: The distribution to add. | ||
68 | """ | ||
69 | if dist.path not in self.path: | ||
70 | self.path[dist.path] = dist | ||
71 | self.name.setdefault(dist.key, []).append(dist) | ||
72 | |||
73 | |||
74 | class DistributionPath(object): | ||
75 | """ | ||
76 | Represents a set of distributions installed on a path (typically sys.path). | ||
77 | """ | ||
78 | def __init__(self, path=None, include_egg=False): | ||
79 | """ | ||
80 | Create an instance from a path, optionally including legacy (distutils/ | ||
81 | setuptools/distribute) distributions. | ||
82 | :param path: The path to use, as a list of directories. If not specified, | ||
83 | sys.path is used. | ||
84 | :param include_egg: If True, this instance will look for and return legacy | ||
85 | distributions as well as those based on PEP 376. | ||
86 | """ | ||
87 | if path is None: | ||
88 | path = sys.path | ||
89 | self.path = path | ||
90 | self._include_dist = True | ||
91 | self._include_egg = include_egg | ||
92 | |||
93 | self._cache = _Cache() | ||
94 | self._cache_egg = _Cache() | ||
95 | self._cache_enabled = True | ||
96 | self._scheme = get_scheme('default') | ||
97 | |||
98 | def _get_cache_enabled(self): | ||
99 | return self._cache_enabled | ||
100 | |||
101 | def _set_cache_enabled(self, value): | ||
102 | self._cache_enabled = value | ||
103 | |||
104 | cache_enabled = property(_get_cache_enabled, _set_cache_enabled) | ||
105 | |||
106 | def clear_cache(self): | ||
107 | """ | ||
108 | Clears the internal cache. | ||
109 | """ | ||
110 | self._cache.clear() | ||
111 | self._cache_egg.clear() | ||
112 | |||
113 | |||
114 | def _yield_distributions(self): | ||
115 | """ | ||
116 | Yield .dist-info and/or .egg(-info) distributions. | ||
117 | """ | ||
118 | # We need to check if we've seen some resources already, because on | ||
119 | # some Linux systems (e.g. some Debian/Ubuntu variants) there are | ||
120 | # symlinks which alias other files in the environment. | ||
121 | seen = set() | ||
122 | for path in self.path: | ||
123 | finder = resources.finder_for_path(path) | ||
124 | if finder is None: | ||
125 | continue | ||
126 | r = finder.find('') | ||
127 | if not r or not r.is_container: | ||
128 | continue | ||
129 | rset = sorted(r.resources) | ||
130 | for entry in rset: | ||
131 | r = finder.find(entry) | ||
132 | if not r or r.path in seen: | ||
133 | continue | ||
134 | if self._include_dist and entry.endswith(DISTINFO_EXT): | ||
135 | possible_filenames = [METADATA_FILENAME, WHEEL_METADATA_FILENAME] | ||
136 | for metadata_filename in possible_filenames: | ||
137 | metadata_path = posixpath.join(entry, metadata_filename) | ||
138 | pydist = finder.find(metadata_path) | ||
139 | if pydist: | ||
140 | break | ||
141 | else: | ||
142 | continue | ||
143 | |||
144 | with contextlib.closing(pydist.as_stream()) as stream: | ||
145 | metadata = Metadata(fileobj=stream, scheme='legacy') | ||
146 | logger.debug('Found %s', r.path) | ||
147 | seen.add(r.path) | ||
148 | yield new_dist_class(r.path, metadata=metadata, | ||
149 | env=self) | ||
150 | elif self._include_egg and entry.endswith(('.egg-info', | ||
151 | '.egg')): | ||
152 | logger.debug('Found %s', r.path) | ||
153 | seen.add(r.path) | ||
154 | yield old_dist_class(r.path, self) | ||
155 | |||
156 | def _generate_cache(self): | ||
157 | """ | ||
158 | Scan the path for distributions and populate the cache with | ||
159 | those that are found. | ||
160 | """ | ||
161 | gen_dist = not self._cache.generated | ||
162 | gen_egg = self._include_egg and not self._cache_egg.generated | ||
163 | if gen_dist or gen_egg: | ||
164 | for dist in self._yield_distributions(): | ||
165 | if isinstance(dist, InstalledDistribution): | ||
166 | self._cache.add(dist) | ||
167 | else: | ||
168 | self._cache_egg.add(dist) | ||
169 | |||
170 | if gen_dist: | ||
171 | self._cache.generated = True | ||
172 | if gen_egg: | ||
173 | self._cache_egg.generated = True | ||
174 | |||
175 | @classmethod | ||
176 | def distinfo_dirname(cls, name, version): | ||
177 | """ | ||
178 | The *name* and *version* parameters are converted into their | ||
179 | filename-escaped form, i.e. any ``'-'`` characters are replaced | ||
180 | with ``'_'`` other than the one in ``'dist-info'`` and the one | ||
181 | separating the name from the version number. | ||
182 | |||
183 | :parameter name: is converted to a standard distribution name by replacing | ||
184 | any runs of non- alphanumeric characters with a single | ||
185 | ``'-'``. | ||
186 | :type name: string | ||
187 | :parameter version: is converted to a standard version string. Spaces | ||
188 | become dots, and all other non-alphanumeric characters | ||
189 | (except dots) become dashes, with runs of multiple | ||
190 | dashes condensed to a single dash. | ||
191 | :type version: string | ||
192 | :returns: directory name | ||
193 | :rtype: string""" | ||
194 | name = name.replace('-', '_') | ||
195 | return '-'.join([name, version]) + DISTINFO_EXT | ||
196 | |||
197 | def get_distributions(self): | ||
198 | """ | ||
199 | Provides an iterator that looks for distributions and returns | ||
200 | :class:`InstalledDistribution` or | ||
201 | :class:`EggInfoDistribution` instances for each one of them. | ||
202 | |||
203 | :rtype: iterator of :class:`InstalledDistribution` and | ||
204 | :class:`EggInfoDistribution` instances | ||
205 | """ | ||
206 | if not self._cache_enabled: | ||
207 | for dist in self._yield_distributions(): | ||
208 | yield dist | ||
209 | else: | ||
210 | self._generate_cache() | ||
211 | |||
212 | for dist in self._cache.path.values(): | ||
213 | yield dist | ||
214 | |||
215 | if self._include_egg: | ||
216 | for dist in self._cache_egg.path.values(): | ||
217 | yield dist | ||
218 | |||
219 | def get_distribution(self, name): | ||
220 | """ | ||
221 | Looks for a named distribution on the path. | ||
222 | |||
223 | This function only returns the first result found, as no more than one | ||
224 | value is expected. If nothing is found, ``None`` is returned. | ||
225 | |||
226 | :rtype: :class:`InstalledDistribution`, :class:`EggInfoDistribution` | ||
227 | or ``None`` | ||
228 | """ | ||
229 | result = None | ||
230 | name = name.lower() | ||
231 | if not self._cache_enabled: | ||
232 | for dist in self._yield_distributions(): | ||
233 | if dist.key == name: | ||
234 | result = dist | ||
235 | break | ||
236 | else: | ||
237 | self._generate_cache() | ||
238 | |||
239 | if name in self._cache.name: | ||
240 | result = self._cache.name[name][0] | ||
241 | elif self._include_egg and name in self._cache_egg.name: | ||
242 | result = self._cache_egg.name[name][0] | ||
243 | return result | ||
244 | |||
245 | def provides_distribution(self, name, version=None): | ||
246 | """ | ||
247 | Iterates over all distributions to find which distributions provide *name*. | ||
248 | If a *version* is provided, it will be used to filter the results. | ||
249 | |||
250 | This function only returns the first result found, since no more than | ||
251 | one values are expected. If the directory is not found, returns ``None``. | ||
252 | |||
253 | :parameter version: a version specifier that indicates the version | ||
254 | required, conforming to the format in ``PEP-345`` | ||
255 | |||
256 | :type name: string | ||
257 | :type version: string | ||
258 | """ | ||
259 | matcher = None | ||
260 | if version is not None: | ||
261 | try: | ||
262 | matcher = self._scheme.matcher('%s (%s)' % (name, version)) | ||
263 | except ValueError: | ||
264 | raise DistlibException('invalid name or version: %r, %r' % | ||
265 | (name, version)) | ||
266 | |||
267 | for dist in self.get_distributions(): | ||
268 | # We hit a problem on Travis where enum34 was installed and doesn't | ||
269 | # have a provides attribute ... | ||
270 | if not hasattr(dist, 'provides'): | ||
271 | logger.debug('No "provides": %s', dist) | ||
272 | else: | ||
273 | provided = dist.provides | ||
274 | |||
275 | for p in provided: | ||
276 | p_name, p_ver = parse_name_and_version(p) | ||
277 | if matcher is None: | ||
278 | if p_name == name: | ||
279 | yield dist | ||
280 | break | ||
281 | else: | ||
282 | if p_name == name and matcher.match(p_ver): | ||
283 | yield dist | ||
284 | break | ||
285 | |||
286 | def get_file_path(self, name, relative_path): | ||
287 | """ | ||
288 | Return the path to a resource file. | ||
289 | """ | ||
290 | dist = self.get_distribution(name) | ||
291 | if dist is None: | ||
292 | raise LookupError('no distribution named %r found' % name) | ||
293 | return dist.get_resource_path(relative_path) | ||
294 | |||
295 | def get_exported_entries(self, category, name=None): | ||
296 | """ | ||
297 | Return all of the exported entries in a particular category. | ||
298 | |||
299 | :param category: The category to search for entries. | ||
300 | :param name: If specified, only entries with that name are returned. | ||
301 | """ | ||
302 | for dist in self.get_distributions(): | ||
303 | r = dist.exports | ||
304 | if category in r: | ||
305 | d = r[category] | ||
306 | if name is not None: | ||
307 | if name in d: | ||
308 | yield d[name] | ||
309 | else: | ||
310 | for v in d.values(): | ||
311 | yield v | ||
312 | |||
313 | |||
314 | class Distribution(object): | ||
315 | """ | ||
316 | A base class for distributions, whether installed or from indexes. | ||
317 | Either way, it must have some metadata, so that's all that's needed | ||
318 | for construction. | ||
319 | """ | ||
320 | |||
321 | build_time_dependency = False | ||
322 | """ | ||
323 | Set to True if it's known to be only a build-time dependency (i.e. | ||
324 | not needed after installation). | ||
325 | """ | ||
326 | |||
327 | requested = False | ||
328 | """A boolean that indicates whether the ``REQUESTED`` metadata file is | ||
329 | present (in other words, whether the package was installed by user | ||
330 | request or it was installed as a dependency).""" | ||
331 | |||
332 | def __init__(self, metadata): | ||
333 | """ | ||
334 | Initialise an instance. | ||
335 | :param metadata: The instance of :class:`Metadata` describing this | ||
336 | distribution. | ||
337 | """ | ||
338 | self.metadata = metadata | ||
339 | self.name = metadata.name | ||
340 | self.key = self.name.lower() # for case-insensitive comparisons | ||
341 | self.version = metadata.version | ||
342 | self.locator = None | ||
343 | self.digest = None | ||
344 | self.extras = None # additional features requested | ||
345 | self.context = None # environment marker overrides | ||
346 | self.download_urls = set() | ||
347 | self.digests = {} | ||
348 | |||
349 | @property | ||
350 | def source_url(self): | ||
351 | """ | ||
352 | The source archive download URL for this distribution. | ||
353 | """ | ||
354 | return self.metadata.source_url | ||
355 | |||
356 | download_url = source_url # Backward compatibility | ||
357 | |||
358 | @property | ||
359 | def name_and_version(self): | ||
360 | """ | ||
361 | A utility property which displays the name and version in parentheses. | ||
362 | """ | ||
363 | return '%s (%s)' % (self.name, self.version) | ||
364 | |||
365 | @property | ||
366 | def provides(self): | ||
367 | """ | ||
368 | A set of distribution names and versions provided by this distribution. | ||
369 | :return: A set of "name (version)" strings. | ||
370 | """ | ||
371 | plist = self.metadata.provides | ||
372 | s = '%s (%s)' % (self.name, self.version) | ||
373 | if s not in plist: | ||
374 | plist.append(s) | ||
375 | return plist | ||
376 | |||
377 | def _get_requirements(self, req_attr): | ||
378 | md = self.metadata | ||
379 | logger.debug('Getting requirements from metadata %r', md.todict()) | ||
380 | reqts = getattr(md, req_attr) | ||
381 | return set(md.get_requirements(reqts, extras=self.extras, | ||
382 | env=self.context)) | ||
383 | |||
384 | @property | ||
385 | def run_requires(self): | ||
386 | return self._get_requirements('run_requires') | ||
387 | |||
388 | @property | ||
389 | def meta_requires(self): | ||
390 | return self._get_requirements('meta_requires') | ||
391 | |||
392 | @property | ||
393 | def build_requires(self): | ||
394 | return self._get_requirements('build_requires') | ||
395 | |||
396 | @property | ||
397 | def test_requires(self): | ||
398 | return self._get_requirements('test_requires') | ||
399 | |||
400 | @property | ||
401 | def dev_requires(self): | ||
402 | return self._get_requirements('dev_requires') | ||
403 | |||
404 | def matches_requirement(self, req): | ||
405 | """ | ||
406 | Say if this instance matches (fulfills) a requirement. | ||
407 | :param req: The requirement to match. | ||
408 | :rtype req: str | ||
409 | :return: True if it matches, else False. | ||
410 | """ | ||
411 | # Requirement may contain extras - parse to lose those | ||
412 | # from what's passed to the matcher | ||
413 | r = parse_requirement(req) | ||
414 | scheme = get_scheme(self.metadata.scheme) | ||
415 | try: | ||
416 | matcher = scheme.matcher(r.requirement) | ||
417 | except UnsupportedVersionError: | ||
418 | # XXX compat-mode if cannot read the version | ||
419 | logger.warning('could not read version %r - using name only', | ||
420 | req) | ||
421 | name = req.split()[0] | ||
422 | matcher = scheme.matcher(name) | ||
423 | |||
424 | name = matcher.key # case-insensitive | ||
425 | |||
426 | result = False | ||
427 | for p in self.provides: | ||
428 | p_name, p_ver = parse_name_and_version(p) | ||
429 | if p_name != name: | ||
430 | continue | ||
431 | try: | ||
432 | result = matcher.match(p_ver) | ||
433 | break | ||
434 | except UnsupportedVersionError: | ||
435 | pass | ||
436 | return result | ||
437 | |||
438 | def __repr__(self): | ||
439 | """ | ||
440 | Return a textual representation of this instance, | ||
441 | """ | ||
442 | if self.source_url: | ||
443 | suffix = ' [%s]' % self.source_url | ||
444 | else: | ||
445 | suffix = '' | ||
446 | return '<Distribution %s (%s)%s>' % (self.name, self.version, suffix) | ||
447 | |||
448 | def __eq__(self, other): | ||
449 | """ | ||
450 | See if this distribution is the same as another. | ||
451 | :param other: The distribution to compare with. To be equal to one | ||
452 | another. distributions must have the same type, name, | ||
453 | version and source_url. | ||
454 | :return: True if it is the same, else False. | ||
455 | """ | ||
456 | if type(other) is not type(self): | ||
457 | result = False | ||
458 | else: | ||
459 | result = (self.name == other.name and | ||
460 | self.version == other.version and | ||
461 | self.source_url == other.source_url) | ||
462 | return result | ||
463 | |||
464 | def __hash__(self): | ||
465 | """ | ||
466 | Compute hash in a way which matches the equality test. | ||
467 | """ | ||
468 | return hash(self.name) + hash(self.version) + hash(self.source_url) | ||
469 | |||
470 | |||
471 | class BaseInstalledDistribution(Distribution): | ||
472 | """ | ||
473 | This is the base class for installed distributions (whether PEP 376 or | ||
474 | legacy). | ||
475 | """ | ||
476 | |||
477 | hasher = None | ||
478 | |||
479 | def __init__(self, metadata, path, env=None): | ||
480 | """ | ||
481 | Initialise an instance. | ||
482 | :param metadata: An instance of :class:`Metadata` which describes the | ||
483 | distribution. This will normally have been initialised | ||
484 | from a metadata file in the ``path``. | ||
485 | :param path: The path of the ``.dist-info`` or ``.egg-info`` | ||
486 | directory for the distribution. | ||
487 | :param env: This is normally the :class:`DistributionPath` | ||
488 | instance where this distribution was found. | ||
489 | """ | ||
490 | super(BaseInstalledDistribution, self).__init__(metadata) | ||
491 | self.path = path | ||
492 | self.dist_path = env | ||
493 | |||
494 | def get_hash(self, data, hasher=None): | ||
495 | """ | ||
496 | Get the hash of some data, using a particular hash algorithm, if | ||
497 | specified. | ||
498 | |||
499 | :param data: The data to be hashed. | ||
500 | :type data: bytes | ||
501 | :param hasher: The name of a hash implementation, supported by hashlib, | ||
502 | or ``None``. Examples of valid values are ``'sha1'``, | ||
503 | ``'sha224'``, ``'sha384'``, '``sha256'``, ``'md5'`` and | ||
504 | ``'sha512'``. If no hasher is specified, the ``hasher`` | ||
505 | attribute of the :class:`InstalledDistribution` instance | ||
506 | is used. If the hasher is determined to be ``None``, MD5 | ||
507 | is used as the hashing algorithm. | ||
508 | :returns: The hash of the data. If a hasher was explicitly specified, | ||
509 | the returned hash will be prefixed with the specified hasher | ||
510 | followed by '='. | ||
511 | :rtype: str | ||
512 | """ | ||
513 | if hasher is None: | ||
514 | hasher = self.hasher | ||
515 | if hasher is None: | ||
516 | hasher = hashlib.md5 | ||
517 | prefix = '' | ||
518 | else: | ||
519 | hasher = getattr(hashlib, hasher) | ||
520 | prefix = '%s=' % self.hasher | ||
521 | digest = hasher(data).digest() | ||
522 | digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii') | ||
523 | return '%s%s' % (prefix, digest) | ||
524 | |||
525 | |||
526 | class InstalledDistribution(BaseInstalledDistribution): | ||
527 | """ | ||
528 | Created with the *path* of the ``.dist-info`` directory provided to the | ||
529 | constructor. It reads the metadata contained in ``pydist.json`` when it is | ||
530 | instantiated., or uses a passed in Metadata instance (useful for when | ||
531 | dry-run mode is being used). | ||
532 | """ | ||
533 | |||
534 | hasher = 'sha256' | ||
535 | |||
536 | def __init__(self, path, metadata=None, env=None): | ||
537 | self.modules = [] | ||
538 | self.finder = finder = resources.finder_for_path(path) | ||
539 | if finder is None: | ||
540 | raise ValueError('finder unavailable for %s' % path) | ||
541 | if env and env._cache_enabled and path in env._cache.path: | ||
542 | metadata = env._cache.path[path].metadata | ||
543 | elif metadata is None: | ||
544 | r = finder.find(METADATA_FILENAME) | ||
545 | # Temporary - for Wheel 0.23 support | ||
546 | if r is None: | ||
547 | r = finder.find(WHEEL_METADATA_FILENAME) | ||
548 | # Temporary - for legacy support | ||
549 | if r is None: | ||
550 | r = finder.find('METADATA') | ||
551 | if r is None: | ||
552 | raise ValueError('no %s found in %s' % (METADATA_FILENAME, | ||
553 | path)) | ||
554 | with contextlib.closing(r.as_stream()) as stream: | ||
555 | metadata = Metadata(fileobj=stream, scheme='legacy') | ||
556 | |||
557 | super(InstalledDistribution, self).__init__(metadata, path, env) | ||
558 | |||
559 | if env and env._cache_enabled: | ||
560 | env._cache.add(self) | ||
561 | |||
562 | r = finder.find('REQUESTED') | ||
563 | self.requested = r is not None | ||
564 | p = os.path.join(path, 'top_level.txt') | ||
565 | if os.path.exists(p): | ||
566 | with open(p, 'rb') as f: | ||
567 | data = f.read() | ||
568 | self.modules = data.splitlines() | ||
569 | |||
570 | def __repr__(self): | ||
571 | return '<InstalledDistribution %r %s at %r>' % ( | ||
572 | self.name, self.version, self.path) | ||
573 | |||
574 | def __str__(self): | ||
575 | return "%s %s" % (self.name, self.version) | ||
576 | |||
577 | def _get_records(self): | ||
578 | """ | ||
579 | Get the list of installed files for the distribution | ||
580 | :return: A list of tuples of path, hash and size. Note that hash and | ||
581 | size might be ``None`` for some entries. The path is exactly | ||
582 | as stored in the file (which is as in PEP 376). | ||
583 | """ | ||
584 | results = [] | ||
585 | r = self.get_distinfo_resource('RECORD') | ||
586 | with contextlib.closing(r.as_stream()) as stream: | ||
587 | with CSVReader(stream=stream) as record_reader: | ||
588 | # Base location is parent dir of .dist-info dir | ||
589 | #base_location = os.path.dirname(self.path) | ||
590 | #base_location = os.path.abspath(base_location) | ||
591 | for row in record_reader: | ||
592 | missing = [None for i in range(len(row), 3)] | ||
593 | path, checksum, size = row + missing | ||
594 | #if not os.path.isabs(path): | ||
595 | # path = path.replace('/', os.sep) | ||
596 | # path = os.path.join(base_location, path) | ||
597 | results.append((path, checksum, size)) | ||
598 | return results | ||
599 | |||
600 | @cached_property | ||
601 | def exports(self): | ||
602 | """ | ||
603 | Return the information exported by this distribution. | ||
604 | :return: A dictionary of exports, mapping an export category to a dict | ||
605 | of :class:`ExportEntry` instances describing the individual | ||
606 | export entries, and keyed by name. | ||
607 | """ | ||
608 | result = {} | ||
609 | r = self.get_distinfo_resource(EXPORTS_FILENAME) | ||
610 | if r: | ||
611 | result = self.read_exports() | ||
612 | return result | ||
613 | |||
614 | def read_exports(self): | ||
615 | """ | ||
616 | Read exports data from a file in .ini format. | ||
617 | |||
618 | :return: A dictionary of exports, mapping an export category to a list | ||
619 | of :class:`ExportEntry` instances describing the individual | ||
620 | export entries. | ||
621 | """ | ||
622 | result = {} | ||
623 | r = self.get_distinfo_resource(EXPORTS_FILENAME) | ||
624 | if r: | ||
625 | with contextlib.closing(r.as_stream()) as stream: | ||
626 | result = read_exports(stream) | ||
627 | return result | ||
628 | |||
629 | def write_exports(self, exports): | ||
630 | """ | ||
631 | Write a dictionary of exports to a file in .ini format. | ||
632 | :param exports: A dictionary of exports, mapping an export category to | ||
633 | a list of :class:`ExportEntry` instances describing the | ||
634 | individual export entries. | ||
635 | """ | ||
636 | rf = self.get_distinfo_file(EXPORTS_FILENAME) | ||
637 | with open(rf, 'w') as f: | ||
638 | write_exports(exports, f) | ||
639 | |||
640 | def get_resource_path(self, relative_path): | ||
641 | """ | ||
642 | NOTE: This API may change in the future. | ||
643 | |||
644 | Return the absolute path to a resource file with the given relative | ||
645 | path. | ||
646 | |||
647 | :param relative_path: The path, relative to .dist-info, of the resource | ||
648 | of interest. | ||
649 | :return: The absolute path where the resource is to be found. | ||
650 | """ | ||
651 | r = self.get_distinfo_resource('RESOURCES') | ||
652 | with contextlib.closing(r.as_stream()) as stream: | ||
653 | with CSVReader(stream=stream) as resources_reader: | ||
654 | for relative, destination in resources_reader: | ||
655 | if relative == relative_path: | ||
656 | return destination | ||
657 | raise KeyError('no resource file with relative path %r ' | ||
658 | 'is installed' % relative_path) | ||
659 | |||
660 | def list_installed_files(self): | ||
661 | """ | ||
662 | Iterates over the ``RECORD`` entries and returns a tuple | ||
663 | ``(path, hash, size)`` for each line. | ||
664 | |||
665 | :returns: iterator of (path, hash, size) | ||
666 | """ | ||
667 | for result in self._get_records(): | ||
668 | yield result | ||
669 | |||
670 | def write_installed_files(self, paths, prefix, dry_run=False): | ||
671 | """ | ||
672 | Writes the ``RECORD`` file, using the ``paths`` iterable passed in. Any | ||
673 | existing ``RECORD`` file is silently overwritten. | ||
674 | |||
675 | prefix is used to determine when to write absolute paths. | ||
676 | """ | ||
677 | prefix = os.path.join(prefix, '') | ||
678 | base = os.path.dirname(self.path) | ||
679 | base_under_prefix = base.startswith(prefix) | ||
680 | base = os.path.join(base, '') | ||
681 | record_path = self.get_distinfo_file('RECORD') | ||
682 | logger.info('creating %s', record_path) | ||
683 | if dry_run: | ||
684 | return None | ||
685 | with CSVWriter(record_path) as writer: | ||
686 | for path in paths: | ||
687 | if os.path.isdir(path) or path.endswith(('.pyc', '.pyo')): | ||
688 | # do not put size and hash, as in PEP-376 | ||
689 | hash_value = size = '' | ||
690 | else: | ||
691 | size = '%d' % os.path.getsize(path) | ||
692 | with open(path, 'rb') as fp: | ||
693 | hash_value = self.get_hash(fp.read()) | ||
694 | if path.startswith(base) or (base_under_prefix and | ||
695 | path.startswith(prefix)): | ||
696 | path = os.path.relpath(path, base) | ||
697 | writer.writerow((path, hash_value, size)) | ||
698 | |||
699 | # add the RECORD file itself | ||
700 | if record_path.startswith(base): | ||
701 | record_path = os.path.relpath(record_path, base) | ||
702 | writer.writerow((record_path, '', '')) | ||
703 | return record_path | ||
704 | |||
705 | def check_installed_files(self): | ||
706 | """ | ||
707 | Checks that the hashes and sizes of the files in ``RECORD`` are | ||
708 | matched by the files themselves. Returns a (possibly empty) list of | ||
709 | mismatches. Each entry in the mismatch list will be a tuple consisting | ||
710 | of the path, 'exists', 'size' or 'hash' according to what didn't match | ||
711 | (existence is checked first, then size, then hash), the expected | ||
712 | value and the actual value. | ||
713 | """ | ||
714 | mismatches = [] | ||
715 | base = os.path.dirname(self.path) | ||
716 | record_path = self.get_distinfo_file('RECORD') | ||
717 | for path, hash_value, size in self.list_installed_files(): | ||
718 | if not os.path.isabs(path): | ||
719 | path = os.path.join(base, path) | ||
720 | if path == record_path: | ||
721 | continue | ||
722 | if not os.path.exists(path): | ||
723 | mismatches.append((path, 'exists', True, False)) | ||
724 | elif os.path.isfile(path): | ||
725 | actual_size = str(os.path.getsize(path)) | ||
726 | if size and actual_size != size: | ||
727 | mismatches.append((path, 'size', size, actual_size)) | ||
728 | elif hash_value: | ||
729 | if '=' in hash_value: | ||
730 | hasher = hash_value.split('=', 1)[0] | ||
731 | else: | ||
732 | hasher = None | ||
733 | |||
734 | with open(path, 'rb') as f: | ||
735 | actual_hash = self.get_hash(f.read(), hasher) | ||
736 | if actual_hash != hash_value: | ||
737 | mismatches.append((path, 'hash', hash_value, actual_hash)) | ||
738 | return mismatches | ||
739 | |||
740 | @cached_property | ||
741 | def shared_locations(self): | ||
742 | """ | ||
743 | A dictionary of shared locations whose keys are in the set 'prefix', | ||
744 | 'purelib', 'platlib', 'scripts', 'headers', 'data' and 'namespace'. | ||
745 | The corresponding value is the absolute path of that category for | ||
746 | this distribution, and takes into account any paths selected by the | ||
747 | user at installation time (e.g. via command-line arguments). In the | ||
748 | case of the 'namespace' key, this would be a list of absolute paths | ||
749 | for the roots of namespace packages in this distribution. | ||
750 | |||
751 | The first time this property is accessed, the relevant information is | ||
752 | read from the SHARED file in the .dist-info directory. | ||
753 | """ | ||
754 | result = {} | ||
755 | shared_path = os.path.join(self.path, 'SHARED') | ||
756 | if os.path.isfile(shared_path): | ||
757 | with codecs.open(shared_path, 'r', encoding='utf-8') as f: | ||
758 | lines = f.read().splitlines() | ||
759 | for line in lines: | ||
760 | key, value = line.split('=', 1) | ||
761 | if key == 'namespace': | ||
762 | result.setdefault(key, []).append(value) | ||
763 | else: | ||
764 | result[key] = value | ||
765 | return result | ||
766 | |||
767 | def write_shared_locations(self, paths, dry_run=False): | ||
768 | """ | ||
769 | Write shared location information to the SHARED file in .dist-info. | ||
770 | :param paths: A dictionary as described in the documentation for | ||
771 | :meth:`shared_locations`. | ||
772 | :param dry_run: If True, the action is logged but no file is actually | ||
773 | written. | ||
774 | :return: The path of the file written to. | ||
775 | """ | ||
776 | shared_path = os.path.join(self.path, 'SHARED') | ||
777 | logger.info('creating %s', shared_path) | ||
778 | if dry_run: | ||
779 | return None | ||
780 | lines = [] | ||
781 | for key in ('prefix', 'lib', 'headers', 'scripts', 'data'): | ||
782 | path = paths[key] | ||
783 | if os.path.isdir(paths[key]): | ||
784 | lines.append('%s=%s' % (key, path)) | ||
785 | for ns in paths.get('namespace', ()): | ||
786 | lines.append('namespace=%s' % ns) | ||
787 | |||
788 | with codecs.open(shared_path, 'w', encoding='utf-8') as f: | ||
789 | f.write('\n'.join(lines)) | ||
790 | return shared_path | ||
791 | |||
792 | def get_distinfo_resource(self, path): | ||
793 | if path not in DIST_FILES: | ||
794 | raise DistlibException('invalid path for a dist-info file: ' | ||
795 | '%r at %r' % (path, self.path)) | ||
796 | finder = resources.finder_for_path(self.path) | ||
797 | if finder is None: | ||
798 | raise DistlibException('Unable to get a finder for %s' % self.path) | ||
799 | return finder.find(path) | ||
800 | |||
801 | def get_distinfo_file(self, path): | ||
802 | """ | ||
803 | Returns a path located under the ``.dist-info`` directory. Returns a | ||
804 | string representing the path. | ||
805 | |||
806 | :parameter path: a ``'/'``-separated path relative to the | ||
807 | ``.dist-info`` directory or an absolute path; | ||
808 | If *path* is an absolute path and doesn't start | ||
809 | with the ``.dist-info`` directory path, | ||
810 | a :class:`DistlibException` is raised | ||
811 | :type path: str | ||
812 | :rtype: str | ||
813 | """ | ||
814 | # Check if it is an absolute path # XXX use relpath, add tests | ||
815 | if path.find(os.sep) >= 0: | ||
816 | # it's an absolute path? | ||
817 | distinfo_dirname, path = path.split(os.sep)[-2:] | ||
818 | if distinfo_dirname != self.path.split(os.sep)[-1]: | ||
819 | raise DistlibException( | ||
820 | 'dist-info file %r does not belong to the %r %s ' | ||
821 | 'distribution' % (path, self.name, self.version)) | ||
822 | |||
823 | # The file must be relative | ||
824 | if path not in DIST_FILES: | ||
825 | raise DistlibException('invalid path for a dist-info file: ' | ||
826 | '%r at %r' % (path, self.path)) | ||
827 | |||
828 | return os.path.join(self.path, path) | ||
829 | |||
830 | def list_distinfo_files(self): | ||
831 | """ | ||
832 | Iterates over the ``RECORD`` entries and returns paths for each line if | ||
833 | the path is pointing to a file located in the ``.dist-info`` directory | ||
834 | or one of its subdirectories. | ||
835 | |||
836 | :returns: iterator of paths | ||
837 | """ | ||
838 | base = os.path.dirname(self.path) | ||
839 | for path, checksum, size in self._get_records(): | ||
840 | # XXX add separator or use real relpath algo | ||
841 | if not os.path.isabs(path): | ||
842 | path = os.path.join(base, path) | ||
843 | if path.startswith(self.path): | ||
844 | yield path | ||
845 | |||
846 | def __eq__(self, other): | ||
847 | return (isinstance(other, InstalledDistribution) and | ||
848 | self.path == other.path) | ||
849 | |||
850 | # See http://docs.python.org/reference/datamodel#object.__hash__ | ||
851 | __hash__ = object.__hash__ | ||
852 | |||
853 | |||
854 | class EggInfoDistribution(BaseInstalledDistribution): | ||
855 | """Created with the *path* of the ``.egg-info`` directory or file provided | ||
856 | to the constructor. It reads the metadata contained in the file itself, or | ||
857 | if the given path happens to be a directory, the metadata is read from the | ||
858 | file ``PKG-INFO`` under that directory.""" | ||
859 | |||
860 | requested = True # as we have no way of knowing, assume it was | ||
861 | shared_locations = {} | ||
862 | |||
863 | def __init__(self, path, env=None): | ||
864 | def set_name_and_version(s, n, v): | ||
865 | s.name = n | ||
866 | s.key = n.lower() # for case-insensitive comparisons | ||
867 | s.version = v | ||
868 | |||
869 | self.path = path | ||
870 | self.dist_path = env | ||
871 | if env and env._cache_enabled and path in env._cache_egg.path: | ||
872 | metadata = env._cache_egg.path[path].metadata | ||
873 | set_name_and_version(self, metadata.name, metadata.version) | ||
874 | else: | ||
875 | metadata = self._get_metadata(path) | ||
876 | |||
877 | # Need to be set before caching | ||
878 | set_name_and_version(self, metadata.name, metadata.version) | ||
879 | |||
880 | if env and env._cache_enabled: | ||
881 | env._cache_egg.add(self) | ||
882 | super(EggInfoDistribution, self).__init__(metadata, path, env) | ||
883 | |||
884 | def _get_metadata(self, path): | ||
885 | requires = None | ||
886 | |||
887 | def parse_requires_data(data): | ||
888 | """Create a list of dependencies from a requires.txt file. | ||
889 | |||
890 | *data*: the contents of a setuptools-produced requires.txt file. | ||
891 | """ | ||
892 | reqs = [] | ||
893 | lines = data.splitlines() | ||
894 | for line in lines: | ||
895 | line = line.strip() | ||
896 | if line.startswith('['): | ||
897 | logger.warning('Unexpected line: quitting requirement scan: %r', | ||
898 | line) | ||
899 | break | ||
900 | r = parse_requirement(line) | ||
901 | if not r: | ||
902 | logger.warning('Not recognised as a requirement: %r', line) | ||
903 | continue | ||
904 | if r.extras: | ||
905 | logger.warning('extra requirements in requires.txt are ' | ||
906 | 'not supported') | ||
907 | if not r.constraints: | ||
908 | reqs.append(r.name) | ||
909 | else: | ||
910 | cons = ', '.join('%s%s' % c for c in r.constraints) | ||
911 | reqs.append('%s (%s)' % (r.name, cons)) | ||
912 | return reqs | ||
913 | |||
914 | def parse_requires_path(req_path): | ||
915 | """Create a list of dependencies from a requires.txt file. | ||
916 | |||
917 | *req_path*: the path to a setuptools-produced requires.txt file. | ||
918 | """ | ||
919 | |||
920 | reqs = [] | ||
921 | try: | ||
922 | with codecs.open(req_path, 'r', 'utf-8') as fp: | ||
923 | reqs = parse_requires_data(fp.read()) | ||
924 | except IOError: | ||
925 | pass | ||
926 | return reqs | ||
927 | |||
928 | tl_path = tl_data = None | ||
929 | if path.endswith('.egg'): | ||
930 | if os.path.isdir(path): | ||
931 | p = os.path.join(path, 'EGG-INFO') | ||
932 | meta_path = os.path.join(p, 'PKG-INFO') | ||
933 | metadata = Metadata(path=meta_path, scheme='legacy') | ||
934 | req_path = os.path.join(p, 'requires.txt') | ||
935 | tl_path = os.path.join(p, 'top_level.txt') | ||
936 | requires = parse_requires_path(req_path) | ||
937 | else: | ||
938 | # FIXME handle the case where zipfile is not available | ||
939 | zipf = zipimport.zipimporter(path) | ||
940 | fileobj = StringIO( | ||
941 | zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8')) | ||
942 | metadata = Metadata(fileobj=fileobj, scheme='legacy') | ||
943 | try: | ||
944 | data = zipf.get_data('EGG-INFO/requires.txt') | ||
945 | tl_data = zipf.get_data('EGG-INFO/top_level.txt').decode('utf-8') | ||
946 | requires = parse_requires_data(data.decode('utf-8')) | ||
947 | except IOError: | ||
948 | requires = None | ||
949 | elif path.endswith('.egg-info'): | ||
950 | if os.path.isdir(path): | ||
951 | req_path = os.path.join(path, 'requires.txt') | ||
952 | requires = parse_requires_path(req_path) | ||
953 | path = os.path.join(path, 'PKG-INFO') | ||
954 | tl_path = os.path.join(path, 'top_level.txt') | ||
955 | metadata = Metadata(path=path, scheme='legacy') | ||
956 | else: | ||
957 | raise DistlibException('path must end with .egg-info or .egg, ' | ||
958 | 'got %r' % path) | ||
959 | |||
960 | if requires: | ||
961 | metadata.add_requirements(requires) | ||
962 | # look for top-level modules in top_level.txt, if present | ||
963 | if tl_data is None: | ||
964 | if tl_path is not None and os.path.exists(tl_path): | ||
965 | with open(tl_path, 'rb') as f: | ||
966 | tl_data = f.read().decode('utf-8') | ||
967 | if not tl_data: | ||
968 | tl_data = [] | ||
969 | else: | ||
970 | tl_data = tl_data.splitlines() | ||
971 | self.modules = tl_data | ||
972 | return metadata | ||
973 | |||
974 | def __repr__(self): | ||
975 | return '<EggInfoDistribution %r %s at %r>' % ( | ||
976 | self.name, self.version, self.path) | ||
977 | |||
978 | def __str__(self): | ||
979 | return "%s %s" % (self.name, self.version) | ||
980 | |||
981 | def check_installed_files(self): | ||
982 | """ | ||
983 | Checks that the hashes and sizes of the files in ``RECORD`` are | ||
984 | matched by the files themselves. Returns a (possibly empty) list of | ||
985 | mismatches. Each entry in the mismatch list will be a tuple consisting | ||
986 | of the path, 'exists', 'size' or 'hash' according to what didn't match | ||
987 | (existence is checked first, then size, then hash), the expected | ||
988 | value and the actual value. | ||
989 | """ | ||
990 | mismatches = [] | ||
991 | record_path = os.path.join(self.path, 'installed-files.txt') | ||
992 | if os.path.exists(record_path): | ||
993 | for path, _, _ in self.list_installed_files(): | ||
994 | if path == record_path: | ||
995 | continue | ||
996 | if not os.path.exists(path): | ||
997 | mismatches.append((path, 'exists', True, False)) | ||
998 | return mismatches | ||
999 | |||
1000 | def list_installed_files(self): | ||
1001 | """ | ||
1002 | Iterates over the ``installed-files.txt`` entries and returns a tuple | ||
1003 | ``(path, hash, size)`` for each line. | ||
1004 | |||
1005 | :returns: a list of (path, hash, size) | ||
1006 | """ | ||
1007 | |||
1008 | def _md5(path): | ||
1009 | f = open(path, 'rb') | ||
1010 | try: | ||
1011 | content = f.read() | ||
1012 | finally: | ||
1013 | f.close() | ||
1014 | return hashlib.md5(content).hexdigest() | ||
1015 | |||
1016 | def _size(path): | ||
1017 | return os.stat(path).st_size | ||
1018 | |||
1019 | record_path = os.path.join(self.path, 'installed-files.txt') | ||
1020 | result = [] | ||
1021 | if os.path.exists(record_path): | ||
1022 | with codecs.open(record_path, 'r', encoding='utf-8') as f: | ||
1023 | for line in f: | ||
1024 | line = line.strip() | ||
1025 | p = os.path.normpath(os.path.join(self.path, line)) | ||
1026 | # "./" is present as a marker between installed files | ||
1027 | # and installation metadata files | ||
1028 | if not os.path.exists(p): | ||
1029 | logger.warning('Non-existent file: %s', p) | ||
1030 | if p.endswith(('.pyc', '.pyo')): | ||
1031 | continue | ||
1032 | #otherwise fall through and fail | ||
1033 | if not os.path.isdir(p): | ||
1034 | result.append((p, _md5(p), _size(p))) | ||
1035 | result.append((record_path, None, None)) | ||
1036 | return result | ||
1037 | |||
1038 | def list_distinfo_files(self, absolute=False): | ||
1039 | """ | ||
1040 | Iterates over the ``installed-files.txt`` entries and returns paths for | ||
1041 | each line if the path is pointing to a file located in the | ||
1042 | ``.egg-info`` directory or one of its subdirectories. | ||
1043 | |||
1044 | :parameter absolute: If *absolute* is ``True``, each returned path is | ||
1045 | transformed into a local absolute path. Otherwise the | ||
1046 | raw value from ``installed-files.txt`` is returned. | ||
1047 | :type absolute: boolean | ||
1048 | :returns: iterator of paths | ||
1049 | """ | ||
1050 | record_path = os.path.join(self.path, 'installed-files.txt') | ||
1051 | if os.path.exists(record_path): | ||
1052 | skip = True | ||
1053 | with codecs.open(record_path, 'r', encoding='utf-8') as f: | ||
1054 | for line in f: | ||
1055 | line = line.strip() | ||
1056 | if line == './': | ||
1057 | skip = False | ||
1058 | continue | ||
1059 | if not skip: | ||
1060 | p = os.path.normpath(os.path.join(self.path, line)) | ||
1061 | if p.startswith(self.path): | ||
1062 | if absolute: | ||
1063 | yield p | ||
1064 | else: | ||
1065 | yield line | ||
1066 | |||
1067 | def __eq__(self, other): | ||
1068 | return (isinstance(other, EggInfoDistribution) and | ||
1069 | self.path == other.path) | ||
1070 | |||
1071 | # See http://docs.python.org/reference/datamodel#object.__hash__ | ||
1072 | __hash__ = object.__hash__ | ||
1073 | |||
1074 | new_dist_class = InstalledDistribution | ||
1075 | old_dist_class = EggInfoDistribution | ||
1076 | |||
1077 | |||
1078 | class DependencyGraph(object): | ||
1079 | """ | ||
1080 | Represents a dependency graph between distributions. | ||
1081 | |||
1082 | The dependency relationships are stored in an ``adjacency_list`` that maps | ||
1083 | distributions to a list of ``(other, label)`` tuples where ``other`` | ||
1084 | is a distribution and the edge is labeled with ``label`` (i.e. the version | ||
1085 | specifier, if such was provided). Also, for more efficient traversal, for | ||
1086 | every distribution ``x``, a list of predecessors is kept in | ||
1087 | ``reverse_list[x]``. An edge from distribution ``a`` to | ||
1088 | distribution ``b`` means that ``a`` depends on ``b``. If any missing | ||
1089 | dependencies are found, they are stored in ``missing``, which is a | ||
1090 | dictionary that maps distributions to a list of requirements that were not | ||
1091 | provided by any other distributions. | ||
1092 | """ | ||
1093 | |||
1094 | def __init__(self): | ||
1095 | self.adjacency_list = {} | ||
1096 | self.reverse_list = {} | ||
1097 | self.missing = {} | ||
1098 | |||
1099 | def add_distribution(self, distribution): | ||
1100 | """Add the *distribution* to the graph. | ||
1101 | |||
1102 | :type distribution: :class:`distutils2.database.InstalledDistribution` | ||
1103 | or :class:`distutils2.database.EggInfoDistribution` | ||
1104 | """ | ||
1105 | self.adjacency_list[distribution] = [] | ||
1106 | self.reverse_list[distribution] = [] | ||
1107 | #self.missing[distribution] = [] | ||
1108 | |||
1109 | def add_edge(self, x, y, label=None): | ||
1110 | """Add an edge from distribution *x* to distribution *y* with the given | ||
1111 | *label*. | ||
1112 | |||
1113 | :type x: :class:`distutils2.database.InstalledDistribution` or | ||
1114 | :class:`distutils2.database.EggInfoDistribution` | ||
1115 | :type y: :class:`distutils2.database.InstalledDistribution` or | ||
1116 | :class:`distutils2.database.EggInfoDistribution` | ||
1117 | :type label: ``str`` or ``None`` | ||
1118 | """ | ||
1119 | self.adjacency_list[x].append((y, label)) | ||
1120 | # multiple edges are allowed, so be careful | ||
1121 | if x not in self.reverse_list[y]: | ||
1122 | self.reverse_list[y].append(x) | ||
1123 | |||
1124 | def add_missing(self, distribution, requirement): | ||
1125 | """ | ||
1126 | Add a missing *requirement* for the given *distribution*. | ||
1127 | |||
1128 | :type distribution: :class:`distutils2.database.InstalledDistribution` | ||
1129 | or :class:`distutils2.database.EggInfoDistribution` | ||
1130 | :type requirement: ``str`` | ||
1131 | """ | ||
1132 | logger.debug('%s missing %r', distribution, requirement) | ||
1133 | self.missing.setdefault(distribution, []).append(requirement) | ||
1134 | |||
1135 | def _repr_dist(self, dist): | ||
1136 | return '%s %s' % (dist.name, dist.version) | ||
1137 | |||
1138 | def repr_node(self, dist, level=1): | ||
1139 | """Prints only a subgraph""" | ||
1140 | output = [self._repr_dist(dist)] | ||
1141 | for other, label in self.adjacency_list[dist]: | ||
1142 | dist = self._repr_dist(other) | ||
1143 | if label is not None: | ||
1144 | dist = '%s [%s]' % (dist, label) | ||
1145 | output.append(' ' * level + str(dist)) | ||
1146 | suboutput = self.repr_node(other, level + 1) | ||
1147 | subs = suboutput.split('\n') | ||
1148 | output.extend(subs[1:]) | ||
1149 | return '\n'.join(output) | ||
1150 | |||
1151 | def to_dot(self, f, skip_disconnected=True): | ||
1152 | """Writes a DOT output for the graph to the provided file *f*. | ||
1153 | |||
1154 | If *skip_disconnected* is set to ``True``, then all distributions | ||
1155 | that are not dependent on any other distribution are skipped. | ||
1156 | |||
1157 | :type f: has to support ``file``-like operations | ||
1158 | :type skip_disconnected: ``bool`` | ||
1159 | """ | ||
1160 | disconnected = [] | ||
1161 | |||
1162 | f.write("digraph dependencies {\n") | ||
1163 | for dist, adjs in self.adjacency_list.items(): | ||
1164 | if len(adjs) == 0 and not skip_disconnected: | ||
1165 | disconnected.append(dist) | ||
1166 | for other, label in adjs: | ||
1167 | if not label is None: | ||
1168 | f.write('"%s" -> "%s" [label="%s"]\n' % | ||
1169 | (dist.name, other.name, label)) | ||
1170 | else: | ||
1171 | f.write('"%s" -> "%s"\n' % (dist.name, other.name)) | ||
1172 | if not skip_disconnected and len(disconnected) > 0: | ||
1173 | f.write('subgraph disconnected {\n') | ||
1174 | f.write('label = "Disconnected"\n') | ||
1175 | f.write('bgcolor = red\n') | ||
1176 | |||
1177 | for dist in disconnected: | ||
1178 | f.write('"%s"' % dist.name) | ||
1179 | f.write('\n') | ||
1180 | f.write('}\n') | ||
1181 | f.write('}\n') | ||
1182 | |||
1183 | def topological_sort(self): | ||
1184 | """ | ||
1185 | Perform a topological sort of the graph. | ||
1186 | :return: A tuple, the first element of which is a topologically sorted | ||
1187 | list of distributions, and the second element of which is a | ||
1188 | list of distributions that cannot be sorted because they have | ||
1189 | circular dependencies and so form a cycle. | ||
1190 | """ | ||
1191 | result = [] | ||
1192 | # Make a shallow copy of the adjacency list | ||
1193 | alist = {} | ||
1194 | for k, v in self.adjacency_list.items(): | ||
1195 | alist[k] = v[:] | ||
1196 | while True: | ||
1197 | # See what we can remove in this run | ||
1198 | to_remove = [] | ||
1199 | for k, v in list(alist.items())[:]: | ||
1200 | if not v: | ||
1201 | to_remove.append(k) | ||
1202 | del alist[k] | ||
1203 | if not to_remove: | ||
1204 | # What's left in alist (if anything) is a cycle. | ||
1205 | break | ||
1206 | # Remove from the adjacency list of others | ||
1207 | for k, v in alist.items(): | ||
1208 | alist[k] = [(d, r) for d, r in v if d not in to_remove] | ||
1209 | logger.debug('Moving to result: %s', | ||
1210 | ['%s (%s)' % (d.name, d.version) for d in to_remove]) | ||
1211 | result.extend(to_remove) | ||
1212 | return result, list(alist.keys()) | ||
1213 | |||
1214 | def __repr__(self): | ||
1215 | """Representation of the graph""" | ||
1216 | output = [] | ||
1217 | for dist, adjs in self.adjacency_list.items(): | ||
1218 | output.append(self.repr_node(dist)) | ||
1219 | return '\n'.join(output) | ||
1220 | |||
1221 | |||
1222 | def make_graph(dists, scheme='default'): | ||
1223 | """Makes a dependency graph from the given distributions. | ||
1224 | |||
1225 | :parameter dists: a list of distributions | ||
1226 | :type dists: list of :class:`distutils2.database.InstalledDistribution` and | ||
1227 | :class:`distutils2.database.EggInfoDistribution` instances | ||
1228 | :rtype: a :class:`DependencyGraph` instance | ||
1229 | """ | ||
1230 | scheme = get_scheme(scheme) | ||
1231 | graph = DependencyGraph() | ||
1232 | provided = {} # maps names to lists of (version, dist) tuples | ||
1233 | |||
1234 | # first, build the graph and find out what's provided | ||
1235 | for dist in dists: | ||
1236 | graph.add_distribution(dist) | ||
1237 | |||
1238 | for p in dist.provides: | ||
1239 | name, version = parse_name_and_version(p) | ||
1240 | logger.debug('Add to provided: %s, %s, %s', name, version, dist) | ||
1241 | provided.setdefault(name, []).append((version, dist)) | ||
1242 | |||
1243 | # now make the edges | ||
1244 | for dist in dists: | ||
1245 | requires = (dist.run_requires | dist.meta_requires | | ||
1246 | dist.build_requires | dist.dev_requires) | ||
1247 | for req in requires: | ||
1248 | try: | ||
1249 | matcher = scheme.matcher(req) | ||
1250 | except UnsupportedVersionError: | ||
1251 | # XXX compat-mode if cannot read the version | ||
1252 | logger.warning('could not read version %r - using name only', | ||
1253 | req) | ||
1254 | name = req.split()[0] | ||
1255 | matcher = scheme.matcher(name) | ||
1256 | |||
1257 | name = matcher.key # case-insensitive | ||
1258 | |||
1259 | matched = False | ||
1260 | if name in provided: | ||
1261 | for version, provider in provided[name]: | ||
1262 | try: | ||
1263 | match = matcher.match(version) | ||
1264 | except UnsupportedVersionError: | ||
1265 | match = False | ||
1266 | |||
1267 | if match: | ||
1268 | graph.add_edge(dist, provider, req) | ||
1269 | matched = True | ||
1270 | break | ||
1271 | if not matched: | ||
1272 | graph.add_missing(dist, req) | ||
1273 | return graph | ||
1274 | |||
1275 | |||
1276 | def get_dependent_dists(dists, dist): | ||
1277 | """Recursively generate a list of distributions from *dists* that are | ||
1278 | dependent on *dist*. | ||
1279 | |||
1280 | :param dists: a list of distributions | ||
1281 | :param dist: a distribution, member of *dists* for which we are interested | ||
1282 | """ | ||
1283 | if dist not in dists: | ||
1284 | raise DistlibException('given distribution %r is not a member ' | ||
1285 | 'of the list' % dist.name) | ||
1286 | graph = make_graph(dists) | ||
1287 | |||
1288 | dep = [dist] # dependent distributions | ||
1289 | todo = graph.reverse_list[dist] # list of nodes we should inspect | ||
1290 | |||
1291 | while todo: | ||
1292 | d = todo.pop() | ||
1293 | dep.append(d) | ||
1294 | for succ in graph.reverse_list[d]: | ||
1295 | if succ not in dep: | ||
1296 | todo.append(succ) | ||
1297 | |||
1298 | dep.pop(0) # remove dist from dep, was there to prevent infinite loops | ||
1299 | return dep | ||
1300 | |||
1301 | |||
1302 | def get_required_dists(dists, dist): | ||
1303 | """Recursively generate a list of distributions from *dists* that are | ||
1304 | required by *dist*. | ||
1305 | |||
1306 | :param dists: a list of distributions | ||
1307 | :param dist: a distribution, member of *dists* for which we are interested | ||
1308 | """ | ||
1309 | if dist not in dists: | ||
1310 | raise DistlibException('given distribution %r is not a member ' | ||
1311 | 'of the list' % dist.name) | ||
1312 | graph = make_graph(dists) | ||
1313 | |||
1314 | req = [] # required distributions | ||
1315 | todo = graph.adjacency_list[dist] # list of nodes we should inspect | ||
1316 | |||
1317 | while todo: | ||
1318 | d = todo.pop()[0] | ||
1319 | req.append(d) | ||
1320 | for pred in graph.adjacency_list[d]: | ||
1321 | if pred not in req: | ||
1322 | todo.append(pred) | ||
1323 | |||
1324 | return req | ||
1325 | |||
1326 | |||
1327 | def make_dist(name, version, **kwargs): | ||
1328 | """ | ||
1329 | A convenience method for making a dist given just a name and version. | ||
1330 | """ | ||
1331 | summary = kwargs.pop('summary', 'Placeholder for summary') | ||
1332 | md = Metadata(**kwargs) | ||
1333 | md.name = name | ||
1334 | md.version = version | ||
1335 | md.summary = summary or 'Placeholder for summary' | ||
1336 | return Distribution(md) | ||