Skip to content
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

Remove compatibility shims for entry points. #405

Merged
merged 8 commits into from Oct 2, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
@@ -1,3 +1,9 @@
v5.0.0
======

* #97, #284, #300: Removed compatibility shims for deprecated entry
point interfaces.

v4.13.0
=======

Expand Down
9 changes: 4 additions & 5 deletions docs/using.rst
Expand Up @@ -164,11 +164,10 @@ for more information on entry points, their definition, and usage.
The "selectable" entry points were introduced in ``importlib_metadata``
3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted
no parameters and always returned a dictionary of entry points, keyed
by group. For compatibility, if no parameters are passed to entry_points,
a ``SelectableGroups`` object is returned, implementing that dict
interface. In the future, calling ``entry_points`` with no parameters
will return an ``EntryPoints`` object. Users should rely on the selection
interface to retrieve entry points by group.
by group. With ``importlib_metadata`` 5.0 and Python 3.12,
``entry_points`` always returns an ``EntryPoints`` object. See
`backports.entry_points_selectable <https://pypi.org/project/backports.entry_points_selectable>`_
for compatibility options.


.. _metadata:
Expand Down
206 changes: 5 additions & 201 deletions importlib_metadata/__init__.py
Expand Up @@ -29,7 +29,7 @@
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
from typing import List, Mapping, Optional, Union
from typing import List, Mapping, Optional


__all__ = [
Expand Down Expand Up @@ -227,17 +227,6 @@ def _for(self, dist):
vars(self).update(dist=dist)
return self

def __iter__(self):
"""
Supply iter so one may construct dicts of EntryPoints by name.
"""
msg = (
"Construction of dict of EntryPoints is deprecated in "
"favor of EntryPoints."
)
warnings.warn(msg, DeprecationWarning)
return iter((self.name, self))

def matches(self, **params):
"""
EntryPoint matches the given parameters.
Expand Down Expand Up @@ -283,77 +272,7 @@ def __hash__(self):
return hash(self._key())


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
"""

__slots__ = ()

_warn = functools.partial(
warnings.warn,
"EntryPoints list interface is deprecated. Cast to list if needed.",
DeprecationWarning,
stacklevel=pypy_partial(2),
)

def _wrap_deprecated_method(method_name: str): # type: ignore
def wrapped(self, *args, **kwargs):
self._warn()
return getattr(super(), method_name)(*args, **kwargs)

return method_name, wrapped

locals().update(
map(
_wrap_deprecated_method,
'__setitem__ __delitem__ append reverse extend pop remove '
'__iadd__ insert sort'.split(),
)
)

def __add__(self, other):
if not isinstance(other, tuple):
self._warn()
other = tuple(other)
return self.__class__(tuple(self) + other)

def __eq__(self, other):
if not isinstance(other, tuple):
self._warn()
other = tuple(other)

return tuple(self).__eq__(other)


class EntryPoints(DeprecatedList):
class EntryPoints(tuple):
"""
An immutable collection of selectable EntryPoint objects.
"""
Expand All @@ -364,14 +283,6 @@ 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 All @@ -396,10 +307,6 @@ def names(self):
def groups(self):
"""
Return the set of all groups of all entry points.

For coverage while SelectableGroups is present.
>>> EntryPoints().groups
set()
"""
return {ep.group for ep in self}

Expand All @@ -415,101 +322,6 @@ def _from_text(text):
)


class Deprecated:
"""
Compatibility add-in for mapping to indicate that
mapping behavior is deprecated.

>>> recwarn = getfixture('recwarn')
>>> class DeprecatedDict(Deprecated, dict): pass
>>> dd = DeprecatedDict(foo='bar')
>>> dd.get('baz', None)
>>> dd['foo']
'bar'
>>> list(dd)
['foo']
>>> list(dd.keys())
['foo']
>>> 'foo' in dd
True
>>> list(dd.values())
['bar']
>>> len(recwarn)
1
"""

_warn = functools.partial(
warnings.warn,
"SelectableGroups dict interface is deprecated. Use select.",
DeprecationWarning,
stacklevel=pypy_partial(2),
)

def __getitem__(self, name):
self._warn()
return super().__getitem__(name)

def get(self, name, default=None):
self._warn()
return super().get(name, default)

def __iter__(self):
self._warn()
return super().__iter__()

def __contains__(self, *args):
self._warn()
return super().__contains__(*args)

def keys(self):
self._warn()
return super().keys()

def values(self):
self._warn()
return super().values()


class SelectableGroups(Deprecated, dict):
"""
A backward- and forward-compatible result from
entry_points that fully implements the dict interface.
"""

@classmethod
def load(cls, eps):
by_group = operator.attrgetter('group')
ordered = sorted(eps, key=by_group)
grouped = itertools.groupby(ordered, by_group)
return cls((group, EntryPoints(eps)) for group, eps in grouped)

@property
def _all(self):
"""
Reconstruct a list of all entrypoints from the groups.
"""
groups = super(Deprecated, self).values()
return EntryPoints(itertools.chain.from_iterable(groups))

@property
def groups(self):
return self._all.groups

@property
def names(self):
"""
for coverage:
>>> SelectableGroups().names
set()
"""
return self._all.names

def select(self, **params):
if not params:
return self
return self._all.select(**params)


class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""

Expand Down Expand Up @@ -1029,27 +841,19 @@ def version(distribution_name):
"""


def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
def entry_points(**params) -> EntryPoints:
"""Return EntryPoint objects for all installed packages.

Pass selection parameters (group or name) to filter the
result to entry points matching those properties (see
EntryPoints.select()).

For compatibility, returns ``SelectableGroups`` object unless
selection parameters are supplied. In the future, this function
will return ``EntryPoints`` instead of ``SelectableGroups``
even when no selection parameters are supplied.

For maximum future compatibility, pass selection parameters
or invoke ``.select`` with parameters on the result.

:return: EntryPoints or SelectableGroups for all installed packages.
:return: EntryPoints for all installed packages.
"""
eps = itertools.chain.from_iterable(
dist.entry_points for dist in _unique(distributions())
)
return SelectableGroups.load(eps).select(**params)
return EntryPoints(eps).select(**params)


def files(distribution_name):
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Expand Up @@ -14,7 +14,8 @@ addopts = "--black"
addopts = "--mypy"

[tool.pytest-enabler.flake8]
addopts = "--flake8"
# disabled due to PyCQA/flake8#1438
# addopts = "--flake8"

[tool.pytest-enabler.cov]
addopts = "--cov"
56 changes: 0 additions & 56 deletions tests/test_api.py
Expand Up @@ -124,62 +124,6 @@ def test_entry_points_missing_name(self):
def test_entry_points_missing_group(self):
assert entry_points(group='missing') == ()

def test_entry_points_dict_construction(self):
"""
Prior versions of entry_points() returned simple lists and
allowed casting those lists into maps by name using ``dict()``.
Capture this now deprecated use-case.
"""
with suppress_known_deprecation() as caught:
eps = dict(entry_points(group='entries'))

assert 'main' in eps
assert eps['main'] == entry_points(group='entries')['main']

# check warning
expected = next(iter(caught))
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 suppress_known_deprecation() 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
migrate.
"""
with suppress_known_deprecation():
entry_points()['entries'] == entry_points(group='entries')

with self.assertRaises(KeyError):
entry_points()['missing']

def test_entry_points_groups_get(self):
"""
Prior versions of entry_points() returned a dict. Ensure
that callers using '.get()' are supported but warned to
migrate.
"""
with suppress_known_deprecation():
entry_points().get('missing', 'default') == 'default'
entry_points().get('entries', 'default') == entry_points()['entries']
entry_points().get('missing', ()) == ()

def test_entry_points_allows_no_attributes(self):
ep = entry_points().select(group='entries', name='main')
with self.assertRaises(AttributeError):
Expand Down
10 changes: 0 additions & 10 deletions tests/test_main.py
@@ -1,8 +1,6 @@
import re
import json
import pickle
import unittest
import warnings
import importlib
import importlib_metadata
import pyfakefs.fake_filesystem_unittest as ffs
Expand Down Expand Up @@ -259,14 +257,6 @@ def test_hashable(self):
"""EntryPoints should be hashable"""
hash(self.ep)

def test_json_dump(self):
"""
json should not expect to be able to dump an EntryPoint
"""
with self.assertRaises(Exception):
with warnings.catch_warnings(record=True):
json.dumps(self.ep)

def test_module(self):
assert self.ep.module == 'value'

Expand Down