Skip to content

bpo-44246: Restore compatibility in entry_points #26468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 31, 2021
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
127 changes: 125 additions & 2 deletions Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,100 @@ def matches(self, **params):
return all(map(operator.eq, params.values(), attrs))


class EntryPoints(tuple):
class DeprecatedList(list):
"""
Allow an otherwise immutable object to implement mutability
for compatibility.

>>> recwarn = getfixture('recwarn')
>>> dl = DeprecatedList(range(3))
>>> dl[0] = 1
>>> dl.append(3)
>>> del dl[3]
>>> dl.reverse()
>>> dl.sort()
>>> dl.extend([4])
>>> dl.pop(-1)
4
>>> dl.remove(1)
>>> dl += [5]
>>> dl + [6]
[1, 2, 5, 6]
>>> dl + (6,)
[1, 2, 5, 6]
>>> dl.insert(0, 0)
>>> dl
[0, 1, 2, 5]
>>> dl == [0, 1, 2, 5]
True
>>> dl == (0, 1, 2, 5)
True
>>> len(recwarn)
1
"""

_warn = functools.partial(
warnings.warn,
"EntryPoints list interface is deprecated. Cast to list if needed.",
DeprecationWarning,
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 __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()
other = tuple(other)

return tuple(self).__eq__(other)


class EntryPoints(DeprecatedList):
"""
An immutable collection of selectable EntryPoint objects.
"""
Expand All @@ -215,6 +308,14 @@ def __getitem__(self, name): # -> EntryPoint:
"""
Get the EntryPoint in self matching name.
"""
if isinstance(name, int):
warnings.warn(
"Accessing entry points by index is deprecated. "
"Cast to tuple if needed.",
DeprecationWarning,
stacklevel=2,
)
return super().__getitem__(name)
try:
return next(iter(self.select(name=name)))
except StopIteration:
Expand Down Expand Up @@ -493,6 +594,11 @@ def name(self):
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']

@property
def _normalized_name(self):
"""Return a normalized version of the name."""
return Prepared.normalize(self.name)

@property
def version(self):
"""Return the 'Version' metadata for the distribution package."""
Expand Down Expand Up @@ -795,6 +901,22 @@ def read_text(self, filename):
def locate_file(self, path):
return self._path.parent / path

@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))
return self._name_from_stem(stem) or super()._normalized_name

def _name_from_stem(self, stem):
name, ext = os.path.splitext(stem)
if ext not in ('.dist-info', '.egg-info'):
return
name, sep, rest = stem.partition('-')
return name


def distribution(distribution_name):
"""Get the ``Distribution`` instance for the named package.
Expand Down Expand Up @@ -849,7 +971,8 @@ def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:

:return: EntryPoints or SelectableGroups for all installed packages.
"""
unique = functools.partial(unique_everseen, key=operator.attrgetter('name'))
norm_name = operator.attrgetter('_normalized_name')
unique = functools.partial(unique_everseen, key=norm_name)
eps = itertools.chain.from_iterable(
dist.entry_points for dist in unique(distributions())
)
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_importlib/test_metadata_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ def test_entry_points_dict_construction(self):
assert expected.category is DeprecationWarning
assert "Construction of dict of EntryPoints is deprecated" in str(expected)

def test_entry_points_by_index(self):
"""
Prior versions of Distribution.entry_points would return a
tuple that allowed access by index.
Capture this now deprecated use-case
See python/importlib_metadata#300 and bpo-44246.
"""
eps = distribution('distinfo-pkg').entry_points
with warnings.catch_warnings(record=True) as caught:
eps[0]

# check warning
expected = next(iter(caught))
assert expected.category is DeprecationWarning
assert "Accessing entry points by index is deprecated" in str(expected)

def test_entry_points_groups_getitem(self):
# Prior versions of entry_points() returned a dict. Ensure
# that callers using '.__getitem__()' are supported but warned to
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_importlib/test_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@ def test_files(self):
for file in files('example'):
path = str(file.dist.locate_file(file))
assert '.egg/' in path, path

def test_normalized_name(self):
dist = distribution('example')
assert dist._normalized_name == 'example'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
In importlib.metadata.entry_points, de-duplication of distributions no
longer requires loading the full metadata for PathDistribution objects,
improving entry point loading performance by ~10x.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
In ``importlib.metadata``, restore compatibility in the result from
``Distribution.entry_points`` (``EntryPoints``) to honor expectations in
older implementations and issuing deprecation warnings for these cases: A. ``EntryPoints`` objects are once again mutable, allowing for ``sort()``
and other list-based mutation operations. Avoid deprecation warnings by
casting to a mutable sequence (e.g. ``list(dist.entry_points).sort()``). B. ``EntryPoints`` results once again allow for access by index. To avoid
deprecation warnings, cast the result to a Sequence first (e.g.
``tuple(dist.entry_points)[0]``).