Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 100 additions & 63 deletions Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
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
Expand Down Expand Up @@ -121,8 +120,33 @@ def valid(line):
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
Expand Down Expand Up @@ -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,
Expand All @@ -179,7 +206,7 @@ def extras(self):
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):
Expand All @@ -193,16 +220,31 @@ def __iter__(self):
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):
"""
Expand Down Expand Up @@ -243,52 +285,33 @@ 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):
self._warn()
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()
Expand Down Expand Up @@ -333,7 +356,7 @@ def names(self):
"""
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):
Expand All @@ -344,21 +367,17 @@ def groups(self):
>>> 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 '')
)


Expand Down Expand Up @@ -611,7 +630,6 @@ def files(self):
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)
Expand All @@ -620,7 +638,11 @@ def make_file(name, hash=None, size_str=None):
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):
"""
Expand Down Expand Up @@ -733,6 +755,9 @@ class FastPath:
"""
Micro-optimized class for searching a path for
children.

>>> FastPath('').children()
['...']
"""

@functools.lru_cache() # type: ignore
Expand Down Expand Up @@ -1002,6 +1027,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"
}
19 changes: 19 additions & 0 deletions Lib/importlib/metadata/_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,22 @@ def wrapper(self, *args, **kwargs):
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
54 changes: 54 additions & 0 deletions Lib/importlib/metadata/_itertools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,))
2 changes: 1 addition & 1 deletion Lib/importlib/metadata/_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
4 changes: 2 additions & 2 deletions Lib/importlib/metadata/_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __hash__(self):
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?"
Expand All @@ -89,7 +89,7 @@ def in_(self, other):
# 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())
Expand Down
Binary file not shown.
Loading