| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  | import textwrap | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | import warnings | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | import functools | 
					
						
							|  |  |  | import itertools | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | import posixpath | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  | import collections | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-02 17:03:40 -04:00
										 |  |  | from . import _adapters, _meta | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  | from ._collections import FreezableDefaultDict, Pair | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  | from ._functools import method_cache, pass_none | 
					
						
							|  |  |  | from ._itertools import always_iterable, unique_everseen | 
					
						
							| 
									
										
										
										
											2021-05-26 13:40:05 -04:00
										 |  |  | from ._meta import PackageMetadata, SimplePath | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | from contextlib import suppress | 
					
						
							|  |  |  | from importlib import import_module | 
					
						
							|  |  |  | from importlib.abc import MetaPathFinder | 
					
						
							|  |  |  | from itertools import starmap | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  | from typing import List, Mapping, Optional | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | __all__ = [ | 
					
						
							|  |  |  |     'Distribution', | 
					
						
							| 
									
										
										
										
											2019-09-10 14:53:31 +01:00
										 |  |  |     'DistributionFinder', | 
					
						
							| 
									
										
										
										
											2021-05-23 20:04:05 +01:00
										 |  |  |     'PackageMetadata', | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     'PackageNotFoundError', | 
					
						
							|  |  |  |     'distribution', | 
					
						
							|  |  |  |     'distributions', | 
					
						
							|  |  |  |     'entry_points', | 
					
						
							|  |  |  |     'files', | 
					
						
							|  |  |  |     'metadata', | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  |     'packages_distributions', | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     'requires', | 
					
						
							|  |  |  |     'version', | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  | ] | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class PackageNotFoundError(ModuleNotFoundError): | 
					
						
							|  |  |  |     """The package was not found.""" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-19 14:14:21 -07:00
										 |  |  |     def __str__(self): | 
					
						
							| 
									
										
										
										
											2021-05-26 14:49:06 -04:00
										 |  |  |         return f"No package metadata was found for {self.name}" | 
					
						
							| 
									
										
										
										
											2020-10-19 14:14:21 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def name(self): | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |         (name,) = self.args | 
					
						
							| 
									
										
										
										
											2020-10-19 14:14:21 -07:00
										 |  |  |         return name | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  | class Sectioned: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     A simple entry point config parser for performance | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     >>> for item in Sectioned.read(Sectioned._sample): | 
					
						
							|  |  |  |     ...     print(item) | 
					
						
							|  |  |  |     Pair(name='sec1', value='# comments ignored') | 
					
						
							|  |  |  |     Pair(name='sec1', value='a = 1') | 
					
						
							|  |  |  |     Pair(name='sec1', value='b = 2') | 
					
						
							|  |  |  |     Pair(name='sec2', value='a = 2') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     >>> res = Sectioned.section_pairs(Sectioned._sample) | 
					
						
							|  |  |  |     >>> item = next(res) | 
					
						
							|  |  |  |     >>> item.name | 
					
						
							|  |  |  |     'sec1' | 
					
						
							|  |  |  |     >>> item.value | 
					
						
							|  |  |  |     Pair(name='a', value='1') | 
					
						
							|  |  |  |     >>> item = next(res) | 
					
						
							|  |  |  |     >>> item.value | 
					
						
							|  |  |  |     Pair(name='b', value='2') | 
					
						
							|  |  |  |     >>> item = next(res) | 
					
						
							|  |  |  |     >>> item.name | 
					
						
							|  |  |  |     'sec2' | 
					
						
							|  |  |  |     >>> item.value | 
					
						
							|  |  |  |     Pair(name='a', value='2') | 
					
						
							|  |  |  |     >>> list(res) | 
					
						
							|  |  |  |     [] | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _sample = textwrap.dedent( | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         [sec1] | 
					
						
							|  |  |  |         # comments ignored | 
					
						
							|  |  |  |         a = 1 | 
					
						
							|  |  |  |         b = 2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         [sec2] | 
					
						
							|  |  |  |         a = 2 | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |     ).lstrip() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def section_pairs(cls, text): | 
					
						
							|  |  |  |         return ( | 
					
						
							|  |  |  |             section._replace(value=Pair.parse(section.value)) | 
					
						
							|  |  |  |             for section in cls.read(text, filter_=cls.valid) | 
					
						
							|  |  |  |             if section.name is not None | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def read(text, filter_=None): | 
					
						
							|  |  |  |         lines = filter(filter_, map(str.strip, text.splitlines())) | 
					
						
							|  |  |  |         name = None | 
					
						
							|  |  |  |         for value in lines: | 
					
						
							|  |  |  |             section_match = value.startswith('[') and value.endswith(']') | 
					
						
							|  |  |  |             if section_match: | 
					
						
							|  |  |  |                 name = value.strip('[]') | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             yield Pair(name, value) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def valid(line): | 
					
						
							|  |  |  |         return line and not line.startswith('#') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  | class DeprecatedTuple: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Provide subscript item access for backward compatibility. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     >>> recwarn = getfixture('recwarn') | 
					
						
							|  |  |  |     >>> ep = EntryPoint(name='name', value='value', group='group') | 
					
						
							|  |  |  |     >>> ep[:] | 
					
						
							|  |  |  |     ('name', 'value', 'group') | 
					
						
							|  |  |  |     >>> ep[0] | 
					
						
							|  |  |  |     'name' | 
					
						
							|  |  |  |     >>> len(recwarn) | 
					
						
							|  |  |  |     1 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  |     # Do not remove prior to 2023-05-01 or Python 3.13 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |     _warn = functools.partial( | 
					
						
							|  |  |  |         warnings.warn, | 
					
						
							|  |  |  |         "EntryPoint tuple interface is deprecated. Access members by name.", | 
					
						
							|  |  |  |         DeprecationWarning, | 
					
						
							|  |  |  |         stacklevel=2, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __getitem__(self, item): | 
					
						
							|  |  |  |         self._warn() | 
					
						
							|  |  |  |         return self._key()[item] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class EntryPoint(DeprecatedTuple): | 
					
						
							| 
									
										
										
										
											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 15:53:29 -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-22 23:00:23 -05:00
										 |  |  |         r'(:\s*(?P<attr>[\w.]+)\s*)?' | 
					
						
							|  |  |  |         r'((?P<extras>\[.*\])\s*)?$' | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |     ) | 
					
						
							| 
									
										
										
										
											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. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  |     name: str | 
					
						
							|  |  |  |     value: str | 
					
						
							|  |  |  |     group: str | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |     dist: Optional['Distribution'] = None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |     def __init__(self, name, value, group): | 
					
						
							|  |  |  |         vars(self).update(name=name, value=value, group=group) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     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 16:34:16 -04: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 15:53:29 -04:00
										 |  |  |         return re.findall(r'\w+', match.group('extras') or '') | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |     def _for(self, dist): | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |         vars(self).update(dist=dist) | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |         return self | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |     def matches(self, **params): | 
					
						
							| 
									
										
										
										
											2022-03-13 15:53:29 -04:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         EntryPoint matches the given parameters. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]') | 
					
						
							|  |  |  |         >>> ep.matches(group='foo') | 
					
						
							|  |  |  |         True | 
					
						
							|  |  |  |         >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]') | 
					
						
							|  |  |  |         True | 
					
						
							|  |  |  |         >>> ep.matches(group='foo', name='other') | 
					
						
							|  |  |  |         False | 
					
						
							|  |  |  |         >>> ep.matches() | 
					
						
							|  |  |  |         True | 
					
						
							|  |  |  |         >>> ep.matches(extras=['extra1', 'extra2']) | 
					
						
							|  |  |  |         True | 
					
						
							|  |  |  |         >>> ep.matches(module='bing') | 
					
						
							|  |  |  |         True | 
					
						
							|  |  |  |         >>> ep.matches(attr='bong') | 
					
						
							|  |  |  |         True | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |         attrs = (getattr(self, param) for param in params) | 
					
						
							|  |  |  |         return all(map(operator.eq, params.values(), attrs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |     def _key(self): | 
					
						
							|  |  |  |         return self.name, self.value, self.group | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __lt__(self, other): | 
					
						
							|  |  |  |         return self._key() < other._key() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __eq__(self, other): | 
					
						
							|  |  |  |         return self._key() == other._key() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __setattr__(self, name, value): | 
					
						
							|  |  |  |         raise AttributeError("EntryPoint objects are immutable.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __repr__(self): | 
					
						
							|  |  |  |         return ( | 
					
						
							|  |  |  |             f'EntryPoint(name={self.name!r}, value={self.value!r}, ' | 
					
						
							|  |  |  |             f'group={self.group!r})' | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __hash__(self): | 
					
						
							|  |  |  |         return hash(self._key()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  | class EntryPoints(tuple): | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     An immutable collection of selectable EntryPoint objects. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     __slots__ = () | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __getitem__(self, name):  # -> EntryPoint: | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Get the EntryPoint in self matching name. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |         try: | 
					
						
							|  |  |  |             return next(iter(self.select(name=name))) | 
					
						
							|  |  |  |         except StopIteration: | 
					
						
							|  |  |  |             raise KeyError(name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def select(self, **params): | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Select entry points from self that match the | 
					
						
							|  |  |  |         given parameters (typically group and/or name). | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |         return EntryPoints(ep for ep in self if ep.matches(**params)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def names(self): | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Return the set of all names of all entry points. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |         return {ep.name for ep in self} | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def groups(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  |         Return the set of all groups of all entry points. | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |         return {ep.group for ep in self} | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def _from_text_for(cls, text, dist): | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         return cls(ep._for(dist) for ep in cls._from_text(text)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |     def _from_text(text): | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         return ( | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |             EntryPoint(name=item.value.name, value=item.value.value, group=item.name) | 
					
						
							|  |  |  |             for item in Sectioned.section_pairs(text or '') | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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): | 
					
						
							| 
									
										
										
										
											2021-05-26 14:49:06 -04:00
										 |  |  |         return f'<FileHash mode: {self.mode} value: {self.value}>' | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |     def from_name(cls, name: str): | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         """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. | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |         :raises ValueError: When an invalid value is supplied for name. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |         if not name: | 
					
						
							|  |  |  |             raise ValueError("A distribution name is required.") | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             return next(cls.discover(name=name)) | 
					
						
							|  |  |  |         except StopIteration: | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |             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( | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |             resolver(context) for resolver in cls._discover_resolvers() | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 = ( | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |             getattr(finder, 'find_distributions', None) for finder in sys.meta_path | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         return filter(None, declared) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							| 
									
										
										
										
											2021-05-02 17:03:40 -04:00
										 |  |  |     def metadata(self) -> _meta.PackageMetadata: | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         """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('') | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2021-05-02 17:03:40 -04:00
										 |  |  |         return _adapters.Message(email.message_from_string(text)) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def name(self): | 
					
						
							|  |  |  |         """Return the 'Name' metadata for the distribution package.""" | 
					
						
							|  |  |  |         return self.metadata['Name'] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-31 11:52:29 -04:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def _normalized_name(self): | 
					
						
							|  |  |  |         """Return a normalized version of the name.""" | 
					
						
							|  |  |  |         return Prepared.normalize(self.name) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def version(self): | 
					
						
							|  |  |  |         """Return the 'Version' metadata for the distribution package.""" | 
					
						
							|  |  |  |         return self.metadata['Version'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def entry_points(self): | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |         return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @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
										 |  |  | 
 | 
					
						
							|  |  |  |         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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |         @pass_none | 
					
						
							|  |  |  |         def make_files(lines): | 
					
						
							|  |  |  |             return list(starmap(make_file, csv.reader(lines))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return make_files(self._read_files_distinfo() or self._read_files_egginfo()) | 
					
						
							| 
									
										
										
										
											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 15:53:29 -04:00
										 |  |  |         return pass_none(self._deps_from_requires_text)(source) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def _deps_from_requires_text(cls, source): | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source)) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @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. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         def make_condition(name): | 
					
						
							| 
									
										
										
										
											2021-05-26 14:49:06 -04:00
										 |  |  |             return name and f'extra == "{name}"' | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 15:48:35 -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-05-26 14:49:06 -04: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 15:48:35 -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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         for section in sections: | 
					
						
							| 
									
										
										
										
											2021-12-16 15:48:35 -05:00
										 |  |  |             space = url_req_space(section.value) | 
					
						
							|  |  |  |             yield section.value + space + quoted_marker(section.name) | 
					
						
							| 
									
										
										
										
											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): | 
					
						
							|  |  |  |             """
 | 
					
						
							| 
									
										
										
										
											2021-05-26 13:40:05 -04:00
										 |  |  |             The sequence of directory path that a distribution finder | 
					
						
							|  |  |  |             should search. | 
					
						
							| 
									
										
										
										
											2019-12-10 20:05:10 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-26 13:40:05 -04:00
										 |  |  |             Typically refers to Python installed package paths such as | 
					
						
							|  |  |  |             "site-packages" directories 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. | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     >>> FastPath('').children() | 
					
						
							|  |  |  |     ['...'] | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |     @functools.lru_cache()  # type: ignore | 
					
						
							|  |  |  |     def __new__(cls, root): | 
					
						
							|  |  |  |         return super().__new__(cls) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  |     def __init__(self, root): | 
					
						
							|  |  |  |         self.root = root | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def joinpath(self, child): | 
					
						
							|  |  |  |         return pathlib.Path(self.root, child) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def children(self): | 
					
						
							|  |  |  |         with suppress(Exception): | 
					
						
							| 
									
										
										
										
											2021-11-09 18:49:43 -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-12-31 12:56:43 -05:00
										 |  |  |         return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names) | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def search(self, name): | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         return self.lookup(self.mtime).search(name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def mtime(self): | 
					
						
							|  |  |  |         with suppress(OSError): | 
					
						
							|  |  |  |             return os.stat(self.root).st_mtime | 
					
						
							|  |  |  |         self.lookup.cache_clear() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @method_cache | 
					
						
							|  |  |  |     def lookup(self, mtime): | 
					
						
							|  |  |  |         return Lookup(self) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Lookup: | 
					
						
							|  |  |  |     def __init__(self, path: FastPath): | 
					
						
							|  |  |  |         base = os.path.basename(path.root).lower() | 
					
						
							|  |  |  |         base_is_egg = base.endswith(".egg") | 
					
						
							|  |  |  |         self.infos = FreezableDefaultDict(list) | 
					
						
							|  |  |  |         self.eggs = FreezableDefaultDict(list) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for child in path.children(): | 
					
						
							|  |  |  |             low = child.lower() | 
					
						
							|  |  |  |             if low.endswith((".dist-info", ".egg-info")): | 
					
						
							|  |  |  |                 # rpartition is faster than splitext and suitable for this purpose. | 
					
						
							|  |  |  |                 name = low.rpartition(".")[0].partition("-")[0] | 
					
						
							|  |  |  |                 normalized = Prepared.normalize(name) | 
					
						
							|  |  |  |                 self.infos[normalized].append(path.joinpath(child)) | 
					
						
							|  |  |  |             elif base_is_egg and low == "egg-info": | 
					
						
							|  |  |  |                 name = base.rpartition(".")[0].partition("-")[0] | 
					
						
							|  |  |  |                 legacy_normalized = Prepared.legacy_normalize(name) | 
					
						
							|  |  |  |                 self.eggs[legacy_normalized].append(path.joinpath(child)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.infos.freeze() | 
					
						
							|  |  |  |         self.eggs.freeze() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def search(self, prepared): | 
					
						
							|  |  |  |         infos = ( | 
					
						
							|  |  |  |             self.infos[prepared.normalized] | 
					
						
							|  |  |  |             if prepared | 
					
						
							|  |  |  |             else itertools.chain.from_iterable(self.infos.values()) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         eggs = ( | 
					
						
							|  |  |  |             self.eggs[prepared.legacy_normalized] | 
					
						
							|  |  |  |             if prepared | 
					
						
							|  |  |  |             else itertools.chain.from_iterable(self.eggs.values()) | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         return itertools.chain(infos, eggs) | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Prepared: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     A prepared search for metadata on a possibly-named package. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     normalized = None | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |     legacy_normalized = None | 
					
						
							| 
									
										
										
										
											2020-01-11 10:37:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, name): | 
					
						
							|  |  |  |         self.name = name | 
					
						
							|  |  |  |         if name is None: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |         self.normalized = self.normalize(name) | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |         self.legacy_normalized = self.legacy_normalize(name) | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def normalize(name): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         PEP 503 normalization plus dashes as underscores. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def legacy_normalize(name): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Normalize the package name as found in the convention in | 
					
						
							|  |  |  |         older packaging tools versions and specs. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return name.lower().replace('-', '_') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |     def __bool__(self): | 
					
						
							|  |  |  |         return bool(self.name) | 
					
						
							| 
									
										
										
										
											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.""" | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |         prepared = Prepared(name) | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |         return itertools.chain.from_iterable( | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |             path.search(prepared) for path in map(FastPath, paths) | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 10:13:51 -04:00
										 |  |  |     def invalidate_caches(cls): | 
					
						
							|  |  |  |         FastPath.__new__.cache_clear() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | class PathDistribution(Distribution): | 
					
						
							| 
									
										
										
										
											2021-05-26 13:40:05 -04:00
										 |  |  |     def __init__(self, path: SimplePath): | 
					
						
							|  |  |  |         """Construct a distribution.
 | 
					
						
							| 
									
										
										
										
											2019-09-02 11:08:03 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-26 13:40:05 -04:00
										 |  |  |         :param path: SimplePath indicating the metadata directory. | 
					
						
							| 
									
										
										
										
											2019-09-02 11:08:03 -04:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |         self._path = path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read_text(self, filename): | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05: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') | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     read_text.__doc__ = Distribution.read_text.__doc__ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def locate_file(self, path): | 
					
						
							|  |  |  |         return self._path.parent / path | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-31 11:52:29 -04:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def _normalized_name(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Performance optimization: where possible, resolve the | 
					
						
							|  |  |  |         normalized name from the file system path. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         stem = os.path.basename(str(self._path)) | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |         return ( | 
					
						
							|  |  |  |             pass_none(Prepared.normalize)(self._name_from_stem(stem)) | 
					
						
							|  |  |  |             or super()._normalized_name | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2021-05-31 11:52:29 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |     @staticmethod | 
					
						
							|  |  |  |     def _name_from_stem(stem): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         >>> PathDistribution._name_from_stem('foo-3.0.egg-info') | 
					
						
							|  |  |  |         'foo' | 
					
						
							|  |  |  |         >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info') | 
					
						
							|  |  |  |         'CherryPy' | 
					
						
							|  |  |  |         >>> PathDistribution._name_from_stem('face.egg-info') | 
					
						
							|  |  |  |         'face' | 
					
						
							|  |  |  |         >>> PathDistribution._name_from_stem('foo.bar') | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         filename, ext = os.path.splitext(stem) | 
					
						
							| 
									
										
										
										
											2021-05-31 11:52:29 -04:00
										 |  |  |         if ext not in ('.dist-info', '.egg-info'): | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |         name, sep, rest = filename.partition('-') | 
					
						
							| 
									
										
										
										
											2021-05-31 11:52:29 -04:00
										 |  |  |         return name | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-02 17:03:40 -04:00
										 |  |  | def metadata(distribution_name) -> _meta.PackageMetadata: | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     """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. | 
					
						
							| 
									
										
										
										
											2020-12-31 12:56:43 -05:00
										 |  |  |     :return: A PackageMetadata containing the parsed metadata. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  | _unique = functools.partial( | 
					
						
							|  |  |  |     unique_everseen, | 
					
						
							|  |  |  |     key=operator.attrgetter('_normalized_name'), | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | Wrapper for ``distributions`` to return unique distributions by name. | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  | def entry_points(**params) -> EntryPoints: | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """Return EntryPoint objects for all installed packages.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |     Pass selection parameters (group or name) to filter the | 
					
						
							|  |  |  |     result to entry points matching those properties (see | 
					
						
							|  |  |  |     EntryPoints.select()). | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  |     :return: EntryPoints for all installed packages. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |     eps = itertools.chain.from_iterable( | 
					
						
							| 
									
										
										
										
											2022-06-25 21:04:28 -04:00
										 |  |  |         dist.entry_points for dist in _unique(distributions()) | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |     ) | 
					
						
							| 
									
										
										
										
											2022-10-06 15:25:24 -04:00
										 |  |  |     return EntryPoints(eps).select(**params) | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2021-05-26 13:40:05 -04:00
										 |  |  |         packaging.requirement.Requirement. | 
					
						
							| 
									
										
										
										
											2019-05-24 19:59:01 -04:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-09-12 10:29:11 +01:00
										 |  |  |     return distribution(distribution_name).requires | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def packages_distributions() -> Mapping[str, List[str]]: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Return a mapping of top-level packages to their | 
					
						
							|  |  |  |     distributions. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-14 22:20:49 -04:00
										 |  |  |     >>> import collections.abc | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |     >>> pkgs = packages_distributions() | 
					
						
							|  |  |  |     >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values()) | 
					
						
							|  |  |  |     True | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     pkg_to_dist = collections.defaultdict(list) | 
					
						
							|  |  |  |     for dist in distributions(): | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  |         for pkg in _top_level_declared(dist) or _top_level_inferred(dist): | 
					
						
							| 
									
										
										
										
											2021-03-13 11:31:45 -05:00
										 |  |  |             pkg_to_dist[pkg].append(dist.metadata['Name']) | 
					
						
							|  |  |  |     return dict(pkg_to_dist) | 
					
						
							| 
									
										
										
										
											2021-12-16 15:49:42 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _top_level_declared(dist): | 
					
						
							|  |  |  |     return (dist.read_text('top_level.txt') or '').split() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _top_level_inferred(dist): | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |         f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name | 
					
						
							|  |  |  |         for f in always_iterable(dist.files) | 
					
						
							|  |  |  |         if f.suffix == ".py" | 
					
						
							|  |  |  |     } |