summaryrefslogtreecommitdiff
path: root/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/database.py
diff options
context:
space:
mode:
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.py1336
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
8from __future__ import unicode_literals
9
10import base64
11import codecs
12import contextlib
13import hashlib
14import logging
15import os
16import posixpath
17import sys
18import zipimport
19
20from . import DistlibException, resources
21from .compat import StringIO
22from .version import get_scheme, UnsupportedVersionError
23from .metadata import Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME
24from .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
33logger = logging.getLogger(__name__)
34
35EXPORTS_FILENAME = 'pydist-exports.json'
36COMMANDS_FILENAME = 'pydist-commands.json'
37
38DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED',
39 'RESOURCES', EXPORTS_FILENAME, 'SHARED')
40
41DISTINFO_EXT = '.dist-info'
42
43
44class _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
74class 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
314class 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
471class 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
526class 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
854class 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
1074new_dist_class = InstalledDistribution
1075old_dist_class = EggInfoDistribution
1076
1077
1078class 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
1222def 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
1276def 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
1302def 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
1327def 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)