summaryrefslogtreecommitdiffstats
path: root/Lib/importlib
diff options
context:
space:
mode:
authorJason R. Coombs <jaraco@jaraco.com>2021-12-16 20:49:42 (GMT)
committerGitHub <noreply@github.com>2021-12-16 20:49:42 (GMT)
commit04deaee4c8d313717f3ea8f6a4fd70286d510d6e (patch)
tree16af6d5242d248eb93107332099485783599fd4b /Lib/importlib
parent109d96602199a91e94eb14b8cb3720841f22ded7 (diff)
downloadcpython-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__.py163
-rw-r--r--Lib/importlib/metadata/_functools.py19
-rw-r--r--Lib/importlib/metadata/_itertools.py54
-rw-r--r--Lib/importlib/metadata/_meta.py2
-rw-r--r--Lib/importlib/metadata/_text.py4
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())