6"""PEP 376 implementation."""
8from __future__
import unicode_literals
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)
29__all__ = [
'Distribution',
'BaseInstalledDistribution',
30 'InstalledDistribution',
'EggInfoDistribution',
36EXPORTS_FILENAME =
'pydist-exports.json'
37COMMANDS_FILENAME =
'pydist-commands.json'
39DIST_FILES = (
'INSTALLER', METADATA_FILENAME,
'RECORD',
'REQUESTED',
40 'RESOURCES', EXPORTS_FILENAME,
'SHARED')
42DISTINFO_EXT =
'.dist-info'
47 A simple cache mapping names and .dist-info paths to distributions
51 Initialise an instance. There is normally one for each DistributionPath.
59 Clear the cache, setting it to its initial state.
67 Add a distribution to the cache.
68 :param dist: The distribution to add.
77 Represents a set of distributions installed on a path (typically sys.path).
79 def __init__(self, path=None, include_egg=False):
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,
85 :param include_egg: If True, this instance will look for and return legacy
86 distributions as well as those based on PEP 376.
105 cache_enabled =
property(_get_cache_enabled, _set_cache_enabled)
109 Clears the internal cache.
117 Yield .dist-info and/or .egg(-info) distributions.
123 for path
in self.
path:
133 if not r
or r.path in seen:
137 possible_filenames = [METADATA_FILENAME,
138 WHEEL_METADATA_FILENAME,
139 LEGACY_METADATA_FILENAME]
140 for metadata_filename
in possible_filenames:
149 metadata =
Metadata(fileobj=stream, scheme=
'legacy')
159 except Exception
as e:
160 msg =
'Unable to read distribution at %s, perhaps due to bad metadata: %s'
167 Scan the path for distributions and populate the cache with
168 those that are found.
170 gen_dist =
not self.
_cache.generated
172 if gen_dist
or gen_egg:
180 self.
_cache.generated =
True
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.
192 :parameter name: is converted to a standard distribution name by replacing
193 any runs of non- alphanumeric characters with a single
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
204 return '-'.join([name, version]) + DISTINFO_EXT
208 Provides an iterator that looks for distributions and returns
209 :class:`InstalledDistribution` or
210 :class:`EggInfoDistribution` instances for each one of them.
212 :rtype: iterator of :class:`InstalledDistribution` and
213 :class:`EggInfoDistribution` instances
230 Looks for a named distribution on the path.
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.
235 :rtype: :class:`InstalledDistribution`, :class:`EggInfoDistribution`
248 if name
in self.
_cache.name:
249 result = self.
_cache.name[name][0]
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.
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``.
262 :parameter version: a version specifier that indicates the version
263 required, conforming to the format in ``PEP-345``
266 :type version: string
269 if version
is not None:
271 matcher = self.
_scheme.matcher(
'%s (%s)' % (name, version))
273 raise DistlibException(
'invalid name or version: %r, %r' %
279 if not hasattr(dist,
'provides'):
285 p_name, p_ver = parse_name_and_version(p)
297 Return the path to a resource file.
301 raise LookupError(
'no distribution named %r found' % name)
306 Return all of the exported entries in a particular category.
308 :param category: The category to search for entries.
309 :param name: If specified, only entries with that name are returned.
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
330 build_time_dependency =
False
332 Set to True if it's known to be only a build-time dependency (i.e.
333 not needed after installation).
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)."""
343 Initialise an instance.
344 :param metadata: The instance of :class:`Metadata` describing this
361 The source archive download URL for this distribution.
365 download_url = source_url
370 A utility property which displays the name and version in parentheses.
377 A set of distribution names and versions provided by this distribution.
378 :return: A set of "name (version)" strings.
389 logger.debug(
'%s: got requirements %r from metadata: %r', self.
name, req_attr,
416 Say if this instance matches (fulfills) a requirement.
417 :param req: The requirement to match.
419 :return: True if it matches, else False.
423 r = parse_requirement(req)
424 scheme = get_scheme(self.
metadata.scheme)
427 except UnsupportedVersionError:
438 p_name, p_ver = parse_name_and_version(p)
444 except UnsupportedVersionError:
450 Return a textual representation of this instance,
456 return '<Distribution %s (%s)%s>' % (self.
name, self.
version, suffix)
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.
466 if type(other)
is not type(self):
476 Compute hash in a way which matches the equality test.
483 This is the base class for installed distributions (whether PEP 376 or
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.
506 Get the hash of some data, using a particular hash algorithm, if
509 :param data: The data to be hashed.
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
529 hasher =
getattr(hashlib, hasher)
530 prefix =
'%s=' % self.
hasher
533 return '%s%s' % (prefix, digest)
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).
550 raise ValueError(
'finder unavailable for %s' % path)
553 elif metadata
is None:
562 raise ValueError(
'no %s found in %s' % (METADATA_FILENAME,
565 metadata =
Metadata(fileobj=stream, scheme=
'legacy')
567 super(InstalledDistribution, self).
__init__(metadata, path, env)
576 with open(p,
'rb')
as f:
577 data =
f.read().decode(
'utf-8')
581 return '<InstalledDistribution %r %s at %r>' % (
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).
597 with CSVReader(stream=stream)
as record_reader:
601 for row
in record_reader:
602 missing = [
None for i
in range(
len(row), 3)]
603 path, checksum, size = row + missing
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.
624 def read_exports(self):
626 Read exports data from a file in .ini format.
628 :return: A dictionary of exports, mapping an export category to a list
629 of :class:`ExportEntry` instances describing the individual
636 result = read_exports(stream)
639 def write_exports(self, exports):
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.
647 with open(rf,
'w')
as f:
648 write_exports(exports, f)
652 NOTE: This API may change in the future.
654 Return the absolute path to a resource file with the given relative
657 :param relative_path: The path, relative to .dist-info, of the resource
659 :return: The absolute path where the resource is to be found.
663 with CSVReader(stream=stream)
as resources_reader:
664 for relative, destination
in resources_reader:
665 if relative == relative_path:
667 raise KeyError(
'no resource file with relative path %r '
668 'is installed' % relative_path)
672 Iterates over the ``RECORD`` entries and returns a tuple
673 ``(path, hash, size)`` for each line.
675 :returns: iterator of (path, hash, size)
682 Writes the ``RECORD`` file, using the ``paths`` iterable passed in. Any
683 existing ``RECORD`` file is silently overwritten.
685 prefix is used to determine when to write absolute paths.
699 hash_value = size =
''
702 with open(path,
'rb')
as fp:
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.
730 if path == record_path:
736 if size
and actual_size != size:
739 if '=' in hash_value:
744 with open(path,
'rb')
as f:
746 if actual_hash != hash_value:
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.
761 The first time this property is accessed, the relevant information is
762 read from the SHARED file in the .dist-info directory.
767 with codecs.open(shared_path,
'r', encoding=
'utf-8')
as f:
771 if key ==
'namespace':
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
784 :return: The path of the file written to.
791 for key
in (
'prefix',
'lib',
'headers',
'scripts',
'data'):
798 with codecs.open(shared_path,
'w', encoding=
'utf-8')
as f:
803 if path
not in DIST_FILES:
804 raise DistlibException(
'invalid path for a dist-info file: '
808 raise DistlibException(
'Unable to get a finder for %s' % self.
pathpath)
813 Returns a path located under the ``.dist-info`` directory. Returns a
814 string representing the path.
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
829 raise DistlibException(
830 'dist-info file %r does not belong to the %r %s '
834 if path
not in DIST_FILES:
835 raise DistlibException(
'invalid path for a dist-info file: '
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.
846 :returns: iterator of paths
857 return (
isinstance(other, InstalledDistribution)
and
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."""
871 shared_locations = {}
892 super(EggInfoDistribution, self).
__init__(metadata, path, env)
898 """Create a list of dependencies from a requires.txt file.
900 *data*: the contents of a setuptools-produced requires.txt file.
910 r = parse_requirement(line)
925 """Create a list of dependencies from a requires.txt file.
927 *req_path*: the path to a setuptools-produced requires.txt file.
938 tl_path = tl_data =
None
943 metadata =
Metadata(path=meta_path, scheme=
'legacy')
952 metadata =
Metadata(fileobj=fileobj, scheme=
'legacy')
955 tl_data =
zipf.get_data(
'EGG-INFO/top_level.txt').decode(
'utf-8')
965 metadata =
Metadata(path=path, scheme=
'legacy')
967 raise DistlibException(
'path must end with .egg-info or .egg, '
975 with open(tl_path,
'rb')
as f:
976 tl_data =
f.read().decode(
'utf-8')
985 return '<EggInfoDistribution %r %s at %r>' % (
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.
1004 if path == record_path:
1012 Iterates over the ``installed-files.txt`` entries and returns a tuple
1013 ``(path, hash, size)`` for each line.
1015 :returns: a list of (path, hash, size)
1019 f = open(path,
'rb')
1032 with codecs.open(record_path,
'r', encoding=
'utf-8')
as f:
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.
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
1063 with codecs.open(record_path,
'r', encoding=
'utf-8')
as f:
1078 return (
isinstance(other, EggInfoDistribution)
and
1084new_dist_class = InstalledDistribution
1085old_dist_class = EggInfoDistribution
1090 Represents a dependency graph between distributions.
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.
1110 """Add the *distribution* to the graph.
1112 :type distribution: :class:`distutils2.database.InstalledDistribution`
1113 or :class:`distutils2.database.EggInfoDistribution`
1120 """Add an edge from distribution *x* to distribution *y* with the given
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``
1136 Add a missing *requirement* for the given *distribution*.
1138 :type distribution: :class:`distutils2.database.InstalledDistribution`
1139 or :class:`distutils2.database.EggInfoDistribution`
1140 :type requirement: ``str``
1142 logger.debug(
'%s missing %r', distribution, requirement)
1143 self.
missing.setdefault(distribution, []).append(requirement)
1149 """Prints only a subgraph"""
1153 if label
is not None:
1154 dist =
'%s [%s]' % (dist, label)
1156 suboutput = self.
repr_node(other, level + 1)
1159 return '\n'.join(output)
1162 """Writes a DOT output for the graph to the provided file *f*.
1164 If *skip_disconnected* is set to ``True``, then all distributions
1165 that are not dependent on any other distribution are skipped.
1167 :type f: has to support ``file``-like operations
1168 :type skip_disconnected: ``bool``
1172 f.write(
"digraph dependencies {\n")
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' %
1182 if not skip_disconnected
and len(disconnected) > 0:
1183 f.write(
'subgraph disconnected {\n')
1184 f.write(
'label = "Disconnected"\n')
1187 for dist
in disconnected:
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.
1218 alist[k] = [(d, r)
for d, r
in v
if d
not in to_remove]
1225 """Representation of the graph"""
1229 return '\n'.join(output)
1233 """Makes a dependency graph from the given distributions.
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
1240 scheme = get_scheme(scheme)
1249 name, version = parse_name_and_version(p)
1250 logger.debug(
'Add to provided: %s, %s, %s', name, version, dist)
1257 for req
in requires:
1260 except UnsupportedVersionError:
1270 if name
in provided:
1271 for version, provider
in provided[name]:
1274 except UnsupportedVersionError:
1287 """Recursively generate a list of distributions from *dists* that are
1288 dependent on *dist*.
1290 :param dists: a list of distributions
1291 :param dist: a distribution, member of *dists* for which we are interested
1293 if dist
not in dists:
1294 raise DistlibException(
'given distribution %r is not a member '
1313 """Recursively generate a list of distributions from *dists* that are
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.
1320 if dist
not in dists:
1321 raise DistlibException(
'given distribution %r is not a member '
1327 seen = set(t[0]
for t
in todo)
1333 for pred
in pred_list:
1335 if d
not in req
and d
not in seen:
1341def make_dist(name, version, **kwargs):
1343 A convenience method for making a dist given just a name and version.
1345 summary =
kwargs.pop(
'summary',
'Placeholder for summary')
1349 md.summary = summary
or 'Placeholder for summary'
get_hash(self, data, hasher=None)
__init__(self, metadata, path, env=None)
add_edge(self, x, y, label=None)
to_dot(self, f, skip_disconnected=True)
repr_node(self, dist, level=1)
add_distribution(self, distribution)
add_missing(self, distribution, requirement)
_yield_distributions(self)
__init__(self, path=None, include_egg=False)
provides_distribution(self, name, version=None)
_set_cache_enabled(self, value)
distinfo_dirname(cls, name, version)
get_distribution(self, name)
get_file_path(self, name, relative_path)
get_exported_entries(self, category, name=None)
matches_requirement(self, req)
_get_requirements(self, req_attr)
list_distinfo_files(self, absolute=False)
check_installed_files(self)
__init__(self, path, env=None)
list_installed_files(self)
_get_metadata(self, path)
list_distinfo_files(self)
__init__(self, path, metadata=None, env=None)
get_resource_path(self, relative_path)
get_distinfo_resource(self, path)
check_installed_files(self)
get_distinfo_file(self, path)
list_installed_files(self)
write_installed_files(self, paths, prefix, dry_run=False)
write_shared_locations(self, paths, dry_run=False)
get_dependent_dists(dists, dist)
get_required_dists(dists, dist)
make_graph(dists, scheme='default')