Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/importlib_metadata/__init__.py : 1%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import os
2import re
3import abc
4import csv
5import sys
6import zipp
7import email
8import pathlib
9import operator
10import textwrap
11import warnings
12import functools
13import itertools
14import posixpath
15import collections
17from ._collections import FreezableDefaultDict, Pair
18from ._compat import (
19 NullFinder,
20 Protocol,
21 PyPy_repr,
22 install,
23)
24from ._functools import method_cache
25from ._itertools import unique_everseen
27from contextlib import suppress
28from importlib import import_module
29from importlib.abc import MetaPathFinder
30from itertools import starmap
31from typing import Any, List, Mapping, Optional, TypeVar, Union
34__all__ = [
35 'Distribution',
36 'DistributionFinder',
37 'PackageNotFoundError',
38 'distribution',
39 'distributions',
40 'entry_points',
41 'files',
42 'metadata',
43 'packages_distributions',
44 'requires',
45 'version',
46]
49class PackageNotFoundError(ModuleNotFoundError):
50 """The package was not found."""
52 def __str__(self):
53 tmpl = "No package metadata was found for {self.name}"
54 return tmpl.format(**locals())
56 @property
57 def name(self):
58 (name,) = self.args
59 return name
62class Sectioned:
63 """
64 A simple entry point config parser for performance
66 >>> for item in Sectioned.read(Sectioned._sample):
67 ... print(item)
68 Pair(name='sec1', value='# comments ignored')
69 Pair(name='sec1', value='a = 1')
70 Pair(name='sec1', value='b = 2')
71 Pair(name='sec2', value='a = 2')
73 >>> res = Sectioned.section_pairs(Sectioned._sample)
74 >>> item = next(res)
75 >>> item.name
76 'sec1'
77 >>> item.value
78 Pair(name='a', value='1')
79 >>> item = next(res)
80 >>> item.value
81 Pair(name='b', value='2')
82 >>> item = next(res)
83 >>> item.name
84 'sec2'
85 >>> item.value
86 Pair(name='a', value='2')
87 >>> list(res)
88 []
89 """
91 _sample = textwrap.dedent(
92 """
93 [sec1]
94 # comments ignored
95 a = 1
96 b = 2
98 [sec2]
99 a = 2
100 """
101 ).lstrip()
103 @classmethod
104 def section_pairs(cls, text):
105 return (
106 section._replace(value=Pair.parse(section.value))
107 for section in cls.read(text, filter_=cls.valid)
108 if section.name is not None
109 )
111 @staticmethod
112 def read(text, filter_=None):
113 lines = filter(filter_, map(str.strip, text.splitlines()))
114 name = None
115 for value in lines:
116 section_match = value.startswith('[') and value.endswith(']')
117 if section_match:
118 name = value.strip('[]')
119 continue
120 yield Pair(name, value)
122 @staticmethod
123 def valid(line):
124 return line and not line.startswith('#')
127class EntryPoint(
128 PyPy_repr, collections.namedtuple('EntryPointBase', 'name value group')
129):
130 """An entry point as defined by Python packaging conventions.
132 See `the packaging docs on entry points
133 <https://packaging.python.org/specifications/entry-points/>`_
134 for more information.
135 """
137 pattern = re.compile(
138 r'(?P<module>[\w.]+)\s*'
139 r'(:\s*(?P<attr>[\w.]+))?\s*'
140 r'(?P<extras>\[.*\])?\s*$'
141 )
142 """
143 A regular expression describing the syntax for an entry point,
144 which might look like:
146 - module
147 - package.module
148 - package.module:attribute
149 - package.module:object.attribute
150 - package.module:attr [extra1, extra2]
152 Other combinations are possible as well.
154 The expression is lenient about whitespace around the ':',
155 following the attr, and following any extras.
156 """
158 dist: Optional['Distribution'] = None
160 def load(self):
161 """Load the entry point from its definition. If only a module
162 is indicated by the value, return that module. Otherwise,
163 return the named object.
164 """
165 match = self.pattern.match(self.value)
166 module = import_module(match.group('module'))
167 attrs = filter(None, (match.group('attr') or '').split('.'))
168 return functools.reduce(getattr, attrs, module)
170 @property
171 def module(self):
172 match = self.pattern.match(self.value)
173 return match.group('module')
175 @property
176 def attr(self):
177 match = self.pattern.match(self.value)
178 return match.group('attr')
180 @property
181 def extras(self):
182 match = self.pattern.match(self.value)
183 return list(re.finditer(r'\w+', match.group('extras') or ''))
185 def _for(self, dist):
186 self.dist = dist
187 return self
189 def __iter__(self):
190 """
191 Supply iter so one may construct dicts of EntryPoints by name.
192 """
193 msg = (
194 "Construction of dict of EntryPoints is deprecated in "
195 "favor of EntryPoints."
196 )
197 warnings.warn(msg, DeprecationWarning)
198 return iter((self.name, self))
200 def __reduce__(self):
201 return (
202 self.__class__,
203 (self.name, self.value, self.group),
204 )
206 def matches(self, **params):
207 attrs = (getattr(self, param) for param in params)
208 return all(map(operator.eq, params.values(), attrs))
211class EntryPoints(tuple):
212 """
213 An immutable collection of selectable EntryPoint objects.
214 """
216 __slots__ = ()
218 def __getitem__(self, name): # -> EntryPoint:
219 """
220 Get the EntryPoint in self matching name.
221 """
222 try:
223 return next(iter(self.select(name=name)))
224 except StopIteration:
225 raise KeyError(name)
227 def select(self, **params):
228 """
229 Select entry points from self that match the
230 given parameters (typically group and/or name).
231 """
232 return EntryPoints(ep for ep in self if ep.matches(**params))
234 @property
235 def names(self):
236 """
237 Return the set of all names of all entry points.
238 """
239 return set(ep.name for ep in self)
241 @property
242 def groups(self):
243 """
244 Return the set of all groups of all entry points.
246 For coverage while SelectableGroups is present.
247 >>> EntryPoints().groups
248 set()
249 """
250 return set(ep.group for ep in self)
252 @classmethod
253 def _from_text_for(cls, text, dist):
254 return cls(ep._for(dist) for ep in cls._from_text(text))
256 @classmethod
257 def _from_text(cls, text):
258 return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
260 @staticmethod
261 def _parse_groups(text):
262 return (
263 (item.value.name, item.value.value, item.name)
264 for item in Sectioned.section_pairs(text)
265 )
268def flake8_bypass(func):
269 # defer inspect import as performance optimization.
270 import inspect
272 is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5])
273 return func if not is_flake8 else lambda: None
276class Deprecated:
277 """
278 Compatibility add-in for mapping to indicate that
279 mapping behavior is deprecated.
281 >>> recwarn = getfixture('recwarn')
282 >>> class DeprecatedDict(Deprecated, dict): pass
283 >>> dd = DeprecatedDict(foo='bar')
284 >>> dd.get('baz', None)
285 >>> dd['foo']
286 'bar'
287 >>> list(dd)
288 ['foo']
289 >>> list(dd.keys())
290 ['foo']
291 >>> 'foo' in dd
292 True
293 >>> list(dd.values())
294 ['bar']
295 >>> len(recwarn)
296 1
297 """
299 _warn = functools.partial(
300 warnings.warn,
301 "SelectableGroups dict interface is deprecated. Use select.",
302 DeprecationWarning,
303 stacklevel=2,
304 )
306 def __getitem__(self, name):
307 self._warn()
308 return super().__getitem__(name)
310 def get(self, name, default=None):
311 flake8_bypass(self._warn)()
312 return super().get(name, default)
314 def __iter__(self):
315 self._warn()
316 return super().__iter__()
318 def __contains__(self, *args):
319 self._warn()
320 return super().__contains__(*args)
322 def keys(self):
323 self._warn()
324 return super().keys()
326 def values(self):
327 self._warn()
328 return super().values()
331class SelectableGroups(Deprecated, dict):
332 """
333 A backward- and forward-compatible result from
334 entry_points that fully implements the dict interface.
335 """
337 @classmethod
338 def load(cls, eps):
339 by_group = operator.attrgetter('group')
340 ordered = sorted(eps, key=by_group)
341 grouped = itertools.groupby(ordered, by_group)
342 return cls((group, EntryPoints(eps)) for group, eps in grouped)
344 @property
345 def _all(self):
346 """
347 Reconstruct a list of all entrypoints from the groups.
348 """
349 groups = super(Deprecated, self).values()
350 return EntryPoints(itertools.chain.from_iterable(groups))
352 @property
353 def groups(self):
354 return self._all.groups
356 @property
357 def names(self):
358 """
359 for coverage:
360 >>> SelectableGroups().names
361 set()
362 """
363 return self._all.names
365 def select(self, **params):
366 if not params:
367 return self
368 return self._all.select(**params)
371class PackagePath(pathlib.PurePosixPath):
372 """A reference to a path in a package"""
374 def read_text(self, encoding='utf-8'):
375 with self.locate().open(encoding=encoding) as stream:
376 return stream.read()
378 def read_binary(self):
379 with self.locate().open('rb') as stream:
380 return stream.read()
382 def locate(self):
383 """Return a path-like object for this path"""
384 return self.dist.locate_file(self)
387class FileHash:
388 def __init__(self, spec):
389 self.mode, _, self.value = spec.partition('=')
391 def __repr__(self):
392 return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
395_T = TypeVar("_T")
398class PackageMetadata(Protocol):
399 def __len__(self) -> int:
400 ... # pragma: no cover
402 def __contains__(self, item: str) -> bool:
403 ... # pragma: no cover
405 def __getitem__(self, key: str) -> str:
406 ... # pragma: no cover
408 def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
409 """
410 Return all values associated with a possibly multi-valued key.
411 """
414class Distribution:
415 """A Python distribution package."""
417 @abc.abstractmethod
418 def read_text(self, filename):
419 """Attempt to load metadata file given by the name.
421 :param filename: The name of the file in the distribution info.
422 :return: The text if found, otherwise None.
423 """
425 @abc.abstractmethod
426 def locate_file(self, path):
427 """
428 Given a path to a file in this distribution, return a path
429 to it.
430 """
432 @classmethod
433 def from_name(cls, name):
434 """Return the Distribution for the given package name.
436 :param name: The name of the distribution package to search for.
437 :return: The Distribution instance (or subclass thereof) for the named
438 package, if found.
439 :raises PackageNotFoundError: When the named package's distribution
440 metadata cannot be found.
441 """
442 for resolver in cls._discover_resolvers():
443 dists = resolver(DistributionFinder.Context(name=name))
444 dist = next(iter(dists), None)
445 if dist is not None:
446 return dist
447 else:
448 raise PackageNotFoundError(name)
450 @classmethod
451 def discover(cls, **kwargs):
452 """Return an iterable of Distribution objects for all packages.
454 Pass a ``context`` or pass keyword arguments for constructing
455 a context.
457 :context: A ``DistributionFinder.Context`` object.
458 :return: Iterable of Distribution objects for all packages.
459 """
460 context = kwargs.pop('context', None)
461 if context and kwargs:
462 raise ValueError("cannot accept context and kwargs")
463 context = context or DistributionFinder.Context(**kwargs)
464 return itertools.chain.from_iterable(
465 resolver(context) for resolver in cls._discover_resolvers()
466 )
468 @staticmethod
469 def at(path):
470 """Return a Distribution for the indicated metadata path
472 :param path: a string or path-like object
473 :return: a concrete Distribution instance for the path
474 """
475 return PathDistribution(pathlib.Path(path))
477 @staticmethod
478 def _discover_resolvers():
479 """Search the meta_path for resolvers."""
480 declared = (
481 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
482 )
483 return filter(None, declared)
485 @classmethod
486 def _local(cls, root='.'):
487 from pep517 import build, meta
489 system = build.compat_system(root)
490 builder = functools.partial(
491 meta.build,
492 source_dir=root,
493 system=system,
494 )
495 return PathDistribution(zipp.Path(meta.build_as_zip(builder)))
497 @property
498 def metadata(self) -> PackageMetadata:
499 """Return the parsed metadata for this Distribution.
501 The returned object will have keys that name the various bits of
502 metadata. See PEP 566 for details.
503 """
504 text = (
505 self.read_text('METADATA')
506 or self.read_text('PKG-INFO')
507 # This last clause is here to support old egg-info files. Its
508 # effect is to just end up using the PathDistribution's self._path
509 # (which points to the egg-info file) attribute unchanged.
510 or self.read_text('')
511 )
512 return email.message_from_string(text)
514 @property
515 def name(self):
516 """Return the 'Name' metadata for the distribution package."""
517 return self.metadata['Name']
519 @property
520 def version(self):
521 """Return the 'Version' metadata for the distribution package."""
522 return self.metadata['Version']
524 @property
525 def entry_points(self):
526 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
528 @property
529 def files(self):
530 """Files in this distribution.
532 :return: List of PackagePath for this distribution or None
534 Result is `None` if the metadata file that enumerates files
535 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
536 missing.
537 Result may be empty if the metadata exists but is empty.
538 """
539 file_lines = self._read_files_distinfo() or self._read_files_egginfo()
541 def make_file(name, hash=None, size_str=None):
542 result = PackagePath(name)
543 result.hash = FileHash(hash) if hash else None
544 result.size = int(size_str) if size_str else None
545 result.dist = self
546 return result
548 return file_lines and list(starmap(make_file, csv.reader(file_lines)))
550 def _read_files_distinfo(self):
551 """
552 Read the lines of RECORD
553 """
554 text = self.read_text('RECORD')
555 return text and text.splitlines()
557 def _read_files_egginfo(self):
558 """
559 SOURCES.txt might contain literal commas, so wrap each line
560 in quotes.
561 """
562 text = self.read_text('SOURCES.txt')
563 return text and map('"{}"'.format, text.splitlines())
565 @property
566 def requires(self):
567 """Generated requirements specified for this Distribution"""
568 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
569 return reqs and list(reqs)
571 def _read_dist_info_reqs(self):
572 return self.metadata.get_all('Requires-Dist')
574 def _read_egg_info_reqs(self):
575 source = self.read_text('requires.txt')
576 return source and self._deps_from_requires_text(source)
578 @classmethod
579 def _deps_from_requires_text(cls, source):
580 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
582 @staticmethod
583 def _convert_egg_info_reqs_to_simple_reqs(sections):
584 """
585 Historically, setuptools would solicit and store 'extra'
586 requirements, including those with environment markers,
587 in separate sections. More modern tools expect each
588 dependency to be defined separately, with any relevant
589 extras and environment markers attached directly to that
590 requirement. This method converts the former to the
591 latter. See _test_deps_from_requires_text for an example.
592 """
594 def make_condition(name):
595 return name and 'extra == "{name}"'.format(name=name)
597 def parse_condition(section):
598 section = section or ''
599 extra, sep, markers = section.partition(':')
600 if extra and markers:
601 markers = '({markers})'.format(markers=markers)
602 conditions = list(filter(None, [markers, make_condition(extra)]))
603 return '; ' + ' and '.join(conditions) if conditions else ''
605 for section in sections:
606 yield section.value + parse_condition(section.name)
609class DistributionFinder(MetaPathFinder):
610 """
611 A MetaPathFinder capable of discovering installed distributions.
612 """
614 class Context:
615 """
616 Keyword arguments presented by the caller to
617 ``distributions()`` or ``Distribution.discover()``
618 to narrow the scope of a search for distributions
619 in all DistributionFinders.
621 Each DistributionFinder may expect any parameters
622 and should attempt to honor the canonical
623 parameters defined below when appropriate.
624 """
626 name = None
627 """
628 Specific name for which a distribution finder should match.
629 A name of ``None`` matches all distributions.
630 """
632 def __init__(self, **kwargs):
633 vars(self).update(kwargs)
635 @property
636 def path(self):
637 """
638 The path that a distribution finder should search.
640 Typically refers to Python package paths and defaults
641 to ``sys.path``.
642 """
643 return vars(self).get('path', sys.path)
645 @abc.abstractmethod
646 def find_distributions(self, context=Context()):
647 """
648 Find distributions.
650 Return an iterable of all Distribution instances capable of
651 loading the metadata for packages matching the ``context``,
652 a DistributionFinder.Context instance.
653 """
656class FastPath:
657 """
658 Micro-optimized class for searching a path for
659 children.
660 """
662 @functools.lru_cache() # type: ignore
663 def __new__(cls, root):
664 return super().__new__(cls)
666 def __init__(self, root):
667 self.root = str(root)
669 def joinpath(self, child):
670 return pathlib.Path(self.root, child)
672 def children(self):
673 with suppress(Exception):
674 return os.listdir(self.root or '')
675 with suppress(Exception):
676 return self.zip_children()
677 return []
679 def zip_children(self):
680 zip_path = zipp.Path(self.root)
681 names = zip_path.root.namelist()
682 self.joinpath = zip_path.joinpath
684 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
686 def search(self, name):
687 return self.lookup(self.mtime).search(name)
689 @property
690 def mtime(self):
691 with suppress(OSError):
692 return os.stat(self.root).st_mtime
693 self.lookup.cache_clear()
695 @method_cache
696 def lookup(self, mtime):
697 return Lookup(self)
700class Lookup:
701 def __init__(self, path: FastPath):
702 base = os.path.basename(path.root).lower()
703 base_is_egg = base.endswith(".egg")
704 self.infos = FreezableDefaultDict(list)
705 self.eggs = FreezableDefaultDict(list)
707 for child in path.children():
708 low = child.lower()
709 if low.endswith((".dist-info", ".egg-info")):
710 # rpartition is faster than splitext and suitable for this purpose.
711 name = low.rpartition(".")[0].partition("-")[0]
712 normalized = Prepared.normalize(name)
713 self.infos[normalized].append(path.joinpath(child))
714 elif base_is_egg and low == "egg-info":
715 name = base.rpartition(".")[0].partition("-")[0]
716 legacy_normalized = Prepared.legacy_normalize(name)
717 self.eggs[legacy_normalized].append(path.joinpath(child))
719 self.infos.freeze()
720 self.eggs.freeze()
722 def search(self, prepared):
723 infos = (
724 self.infos[prepared.normalized]
725 if prepared
726 else itertools.chain.from_iterable(self.infos.values())
727 )
728 eggs = (
729 self.eggs[prepared.legacy_normalized]
730 if prepared
731 else itertools.chain.from_iterable(self.eggs.values())
732 )
733 return itertools.chain(infos, eggs)
736class Prepared:
737 """
738 A prepared search for metadata on a possibly-named package.
739 """
741 normalized = None
742 legacy_normalized = None
744 def __init__(self, name):
745 self.name = name
746 if name is None:
747 return
748 self.normalized = self.normalize(name)
749 self.legacy_normalized = self.legacy_normalize(name)
751 @staticmethod
752 def normalize(name):
753 """
754 PEP 503 normalization plus dashes as underscores.
755 """
756 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
758 @staticmethod
759 def legacy_normalize(name):
760 """
761 Normalize the package name as found in the convention in
762 older packaging tools versions and specs.
763 """
764 return name.lower().replace('-', '_')
766 def __bool__(self):
767 return bool(self.name)
770@install
771class MetadataPathFinder(NullFinder, DistributionFinder):
772 """A degenerate finder for distribution packages on the file system.
774 This finder supplies only a find_distributions() method for versions
775 of Python that do not have a PathFinder find_distributions().
776 """
778 def find_distributions(self, context=DistributionFinder.Context()):
779 """
780 Find distributions.
782 Return an iterable of all Distribution instances capable of
783 loading the metadata for packages matching ``context.name``
784 (or all names if ``None`` indicated) along the paths in the list
785 of directories ``context.path``.
786 """
787 found = self._search_paths(context.name, context.path)
788 return map(PathDistribution, found)
790 @classmethod
791 def _search_paths(cls, name, paths):
792 """Find metadata directories in paths heuristically."""
793 prepared = Prepared(name)
794 return itertools.chain.from_iterable(
795 path.search(prepared) for path in map(FastPath, paths)
796 )
798 def invalidate_caches(cls):
799 FastPath.__new__.cache_clear()
802class PathDistribution(Distribution):
803 def __init__(self, path):
804 """Construct a distribution from a path to the metadata directory.
806 :param path: A pathlib.Path or similar object supporting
807 .joinpath(), __div__, .parent, and .read_text().
808 """
809 self._path = path
811 def read_text(self, filename):
812 with suppress(
813 FileNotFoundError,
814 IsADirectoryError,
815 KeyError,
816 NotADirectoryError,
817 PermissionError,
818 ):
819 return self._path.joinpath(filename).read_text(encoding='utf-8')
821 read_text.__doc__ = Distribution.read_text.__doc__
823 def locate_file(self, path):
824 return self._path.parent / path
827def distribution(distribution_name):
828 """Get the ``Distribution`` instance for the named package.
830 :param distribution_name: The name of the distribution package as a string.
831 :return: A ``Distribution`` instance (or subclass thereof).
832 """
833 return Distribution.from_name(distribution_name)
836def distributions(**kwargs):
837 """Get all ``Distribution`` instances in the current environment.
839 :return: An iterable of ``Distribution`` instances.
840 """
841 return Distribution.discover(**kwargs)
844def metadata(distribution_name) -> PackageMetadata:
845 """Get the metadata for the named package.
847 :param distribution_name: The name of the distribution package to query.
848 :return: A PackageMetadata containing the parsed metadata.
849 """
850 return Distribution.from_name(distribution_name).metadata
853def version(distribution_name):
854 """Get the version string for the named package.
856 :param distribution_name: The name of the distribution package to query.
857 :return: The version string for the package as defined in the package's
858 "Version" metadata key.
859 """
860 return distribution(distribution_name).version
863def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
864 """Return EntryPoint objects for all installed packages.
866 Pass selection parameters (group or name) to filter the
867 result to entry points matching those properties (see
868 EntryPoints.select()).
870 For compatibility, returns ``SelectableGroups`` object unless
871 selection parameters are supplied. In the future, this function
872 will return ``EntryPoints`` instead of ``SelectableGroups``
873 even when no selection parameters are supplied.
875 For maximum future compatibility, pass selection parameters
876 or invoke ``.select`` with parameters on the result.
878 :return: EntryPoints or SelectableGroups for all installed packages.
879 """
880 unique = functools.partial(unique_everseen, key=operator.attrgetter('name'))
881 eps = itertools.chain.from_iterable(
882 dist.entry_points for dist in unique(distributions())
883 )
884 return SelectableGroups.load(eps).select(**params)
887def files(distribution_name):
888 """Return a list of files for the named package.
890 :param distribution_name: The name of the distribution package to query.
891 :return: List of files composing the distribution.
892 """
893 return distribution(distribution_name).files
896def requires(distribution_name):
897 """
898 Return a list of requirements for the named package.
900 :return: An iterator of requirements, suitable for
901 packaging.requirement.Requirement.
902 """
903 return distribution(distribution_name).requires
906def packages_distributions() -> Mapping[str, List[str]]:
907 """
908 Return a mapping of top-level packages to their
909 distributions.
911 >>> import collections.abc
912 >>> pkgs = packages_distributions()
913 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
914 True
915 """
916 pkg_to_dist = collections.defaultdict(list)
917 for dist in distributions():
918 for pkg in (dist.read_text('top_level.txt') or '').split():
919 pkg_to_dist[pkg].append(dist.metadata['Name'])
920 return dict(pkg_to_dist)