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