| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | import io | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | import re | 
					
						
							|  |  |  | import abc | 
					
						
							|  |  |  | import csv | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | import email | 
					
						
							|  |  |  | import pathlib | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | import zipfile | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | import operator | 
					
						
							|  |  |  | import functools | 
					
						
							|  |  |  | import itertools | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | import posixpath | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | import collections | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from configparser import ConfigParser | 
					
						
							|  |  |  | from contextlib import suppress | 
					
						
							|  |  |  | from importlib import import_module | 
					
						
							|  |  |  | from importlib.abc import MetaPathFinder | 
					
						
							|  |  |  | from itertools import starmap | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | __all__ = [ | 
					
						
							|  |  |  |     'Distribution', | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     'DistributionFinder', | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     'PackageNotFoundError', | 
					
						
							|  |  |  |     'distribution', | 
					
						
							|  |  |  |     'distributions', | 
					
						
							|  |  |  |     'entry_points', | 
					
						
							|  |  |  |     'files', | 
					
						
							|  |  |  |     'metadata', | 
					
						
							|  |  |  |     'requires', | 
					
						
							|  |  |  |     'version', | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class PackageNotFoundError(ModuleNotFoundError): | 
					
						
							|  |  |  |     """The package was not found.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-10 20:05:10 -05:00
										 |  |  | class EntryPoint( | 
					
						
							|  |  |  |         collections.namedtuple('EntryPointBase', 'name value group')): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """An entry point as defined by Python packaging conventions.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     See `the packaging docs on entry points | 
					
						
							|  |  |  |     <https://packaging.python.org/specifications/entry-points/>`_ | 
					
						
							|  |  |  |     for more information. | 
					
						
							| 
									
										
										
										
											2022-03-13 17:30:07 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     >>> ep = EntryPoint( | 
					
						
							|  |  |  |     ...     name=None, group=None, value='package.module:attr [extra1, extra2]') | 
					
						
							|  |  |  |     >>> ep.module | 
					
						
							|  |  |  |     'package.module' | 
					
						
							|  |  |  |     >>> ep.attr | 
					
						
							|  |  |  |     'attr' | 
					
						
							|  |  |  |     >>> ep.extras | 
					
						
							|  |  |  |     ['extra1', 'extra2'] | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     pattern = re.compile( | 
					
						
							|  |  |  |         r'(?P<module>[\w.]+)\s*' | 
					
						
							| 
									
										
										
										
											2022-01-23 10:17:41 -05:00
										 |  |  |         r'(:\s*(?P<attr>[\w.]+)\s*)?' | 
					
						
							|  |  |  |         r'((?P<extras>\[.*\])\s*)?$' | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         ) | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     A regular expression describing the syntax for an entry point, | 
					
						
							|  |  |  |     which might look like: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         - module | 
					
						
							|  |  |  |         - package.module | 
					
						
							|  |  |  |         - package.module:attribute | 
					
						
							|  |  |  |         - package.module:object.attribute | 
					
						
							|  |  |  |         - package.module:attr [extra1, extra2] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Other combinations are possible as well. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     The expression is lenient about whitespace around the ':', | 
					
						
							|  |  |  |     following the attr, and following any extras. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def load(self): | 
					
						
							|  |  |  |         """Load the entry point from its definition. If only a module
 | 
					
						
							|  |  |  |         is indicated by the value, return that module. Otherwise, | 
					
						
							|  |  |  |         return the named object. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         match = self.pattern.match(self.value) | 
					
						
							|  |  |  |         module = import_module(match.group('module')) | 
					
						
							|  |  |  |         attrs = filter(None, (match.group('attr') or '').split('.')) | 
					
						
							|  |  |  |         return functools.reduce(getattr, attrs, module) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 14:46:24 -07:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def module(self): | 
					
						
							|  |  |  |         match = self.pattern.match(self.value) | 
					
						
							|  |  |  |         return match.group('module') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def attr(self): | 
					
						
							|  |  |  |         match = self.pattern.match(self.value) | 
					
						
							|  |  |  |         return match.group('attr') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def extras(self): | 
					
						
							|  |  |  |         match = self.pattern.match(self.value) | 
					
						
							| 
									
										
										
										
											2022-03-13 17:30:07 -04:00
										 |  |  |         return re.findall(r'\w+', match.group('extras') or '') | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def _from_config(cls, config): | 
					
						
							|  |  |  |         return [ | 
					
						
							|  |  |  |             cls(name, value, group) | 
					
						
							|  |  |  |             for group in config.sections() | 
					
						
							|  |  |  |             for name, value in config.items(group) | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def _from_text(cls, text): | 
					
						
							| 
									
										
										
										
											2019-07-28 14:59:24 -04:00
										 |  |  |         config = ConfigParser(delimiters='=') | 
					
						
							| 
									
										
										
										
											2019-06-07 14:23:39 -07:00
										 |  |  |         # case sensitive: https://stackoverflow.com/q/1611799/812183 | 
					
						
							|  |  |  |         config.optionxform = str | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         try: | 
					
						
							|  |  |  |             config.read_string(text) | 
					
						
							|  |  |  |         except AttributeError:  # pragma: nocover | 
					
						
							|  |  |  |             # Python 2 has no read_string | 
					
						
							|  |  |  |             config.readfp(io.StringIO(text)) | 
					
						
							|  |  |  |         return EntryPoint._from_config(config) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __iter__(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Supply iter so one may construct dicts of EntryPoints easily. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return iter((self.name, self)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-10 20:05:10 -05:00
										 |  |  |     def __reduce__(self): | 
					
						
							|  |  |  |         return ( | 
					
						
							|  |  |  |             self.__class__, | 
					
						
							|  |  |  |             (self.name, self.value, self.group), | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | class PackagePath(pathlib.PurePosixPath): | 
					
						
							|  |  |  |     """A reference to a path in a package""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read_text(self, encoding='utf-8'): | 
					
						
							|  |  |  |         with self.locate().open(encoding=encoding) as stream: | 
					
						
							|  |  |  |             return stream.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read_binary(self): | 
					
						
							|  |  |  |         with self.locate().open('rb') as stream: | 
					
						
							|  |  |  |             return stream.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def locate(self): | 
					
						
							|  |  |  |         """Return a path-like object for this path""" | 
					
						
							|  |  |  |         return self.dist.locate_file(self) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class FileHash: | 
					
						
							|  |  |  |     def __init__(self, spec): | 
					
						
							|  |  |  |         self.mode, _, self.value = spec.partition('=') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __repr__(self): | 
					
						
							|  |  |  |         return '<FileHash mode: {} value: {}>'.format(self.mode, self.value) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Distribution: | 
					
						
							|  |  |  |     """A Python distribution package.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def read_text(self, filename): | 
					
						
							|  |  |  |         """Attempt to load metadata file given by the name.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         :param filename: The name of the file in the distribution info. | 
					
						
							|  |  |  |         :return: The text if found, otherwise None. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def locate_file(self, path): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Given a path to a file in this distribution, return a path | 
					
						
							|  |  |  |         to it. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_name(cls, name): | 
					
						
							|  |  |  |         """Return the Distribution for the given package name.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         :param name: The name of the distribution package to search for. | 
					
						
							|  |  |  |         :return: The Distribution instance (or subclass thereof) for the named | 
					
						
							|  |  |  |             package, if found. | 
					
						
							|  |  |  |         :raises PackageNotFoundError: When the named package's distribution | 
					
						
							|  |  |  |             metadata cannot be found. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         for resolver in cls._discover_resolvers(): | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |             dists = resolver(DistributionFinder.Context(name=name)) | 
					
						
							| 
									
										
										
										
											2020-06-05 14:46:24 -07:00
										 |  |  |             dist = next(iter(dists), None) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |             if dist is not None: | 
					
						
							|  |  |  |                 return dist | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise PackageNotFoundError(name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     def discover(cls, **kwargs): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         """Return an iterable of Distribution objects for all packages.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         Pass a ``context`` or pass keyword arguments for constructing | 
					
						
							|  |  |  |         a context. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         :context: A ``DistributionFinder.Context`` object. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         :return: Iterable of Distribution objects for all packages. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         context = kwargs.pop('context', None) | 
					
						
							|  |  |  |         if context and kwargs: | 
					
						
							|  |  |  |             raise ValueError("cannot accept context and kwargs") | 
					
						
							|  |  |  |         context = context or DistributionFinder.Context(**kwargs) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         return itertools.chain.from_iterable( | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |             resolver(context) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |             for resolver in cls._discover_resolvers() | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     @staticmethod | 
					
						
							|  |  |  |     def at(path): | 
					
						
							|  |  |  |         """Return a Distribution for the indicated metadata path
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         :param path: a string or path-like object | 
					
						
							|  |  |  |         :return: a concrete Distribution instance for the path | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return PathDistribution(pathlib.Path(path)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     @staticmethod | 
					
						
							|  |  |  |     def _discover_resolvers(): | 
					
						
							|  |  |  |         """Search the meta_path for resolvers.""" | 
					
						
							|  |  |  |         declared = ( | 
					
						
							|  |  |  |             getattr(finder, 'find_distributions', None) | 
					
						
							|  |  |  |             for finder in sys.meta_path | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         return filter(None, declared) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 14:46:24 -07:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def _local(cls, root='.'): | 
					
						
							|  |  |  |         from pep517 import build, meta | 
					
						
							|  |  |  |         system = build.compat_system(root) | 
					
						
							|  |  |  |         builder = functools.partial( | 
					
						
							|  |  |  |             meta.build, | 
					
						
							|  |  |  |             source_dir=root, | 
					
						
							|  |  |  |             system=system, | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         return PathDistribution(zipfile.Path(meta.build_as_zip(builder))) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def metadata(self): | 
					
						
							|  |  |  |         """Return the parsed metadata for this Distribution.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         The returned object will have keys that name the various bits of | 
					
						
							|  |  |  |         metadata.  See PEP 566 for details. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         text = ( | 
					
						
							|  |  |  |             self.read_text('METADATA') | 
					
						
							|  |  |  |             or self.read_text('PKG-INFO') | 
					
						
							|  |  |  |             # This last clause is here to support old egg-info files.  Its | 
					
						
							|  |  |  |             # effect is to just end up using the PathDistribution's self._path | 
					
						
							|  |  |  |             # (which points to the egg-info file) attribute unchanged. | 
					
						
							|  |  |  |             or self.read_text('') | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         return email.message_from_string(text) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def version(self): | 
					
						
							|  |  |  |         """Return the 'Version' metadata for the distribution package.""" | 
					
						
							|  |  |  |         return self.metadata['Version'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def entry_points(self): | 
					
						
							|  |  |  |         return EntryPoint._from_text(self.read_text('entry_points.txt')) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def files(self): | 
					
						
							| 
									
										
										
										
											2019-09-02 11:08:03 -04:00
										 |  |  |         """Files in this distribution.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         :return: List of PackagePath for this distribution or None | 
					
						
							| 
									
										
										
										
											2019-09-02 11:08:03 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         Result is `None` if the metadata file that enumerates files | 
					
						
							|  |  |  |         (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is | 
					
						
							|  |  |  |         missing. | 
					
						
							|  |  |  |         Result may be empty if the metadata exists but is empty. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         file_lines = self._read_files_distinfo() or self._read_files_egginfo() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def make_file(name, hash=None, size_str=None): | 
					
						
							|  |  |  |             result = PackagePath(name) | 
					
						
							|  |  |  |             result.hash = FileHash(hash) if hash else None | 
					
						
							|  |  |  |             result.size = int(size_str) if size_str else None | 
					
						
							|  |  |  |             result.dist = self | 
					
						
							|  |  |  |             return result | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         return file_lines and list(starmap(make_file, csv.reader(file_lines))) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _read_files_distinfo(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Read the lines of RECORD | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         text = self.read_text('RECORD') | 
					
						
							|  |  |  |         return text and text.splitlines() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _read_files_egginfo(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         SOURCES.txt might contain literal commas, so wrap each line | 
					
						
							|  |  |  |         in quotes. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         text = self.read_text('SOURCES.txt') | 
					
						
							|  |  |  |         return text and map('"{}"'.format, text.splitlines()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def requires(self): | 
					
						
							|  |  |  |         """Generated requirements specified for this Distribution""" | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() | 
					
						
							|  |  |  |         return reqs and list(reqs) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _read_dist_info_reqs(self): | 
					
						
							| 
									
										
										
										
											2019-09-02 11:08:03 -04:00
										 |  |  |         return self.metadata.get_all('Requires-Dist') | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _read_egg_info_reqs(self): | 
					
						
							|  |  |  |         source = self.read_text('requires.txt') | 
					
						
							| 
									
										
										
										
											2022-03-13 17:30:07 -04:00
										 |  |  |         return None if source is None else self._deps_from_requires_text(source) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def _deps_from_requires_text(cls, source): | 
					
						
							|  |  |  |         section_pairs = cls._read_sections(source.splitlines()) | 
					
						
							|  |  |  |         sections = { | 
					
						
							|  |  |  |             section: list(map(operator.itemgetter('line'), results)) | 
					
						
							|  |  |  |             for section, results in | 
					
						
							|  |  |  |             itertools.groupby(section_pairs, operator.itemgetter('section')) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         return cls._convert_egg_info_reqs_to_simple_reqs(sections) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def _read_sections(lines): | 
					
						
							|  |  |  |         section = None | 
					
						
							|  |  |  |         for line in filter(None, lines): | 
					
						
							|  |  |  |             section_match = re.match(r'\[(.*)\]$', line) | 
					
						
							|  |  |  |             if section_match: | 
					
						
							|  |  |  |                 section = section_match.group(1) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             yield locals() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def _convert_egg_info_reqs_to_simple_reqs(sections): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Historically, setuptools would solicit and store 'extra' | 
					
						
							|  |  |  |         requirements, including those with environment markers, | 
					
						
							|  |  |  |         in separate sections. More modern tools expect each | 
					
						
							|  |  |  |         dependency to be defined separately, with any relevant | 
					
						
							|  |  |  |         extras and environment markers attached directly to that | 
					
						
							|  |  |  |         requirement. This method converts the former to the | 
					
						
							|  |  |  |         latter. See _test_deps_from_requires_text for an example. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         def make_condition(name): | 
					
						
							|  |  |  |             return name and 'extra == "{name}"'.format(name=name) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 19:58:19 -05:00
										 |  |  |         def quoted_marker(section): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |             section = section or '' | 
					
						
							|  |  |  |             extra, sep, markers = section.partition(':') | 
					
						
							|  |  |  |             if extra and markers: | 
					
						
							| 
									
										
										
										
											2021-12-16 19:58:19 -05:00
										 |  |  |                 markers = f'({markers})' | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |             conditions = list(filter(None, [markers, make_condition(extra)])) | 
					
						
							|  |  |  |             return '; ' + ' and '.join(conditions) if conditions else '' | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 19:58:19 -05:00
										 |  |  |         def url_req_space(req): | 
					
						
							|  |  |  |             """
 | 
					
						
							|  |  |  |             PEP 508 requires a space between the url_spec and the quoted_marker. | 
					
						
							|  |  |  |             Ref python/importlib_metadata#357. | 
					
						
							|  |  |  |             """
 | 
					
						
							|  |  |  |             # '@' is uniquely indicative of a url_req. | 
					
						
							|  |  |  |             return ' ' * ('@' in req) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         for section, deps in sections.items(): | 
					
						
							|  |  |  |             for dep in deps: | 
					
						
							| 
									
										
										
										
											2021-12-16 19:58:19 -05:00
										 |  |  |                 space = url_req_space(dep) | 
					
						
							|  |  |  |                 yield dep + space + quoted_marker(section) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class DistributionFinder(MetaPathFinder): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     A MetaPathFinder capable of discovering installed distributions. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     class Context: | 
					
						
							| 
									
										
										
										
											2019-12-10 20:05:10 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Keyword arguments presented by the caller to | 
					
						
							|  |  |  |         ``distributions()`` or ``Distribution.discover()`` | 
					
						
							|  |  |  |         to narrow the scope of a search for distributions | 
					
						
							|  |  |  |         in all DistributionFinders. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Each DistributionFinder may expect any parameters | 
					
						
							|  |  |  |         and should attempt to honor the canonical | 
					
						
							|  |  |  |         parameters defined below when appropriate. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         name = None | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Specific name for which a distribution finder should match. | 
					
						
							| 
									
										
										
										
											2019-12-10 20:05:10 -05:00
										 |  |  |         A name of ``None`` matches all distributions. | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def __init__(self, **kwargs): | 
					
						
							|  |  |  |             vars(self).update(kwargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         @property | 
					
						
							|  |  |  |         def path(self): | 
					
						
							|  |  |  |             """
 | 
					
						
							|  |  |  |             The path that a distribution finder should search. | 
					
						
							| 
									
										
										
										
											2019-12-10 20:05:10 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |             Typically refers to Python package paths and defaults | 
					
						
							|  |  |  |             to ``sys.path``. | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |             """
 | 
					
						
							|  |  |  |             return vars(self).get('path', sys.path) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     @abc.abstractmethod | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     def find_distributions(self, context=Context()): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Find distributions. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Return an iterable of all Distribution instances capable of | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |         loading the metadata for packages matching the ``context``, | 
					
						
							|  |  |  |         a DistributionFinder.Context instance. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | class FastPath: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Micro-optimized class for searching a path for | 
					
						
							|  |  |  |     children. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, root): | 
					
						
							|  |  |  |         self.root = root | 
					
						
							| 
									
										
										
										
											2020-06-05 14:46:24 -07:00
										 |  |  |         self.base = os.path.basename(self.root).lower() | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def joinpath(self, child): | 
					
						
							|  |  |  |         return pathlib.Path(self.root, child) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def children(self): | 
					
						
							|  |  |  |         with suppress(Exception): | 
					
						
							| 
									
										
										
										
											2021-11-13 15:07:22 -05:00
										 |  |  |             return os.listdir(self.root or '.') | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |         with suppress(Exception): | 
					
						
							|  |  |  |             return self.zip_children() | 
					
						
							|  |  |  |         return [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def zip_children(self): | 
					
						
							|  |  |  |         zip_path = zipfile.Path(self.root) | 
					
						
							|  |  |  |         names = zip_path.root.namelist() | 
					
						
							|  |  |  |         self.joinpath = zip_path.joinpath | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 14:46:24 -07:00
										 |  |  |         return dict.fromkeys( | 
					
						
							|  |  |  |             child.split(posixpath.sep, 1)[0] | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |             for child in names | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def is_egg(self, search): | 
					
						
							| 
									
										
										
										
											2020-02-11 21:58:47 -05:00
										 |  |  |         base = self.base | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |         return ( | 
					
						
							| 
									
										
										
										
											2020-02-11 21:58:47 -05:00
										 |  |  |             base == search.versionless_egg_name | 
					
						
							|  |  |  |             or base.startswith(search.prefix) | 
					
						
							|  |  |  |             and base.endswith('.egg')) | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def search(self, name): | 
					
						
							|  |  |  |         for child in self.children(): | 
					
						
							|  |  |  |             n_low = child.lower() | 
					
						
							|  |  |  |             if (n_low in name.exact_matches | 
					
						
							|  |  |  |                     or n_low.startswith(name.prefix) | 
					
						
							|  |  |  |                     and n_low.endswith(name.suffixes) | 
					
						
							|  |  |  |                     # legacy case: | 
					
						
							|  |  |  |                     or self.is_egg(name) and n_low == 'egg-info'): | 
					
						
							|  |  |  |                 yield self.joinpath(child) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Prepared: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     A prepared search for metadata on a possibly-named package. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     normalized = '' | 
					
						
							|  |  |  |     prefix = '' | 
					
						
							|  |  |  |     suffixes = '.dist-info', '.egg-info' | 
					
						
							|  |  |  |     exact_matches = [''][:0] | 
					
						
							| 
									
										
										
										
											2020-02-11 21:58:47 -05:00
										 |  |  |     versionless_egg_name = '' | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, name): | 
					
						
							|  |  |  |         self.name = name | 
					
						
							|  |  |  |         if name is None: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self.normalized = name.lower().replace('-', '_') | 
					
						
							|  |  |  |         self.prefix = self.normalized + '-' | 
					
						
							|  |  |  |         self.exact_matches = [ | 
					
						
							|  |  |  |             self.normalized + suffix for suffix in self.suffixes] | 
					
						
							| 
									
										
										
										
											2020-02-11 21:58:47 -05:00
										 |  |  |         self.versionless_egg_name = self.normalized + '.egg' | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | class MetadataPathFinder(DistributionFinder): | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def find_distributions(cls, context=DistributionFinder.Context()): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Find distributions. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Return an iterable of all Distribution instances capable of | 
					
						
							|  |  |  |         loading the metadata for packages matching ``context.name`` | 
					
						
							|  |  |  |         (or all names if ``None`` indicated) along the paths in the list | 
					
						
							|  |  |  |         of directories ``context.path``. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |         found = cls._search_paths(context.name, context.path) | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |         return map(PathDistribution, found) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |     def _search_paths(cls, name, paths): | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |         """Find metadata directories in paths heuristically.""" | 
					
						
							|  |  |  |         return itertools.chain.from_iterable( | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |             path.search(Prepared(name)) | 
					
						
							|  |  |  |             for path in map(FastPath, paths) | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | class PathDistribution(Distribution): | 
					
						
							|  |  |  |     def __init__(self, path): | 
					
						
							| 
									
										
										
										
											2019-09-02 11:08:03 -04:00
										 |  |  |         """Construct a distribution from a path to the metadata directory.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         :param path: A pathlib.Path or similar object supporting | 
					
						
							|  |  |  |                      .joinpath(), __div__, .parent, and .read_text(). | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         self._path = path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read_text(self, filename): | 
					
						
							| 
									
										
										
										
											2019-05-29 17:13:12 -07:00
										 |  |  |         with suppress(FileNotFoundError, IsADirectoryError, KeyError, | 
					
						
							|  |  |  |                       NotADirectoryError, PermissionError): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |             return self._path.joinpath(filename).read_text(encoding='utf-8') | 
					
						
							|  |  |  |     read_text.__doc__ = Distribution.read_text.__doc__ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def locate_file(self, path): | 
					
						
							|  |  |  |         return self._path.parent / path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | def distribution(distribution_name): | 
					
						
							|  |  |  |     """Get the ``Distribution`` instance for the named package.
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     :param distribution_name: The name of the distribution package as a string. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     :return: A ``Distribution`` instance (or subclass thereof). | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     return Distribution.from_name(distribution_name) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  | def distributions(**kwargs): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """Get all ``Distribution`` instances in the current environment.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     :return: An iterable of ``Distribution`` instances. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     return Distribution.discover(**kwargs) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | def metadata(distribution_name): | 
					
						
							|  |  |  |     """Get the metadata for the named package.
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     :param distribution_name: The name of the distribution package to query. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     :return: An email.Message containing the parsed metadata. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     return Distribution.from_name(distribution_name).metadata | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | def version(distribution_name): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """Get the version string for the named package.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     :param distribution_name: The name of the distribution package to query. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     :return: The version string for the package as defined in the package's | 
					
						
							|  |  |  |         "Version" metadata key. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     return distribution(distribution_name).version | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def entry_points(): | 
					
						
							|  |  |  |     """Return EntryPoint objects for all installed packages.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     :return: EntryPoint objects for all installed packages. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     eps = itertools.chain.from_iterable( | 
					
						
							|  |  |  |         dist.entry_points for dist in distributions()) | 
					
						
							|  |  |  |     by_group = operator.attrgetter('group') | 
					
						
							|  |  |  |     ordered = sorted(eps, key=by_group) | 
					
						
							|  |  |  |     grouped = itertools.groupby(ordered, by_group) | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |         group: tuple(eps) | 
					
						
							|  |  |  |         for group, eps in grouped | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | def files(distribution_name): | 
					
						
							|  |  |  |     """Return a list of files for the named package.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     :param distribution_name: The name of the distribution package to query. | 
					
						
							|  |  |  |     :return: List of files composing the distribution. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     return distribution(distribution_name).files | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | def requires(distribution_name): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     Return a list of requirements for the named package. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     :return: An iterator of requirements, suitable for | 
					
						
							|  |  |  |     packaging.requirement.Requirement. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     return distribution(distribution_name).requires |