diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2021-12-16 20:49:42 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-16 20:49:42 (GMT) |
commit | 04deaee4c8d313717f3ea8f6a4fd70286d510d6e (patch) | |
tree | 16af6d5242d248eb93107332099485783599fd4b /Lib/importlib | |
parent | 109d96602199a91e94eb14b8cb3720841f22ded7 (diff) | |
download | cpython-04deaee4c8d313717f3ea8f6a4fd70286d510d6e.zip cpython-04deaee4c8d313717f3ea8f6a4fd70286d510d6e.tar.gz cpython-04deaee4c8d313717f3ea8f6a4fd70286d510d6e.tar.bz2 |
bpo-44893: Implement EntryPoint as simple class with attributes. (GH-30150)
* bpo-44893: Implement EntryPoint as simple class and deprecate tuple access in favor of attribute access. Syncs with importlib_metadata 4.8.1.
* Apply refactorings found in importlib_metadata 4.8.2.
Diffstat (limited to 'Lib/importlib')
-rw-r--r-- | Lib/importlib/metadata/__init__.py | 163 | ||||
-rw-r--r-- | Lib/importlib/metadata/_functools.py | 19 | ||||
-rw-r--r-- | Lib/importlib/metadata/_itertools.py | 54 | ||||
-rw-r--r-- | Lib/importlib/metadata/_meta.py | 2 | ||||
-rw-r--r-- | Lib/importlib/metadata/_text.py | 4 |
5 files changed, 176 insertions, 66 deletions
diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index ec41ed3..d44541f 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -15,10 +15,9 @@ import posixpath import collections from . import _adapters, _meta -from ._meta import PackageMetadata from ._collections import FreezableDefaultDict, Pair -from ._functools import method_cache -from ._itertools import unique_everseen +from ._functools import method_cache, pass_none +from ._itertools import always_iterable, unique_everseen from ._meta import PackageMetadata, SimplePath from contextlib import suppress @@ -121,8 +120,33 @@ class Sectioned: return line and not line.startswith('#') -class EntryPoint( - collections.namedtuple('EntryPointBase', 'name value group')): +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 + """ + + _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): """An entry point as defined by Python packaging conventions. See `the packaging docs on entry points @@ -153,6 +177,9 @@ class EntryPoint( dist: Optional['Distribution'] = None + def __init__(self, name, value, group): + vars(self).update(name=name, value=value, group=group) + def load(self): """Load the entry point from its definition. If only a module is indicated by the value, return that module. Otherwise, @@ -179,7 +206,7 @@ class EntryPoint( return list(re.finditer(r'\w+', match.group('extras') or '')) def _for(self, dist): - self.dist = dist + vars(self).update(dist=dist) return self def __iter__(self): @@ -193,16 +220,31 @@ class EntryPoint( warnings.warn(msg, DeprecationWarning) return iter((self.name, self)) - def __reduce__(self): - return ( - self.__class__, - (self.name, self.value, self.group), - ) - def matches(self, **params): attrs = (getattr(self, param) for param in params) return all(map(operator.eq, params.values(), attrs)) + 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()) + class DeprecatedList(list): """ @@ -243,37 +285,26 @@ class DeprecatedList(list): stacklevel=2, ) - def __setitem__(self, *args, **kwargs): - self._warn() - return super().__setitem__(*args, **kwargs) - - def __delitem__(self, *args, **kwargs): - self._warn() - return super().__delitem__(*args, **kwargs) - - def append(self, *args, **kwargs): - self._warn() - return super().append(*args, **kwargs) - - def reverse(self, *args, **kwargs): - self._warn() - return super().reverse(*args, **kwargs) - - def extend(self, *args, **kwargs): - self._warn() - return super().extend(*args, **kwargs) - - def pop(self, *args, **kwargs): - self._warn() - return super().pop(*args, **kwargs) - - def remove(self, *args, **kwargs): - self._warn() - return super().remove(*args, **kwargs) - - def __iadd__(self, *args, **kwargs): - self._warn() - return super().__iadd__(*args, **kwargs) + def _wrap_deprecated_method(method_name: str): # type: ignore + def wrapped(self, *args, **kwargs): + self._warn() + return getattr(super(), method_name)(*args, **kwargs) + + return wrapped + + for method_name in [ + '__setitem__', + '__delitem__', + 'append', + 'reverse', + 'extend', + 'pop', + 'remove', + '__iadd__', + 'insert', + 'sort', + ]: + locals()[method_name] = _wrap_deprecated_method(method_name) def __add__(self, other): if not isinstance(other, tuple): @@ -281,14 +312,6 @@ class DeprecatedList(list): other = tuple(other) return self.__class__(tuple(self) + other) - def insert(self, *args, **kwargs): - self._warn() - return super().insert(*args, **kwargs) - - def sort(self, *args, **kwargs): - self._warn() - return super().sort(*args, **kwargs) - def __eq__(self, other): if not isinstance(other, tuple): self._warn() @@ -333,7 +356,7 @@ class EntryPoints(DeprecatedList): """ Return the set of all names of all entry points. """ - return set(ep.name for ep in self) + return {ep.name for ep in self} @property def groups(self): @@ -344,21 +367,17 @@ class EntryPoints(DeprecatedList): >>> EntryPoints().groups set() """ - return set(ep.group for ep in self) + return {ep.group for ep in self} @classmethod def _from_text_for(cls, text, dist): return cls(ep._for(dist) for ep in cls._from_text(text)) - @classmethod - def _from_text(cls, text): - return itertools.starmap(EntryPoint, cls._parse_groups(text or '')) - @staticmethod - def _parse_groups(text): + def _from_text(text): return ( - (item.value.name, item.value.value, item.name) - for item in Sectioned.section_pairs(text) + EntryPoint(name=item.value.name, value=item.value.value, group=item.name) + for item in Sectioned.section_pairs(text or '') ) @@ -611,7 +630,6 @@ class Distribution: missing. Result may be empty if the metadata exists but is empty. """ - file_lines = self._read_files_distinfo() or self._read_files_egginfo() def make_file(name, hash=None, size_str=None): result = PackagePath(name) @@ -620,7 +638,11 @@ class Distribution: result.dist = self return result - return file_lines and list(starmap(make_file, csv.reader(file_lines))) + @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()) def _read_files_distinfo(self): """ @@ -742,6 +764,9 @@ class FastPath: """ Micro-optimized class for searching a path for children. + + >>> FastPath('').children() + ['...'] """ @functools.lru_cache() # type: ignore @@ -1011,6 +1036,18 @@ def packages_distributions() -> Mapping[str, List[str]]: """ pkg_to_dist = collections.defaultdict(list) for dist in distributions(): - for pkg in (dist.read_text('top_level.txt') or '').split(): + for pkg in _top_level_declared(dist) or _top_level_inferred(dist): pkg_to_dist[pkg].append(dist.metadata['Name']) return dict(pkg_to_dist) + + +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" + } diff --git a/Lib/importlib/metadata/_functools.py b/Lib/importlib/metadata/_functools.py index 73f50d0..71f66bd 100644 --- a/Lib/importlib/metadata/_functools.py +++ b/Lib/importlib/metadata/_functools.py @@ -83,3 +83,22 @@ def method_cache(method, cache_wrapper=None): wrapper.cache_clear = lambda: None return wrapper + + +# From jaraco.functools 3.3 +def pass_none(func): + """ + Wrap func so it's not called if its first param is None + + >>> print_text = pass_none(print) + >>> print_text('text') + text + >>> print_text(None) + """ + + @functools.wraps(func) + def wrapper(param, *args, **kwargs): + if param is not None: + return func(param, *args, **kwargs) + + return wrapper diff --git a/Lib/importlib/metadata/_itertools.py b/Lib/importlib/metadata/_itertools.py index dd45f2f..d4ca9b9 100644 --- a/Lib/importlib/metadata/_itertools.py +++ b/Lib/importlib/metadata/_itertools.py @@ -17,3 +17,57 @@ def unique_everseen(iterable, key=None): if k not in seen: seen_add(k) yield element + + +# copied from more_itertools 8.8 +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py index 1a6edbf..d5c0576 100644 --- a/Lib/importlib/metadata/_meta.py +++ b/Lib/importlib/metadata/_meta.py @@ -37,7 +37,7 @@ class SimplePath(Protocol): def joinpath(self) -> 'SimplePath': ... # pragma: no cover - def __div__(self) -> 'SimplePath': + def __truediv__(self) -> 'SimplePath': ... # pragma: no cover def parent(self) -> 'SimplePath': diff --git a/Lib/importlib/metadata/_text.py b/Lib/importlib/metadata/_text.py index 766979d..c88cfbb 100644 --- a/Lib/importlib/metadata/_text.py +++ b/Lib/importlib/metadata/_text.py @@ -80,7 +80,7 @@ class FoldedCase(str): return hash(self.lower()) def __contains__(self, other): - return super(FoldedCase, self).lower().__contains__(other.lower()) + return super().lower().__contains__(other.lower()) def in_(self, other): "Does self appear in other?" @@ -89,7 +89,7 @@ class FoldedCase(str): # cache lower since it's likely to be called frequently. @method_cache def lower(self): - return super(FoldedCase, self).lower() + return super().lower() def index(self, sub): return self.lower().index(sub.lower()) |