Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/importlib/metadata.py: 48%
234 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1import io
2import os
3import re
4import abc
5import csv
6import sys
7import email
8import pathlib
9import zipfile
10import operator
11import functools
12import itertools
13import posixpath
14import collections
16from configparser import ConfigParser
17from contextlib import suppress
18from importlib import import_module
19from importlib.abc import MetaPathFinder
20from itertools import starmap
23__all__ = [
24 'Distribution',
25 'DistributionFinder',
26 'PackageNotFoundError',
27 'distribution',
28 'distributions',
29 'entry_points',
30 'files',
31 'metadata',
32 'requires',
33 'version',
34 ]
37class PackageNotFoundError(ModuleNotFoundError):
38 """The package was not found."""
41class EntryPoint(
42 collections.namedtuple('EntryPointBase', 'name value group')):
43 """An entry point as defined by Python packaging conventions.
45 See `the packaging docs on entry points
46 <https://packaging.python.org/specifications/entry-points/>`_
47 for more information.
48 """
50 pattern = re.compile(
51 r'(?P<module>[\w.]+)\s*'
52 r'(:\s*(?P<attr>[\w.]+))?\s*'
53 r'(?P<extras>\[.*\])?\s*$'
54 )
55 """
56 A regular expression describing the syntax for an entry point,
57 which might look like:
59 - module
60 - package.module
61 - package.module:attribute
62 - package.module:object.attribute
63 - package.module:attr [extra1, extra2]
65 Other combinations are possible as well.
67 The expression is lenient about whitespace around the ':',
68 following the attr, and following any extras.
69 """
71 def load(self):
72 """Load the entry point from its definition. If only a module
73 is indicated by the value, return that module. Otherwise,
74 return the named object.
75 """
76 match = self.pattern.match(self.value)
77 module = import_module(match.group('module'))
78 attrs = filter(None, (match.group('attr') or '').split('.'))
79 return functools.reduce(getattr, attrs, module)
81 @property
82 def extras(self):
83 match = self.pattern.match(self.value)
84 return list(re.finditer(r'\w+', match.group('extras') or ''))
86 @classmethod
87 def _from_config(cls, config):
88 return [
89 cls(name, value, group)
90 for group in config.sections()
91 for name, value in config.items(group)
92 ]
94 @classmethod
95 def _from_text(cls, text):
96 config = ConfigParser(delimiters='=')
97 # case sensitive: https://stackoverflow.com/q/1611799/812183
98 config.optionxform = str
99 try:
100 config.read_string(text)
101 except AttributeError: # pragma: nocover
102 # Python 2 has no read_string
103 config.readfp(io.StringIO(text))
104 return EntryPoint._from_config(config)
106 def __iter__(self):
107 """
108 Supply iter so one may construct dicts of EntryPoints easily.
109 """
110 return iter((self.name, self))
112 def __reduce__(self):
113 return (
114 self.__class__,
115 (self.name, self.value, self.group),
116 )
119class PackagePath(pathlib.PurePosixPath):
120 """A reference to a path in a package"""
122 def read_text(self, encoding='utf-8'):
123 with self.locate().open(encoding=encoding) as stream:
124 return stream.read()
126 def read_binary(self):
127 with self.locate().open('rb') as stream:
128 return stream.read()
130 def locate(self):
131 """Return a path-like object for this path"""
132 return self.dist.locate_file(self)
135class FileHash:
136 def __init__(self, spec):
137 self.mode, _, self.value = spec.partition('=')
139 def __repr__(self):
140 return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
143class Distribution:
144 """A Python distribution package."""
146 @abc.abstractmethod
147 def read_text(self, filename):
148 """Attempt to load metadata file given by the name.
150 :param filename: The name of the file in the distribution info.
151 :return: The text if found, otherwise None.
152 """
154 @abc.abstractmethod
155 def locate_file(self, path):
156 """
157 Given a path to a file in this distribution, return a path
158 to it.
159 """
161 @classmethod
162 def from_name(cls, name):
163 """Return the Distribution for the given package name.
165 :param name: The name of the distribution package to search for.
166 :return: The Distribution instance (or subclass thereof) for the named
167 package, if found.
168 :raises PackageNotFoundError: When the named package's distribution
169 metadata cannot be found.
170 """
171 for resolver in cls._discover_resolvers():
172 dists = resolver(DistributionFinder.Context(name=name))
173 dist = next(dists, None)
174 if dist is not None:
175 return dist
176 else:
177 raise PackageNotFoundError(name)
179 @classmethod
180 def discover(cls, **kwargs):
181 """Return an iterable of Distribution objects for all packages.
183 Pass a ``context`` or pass keyword arguments for constructing
184 a context.
186 :context: A ``DistributionFinder.Context`` object.
187 :return: Iterable of Distribution objects for all packages.
188 """
189 context = kwargs.pop('context', None)
190 if context and kwargs:
191 raise ValueError("cannot accept context and kwargs")
192 context = context or DistributionFinder.Context(**kwargs)
193 return itertools.chain.from_iterable(
194 resolver(context)
195 for resolver in cls._discover_resolvers()
196 )
198 @staticmethod
199 def at(path):
200 """Return a Distribution for the indicated metadata path
202 :param path: a string or path-like object
203 :return: a concrete Distribution instance for the path
204 """
205 return PathDistribution(pathlib.Path(path))
207 @staticmethod
208 def _discover_resolvers():
209 """Search the meta_path for resolvers."""
210 declared = (
211 getattr(finder, 'find_distributions', None)
212 for finder in sys.meta_path
213 )
214 return filter(None, declared)
216 @property
217 def metadata(self):
218 """Return the parsed metadata for this Distribution.
220 The returned object will have keys that name the various bits of
221 metadata. See PEP 566 for details.
222 """
223 text = (
224 self.read_text('METADATA')
225 or self.read_text('PKG-INFO')
226 # This last clause is here to support old egg-info files. Its
227 # effect is to just end up using the PathDistribution's self._path
228 # (which points to the egg-info file) attribute unchanged.
229 or self.read_text('')
230 )
231 return email.message_from_string(text)
233 @property
234 def version(self):
235 """Return the 'Version' metadata for the distribution package."""
236 return self.metadata['Version']
238 @property
239 def entry_points(self):
240 return EntryPoint._from_text(self.read_text('entry_points.txt'))
242 @property
243 def files(self):
244 """Files in this distribution.
246 :return: List of PackagePath for this distribution or None
248 Result is `None` if the metadata file that enumerates files
249 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
250 missing.
251 Result may be empty if the metadata exists but is empty.
252 """
253 file_lines = self._read_files_distinfo() or self._read_files_egginfo()
255 def make_file(name, hash=None, size_str=None):
256 result = PackagePath(name)
257 result.hash = FileHash(hash) if hash else None
258 result.size = int(size_str) if size_str else None
259 result.dist = self
260 return result
262 return file_lines and list(starmap(make_file, csv.reader(file_lines)))
264 def _read_files_distinfo(self):
265 """
266 Read the lines of RECORD
267 """
268 text = self.read_text('RECORD')
269 return text and text.splitlines()
271 def _read_files_egginfo(self):
272 """
273 SOURCES.txt might contain literal commas, so wrap each line
274 in quotes.
275 """
276 text = self.read_text('SOURCES.txt')
277 return text and map('"{}"'.format, text.splitlines())
279 @property
280 def requires(self):
281 """Generated requirements specified for this Distribution"""
282 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
283 return reqs and list(reqs)
285 def _read_dist_info_reqs(self):
286 return self.metadata.get_all('Requires-Dist')
288 def _read_egg_info_reqs(self):
289 source = self.read_text('requires.txt')
290 return source and self._deps_from_requires_text(source)
292 @classmethod
293 def _deps_from_requires_text(cls, source):
294 section_pairs = cls._read_sections(source.splitlines())
295 sections = {
296 section: list(map(operator.itemgetter('line'), results))
297 for section, results in
298 itertools.groupby(section_pairs, operator.itemgetter('section'))
299 }
300 return cls._convert_egg_info_reqs_to_simple_reqs(sections)
302 @staticmethod
303 def _read_sections(lines):
304 section = None
305 for line in filter(None, lines):
306 section_match = re.match(r'\[(.*)\]$', line)
307 if section_match:
308 section = section_match.group(1)
309 continue
310 yield locals()
312 @staticmethod
313 def _convert_egg_info_reqs_to_simple_reqs(sections):
314 """
315 Historically, setuptools would solicit and store 'extra'
316 requirements, including those with environment markers,
317 in separate sections. More modern tools expect each
318 dependency to be defined separately, with any relevant
319 extras and environment markers attached directly to that
320 requirement. This method converts the former to the
321 latter. See _test_deps_from_requires_text for an example.
322 """
323 def make_condition(name):
324 return name and 'extra == "{name}"'.format(name=name)
326 def parse_condition(section):
327 section = section or ''
328 extra, sep, markers = section.partition(':')
329 if extra and markers:
330 markers = '({markers})'.format(markers=markers)
331 conditions = list(filter(None, [markers, make_condition(extra)]))
332 return '; ' + ' and '.join(conditions) if conditions else ''
334 for section, deps in sections.items():
335 for dep in deps:
336 yield dep + parse_condition(section)
339class DistributionFinder(MetaPathFinder):
340 """
341 A MetaPathFinder capable of discovering installed distributions.
342 """
344 class Context:
345 """
346 Keyword arguments presented by the caller to
347 ``distributions()`` or ``Distribution.discover()``
348 to narrow the scope of a search for distributions
349 in all DistributionFinders.
351 Each DistributionFinder may expect any parameters
352 and should attempt to honor the canonical
353 parameters defined below when appropriate.
354 """
356 name = None
357 """
358 Specific name for which a distribution finder should match.
359 A name of ``None`` matches all distributions.
360 """
362 def __init__(self, **kwargs):
363 vars(self).update(kwargs)
365 @property
366 def path(self):
367 """
368 The path that a distribution finder should search.
370 Typically refers to Python package paths and defaults
371 to ``sys.path``.
372 """
373 return vars(self).get('path', sys.path)
375 @abc.abstractmethod
376 def find_distributions(self, context=Context()):
377 """
378 Find distributions.
380 Return an iterable of all Distribution instances capable of
381 loading the metadata for packages matching the ``context``,
382 a DistributionFinder.Context instance.
383 """
386class FastPath:
387 """
388 Micro-optimized class for searching a path for
389 children.
390 """
392 def __init__(self, root):
393 self.root = root
394 self.base = os.path.basename(root).lower()
396 def joinpath(self, child):
397 return pathlib.Path(self.root, child)
399 def children(self):
400 with suppress(Exception):
401 return os.listdir(self.root or '')
402 with suppress(Exception):
403 return self.zip_children()
404 return []
406 def zip_children(self):
407 zip_path = zipfile.Path(self.root)
408 names = zip_path.root.namelist()
409 self.joinpath = zip_path.joinpath
411 return (
412 posixpath.split(child)[0]
413 for child in names
414 )
416 def is_egg(self, search):
417 base = self.base
418 return (
419 base == search.versionless_egg_name
420 or base.startswith(search.prefix)
421 and base.endswith('.egg'))
423 def search(self, name):
424 for child in self.children():
425 n_low = child.lower()
426 if (n_low in name.exact_matches
427 or n_low.startswith(name.prefix)
428 and n_low.endswith(name.suffixes)
429 # legacy case:
430 or self.is_egg(name) and n_low == 'egg-info'):
431 yield self.joinpath(child)
434class Prepared:
435 """
436 A prepared search for metadata on a possibly-named package.
437 """
438 normalized = ''
439 prefix = ''
440 suffixes = '.dist-info', '.egg-info'
441 exact_matches = [''][:0]
442 versionless_egg_name = ''
444 def __init__(self, name):
445 self.name = name
446 if name is None:
447 return
448 self.normalized = name.lower().replace('-', '_')
449 self.prefix = self.normalized + '-'
450 self.exact_matches = [
451 self.normalized + suffix for suffix in self.suffixes]
452 self.versionless_egg_name = self.normalized + '.egg'
455class MetadataPathFinder(DistributionFinder):
456 @classmethod
457 def find_distributions(cls, context=DistributionFinder.Context()):
458 """
459 Find distributions.
461 Return an iterable of all Distribution instances capable of
462 loading the metadata for packages matching ``context.name``
463 (or all names if ``None`` indicated) along the paths in the list
464 of directories ``context.path``.
465 """
466 found = cls._search_paths(context.name, context.path)
467 return map(PathDistribution, found)
469 @classmethod
470 def _search_paths(cls, name, paths):
471 """Find metadata directories in paths heuristically."""
472 return itertools.chain.from_iterable(
473 path.search(Prepared(name))
474 for path in map(FastPath, paths)
475 )
479class PathDistribution(Distribution):
480 def __init__(self, path):
481 """Construct a distribution from a path to the metadata directory.
483 :param path: A pathlib.Path or similar object supporting
484 .joinpath(), __div__, .parent, and .read_text().
485 """
486 self._path = path
488 def read_text(self, filename):
489 with suppress(FileNotFoundError, IsADirectoryError, KeyError,
490 NotADirectoryError, PermissionError):
491 return self._path.joinpath(filename).read_text(encoding='utf-8')
492 read_text.__doc__ = Distribution.read_text.__doc__
494 def locate_file(self, path):
495 return self._path.parent / path
498def distribution(distribution_name):
499 """Get the ``Distribution`` instance for the named package.
501 :param distribution_name: The name of the distribution package as a string.
502 :return: A ``Distribution`` instance (or subclass thereof).
503 """
504 return Distribution.from_name(distribution_name)
507def distributions(**kwargs):
508 """Get all ``Distribution`` instances in the current environment.
510 :return: An iterable of ``Distribution`` instances.
511 """
512 return Distribution.discover(**kwargs)
515def metadata(distribution_name):
516 """Get the metadata for the named package.
518 :param distribution_name: The name of the distribution package to query.
519 :return: An email.Message containing the parsed metadata.
520 """
521 return Distribution.from_name(distribution_name).metadata
524def version(distribution_name):
525 """Get the version string for the named package.
527 :param distribution_name: The name of the distribution package to query.
528 :return: The version string for the package as defined in the package's
529 "Version" metadata key.
530 """
531 return distribution(distribution_name).version
534def entry_points():
535 """Return EntryPoint objects for all installed packages.
537 :return: EntryPoint objects for all installed packages.
538 """
539 eps = itertools.chain.from_iterable(
540 dist.entry_points for dist in distributions())
541 by_group = operator.attrgetter('group')
542 ordered = sorted(eps, key=by_group)
543 grouped = itertools.groupby(ordered, by_group)
544 return {
545 group: tuple(eps)
546 for group, eps in grouped
547 }
550def files(distribution_name):
551 """Return a list of files for the named package.
553 :param distribution_name: The name of the distribution package to query.
554 :return: List of files composing the distribution.
555 """
556 return distribution(distribution_name).files
559def requires(distribution_name):
560 """
561 Return a list of requirements for the named package.
563 :return: An iterator of requirements, suitable for
564 packaging.requirement.Requirement.
565 """
566 return distribution(distribution_name).requires