diff --git a/CHANGES.rst b/CHANGES.rst index 952cc8c0..5fbf1259 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,22 @@ +v4.4.0 +======= + +* #300: Restore compatibility in the result from + ``Distribution.entry_points`` (``EntryPoints``) to honor + expectations in older implementations and issuing + deprecation warnings for these cases: + + - ``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()``). + + - ``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]``). + v4.3.1 ======= diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index acb45046..0ad0196e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -209,7 +209,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. """ @@ -220,6 +313,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: diff --git a/tests/test_api.py b/tests/test_api.py index b3c8c2f8..819d4841 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -133,6 +133,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