From 7e5bae4c7fbd30366e49249825171b193dff22d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 18:38:15 -0400 Subject: [PATCH 1/7] Remove SelectableGroups --- importlib_metadata/__init__.py | 123 ++------------------------------- tests/test_api.py | 39 ----------- 2 files changed, 4 insertions(+), 158 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 0ad0196e..6abb375c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -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__ = [ @@ -344,10 +344,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 set(ep.group for ep in self) @@ -367,109 +363,6 @@ def _parse_groups(text): ) -def flake8_bypass(func): - # defer inspect import as performance optimization. - import inspect - - is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5]) - return func if not is_flake8 else lambda: None - - -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=2, - ) - - def __getitem__(self, name): - self._warn() - return super().__getitem__(name) - - def get(self, name, default=None): - flake8_bypass(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""" @@ -964,29 +857,21 @@ def version(distribution_name): return distribution(distribution_name).version -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. """ 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()) ) - return SelectableGroups.load(eps).select(**params) + return EntryPoints(eps).select(**params) def files(distribution_name): diff --git a/tests/test_api.py b/tests/test_api.py index 819d4841..58bf2912 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -133,45 +133,6 @@ 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 - migrate. - """ - with warnings.catch_warnings(record=True): - 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 warnings.catch_warnings(record=True): - entry_points().get('missing', 'default') == 'default' - entry_points().get('entries', 'default') == entry_points()['entries'] - entry_points().get('missing', ()) == () - def test_metadata_for_this_package(self): md = metadata('egginfo-pkg') assert md['author'] == 'Steven Ma' From 47544ce7303da9b2147c6603a674a1f82225248f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 18:41:48 -0400 Subject: [PATCH 2/7] Remove DeprecatedList --- importlib_metadata/__init__.py | 95 +--------------------------------- 1 file changed, 1 insertion(+), 94 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6abb375c..9908ac72 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -209,100 +209,7 @@ def matches(self, **params): return all(map(operator.eq, params.values(), attrs)) -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): +class EntryPoints(tuple): """ An immutable collection of selectable EntryPoint objects. """ From 0c819641d314ac496eb32b55f2b15215fa6fa55f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 18:43:09 -0400 Subject: [PATCH 3/7] Remove compatibility for EntryPoints.__getitem__ by index. --- importlib_metadata/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9908ac72..c243ff61 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -220,14 +220,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: From dde2b9de2973ce1c6fa9ba21dfe81069b0baa77b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 19:04:42 -0400 Subject: [PATCH 4/7] Remove support for cast of iterable of entry points to dict. Ref #97. --- importlib_metadata/__init__.py | 12 ------------ tests/test_api.py | 18 ------------------ tests/test_main.py | 10 ---------- 3 files changed, 40 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c243ff61..86599a7a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -8,7 +8,6 @@ import pathlib import operator import textwrap -import warnings import functools import itertools import posixpath @@ -187,17 +186,6 @@ def _for(self, dist): self.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 __reduce__(self): return ( self.__class__, diff --git a/tests/test_api.py b/tests/test_api.py index 58bf2912..850e6ba6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,6 @@ import re import textwrap import unittest -import warnings import importlib from . import fixtures @@ -116,23 +115,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 warnings.catch_warnings(record=True) 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_metadata_for_this_package(self): md = metadata('egginfo-pkg') assert md['author'] == 'Steven Ma' diff --git a/tests/test_main.py b/tests/test_main.py index f7c9c518..d315b781 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,7 @@ import re -import json import pickle import textwrap import unittest -import warnings import importlib import importlib_metadata import pyfakefs.fake_filesystem_unittest as ffs @@ -243,14 +241,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' From 2135490a9d965339de71aa18dd2955844db966aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 19:34:16 -0400 Subject: [PATCH 5/7] Update changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f48b9d20..5aa5776f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.0.0 +====== + +* #97, #284, #300: Removed compatibility shims for deprecated entry + point interfaces. + v4.13.0 ======= From 117d1b470f8c03b4f4e332a95ccfcfebdadb8f52 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 19:48:36 -0400 Subject: [PATCH 6/7] Disable flake8 due to incompatibility. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 60de2424..27b0f18c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From ac9ff953a00d86c341f2a9a892c914d26075d891 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 20:01:33 -0400 Subject: [PATCH 7/7] Update documentation around removal of SelectableGroups. --- docs/using.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 40616d86..bbb3824f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -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 `_ +for compatibility options. .. _metadata: