From 29ec9c38dd5390f7d4326d2c792b4e49a9d0e334 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 13 Aug 2021 18:35:12 -0400 Subject: [PATCH 1/4] EntryPoint is no longer a namedtuple. --- importlib_metadata/__init__.py | 47 +++++++++++++++++++++------------- importlib_metadata/_compat.py | 23 +---------------- tests/test_main.py | 8 +++--- 3 files changed, 35 insertions(+), 43 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9f5bf347..d1517fc1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -18,7 +18,6 @@ from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, - PyPy_repr, install, pypy_partial, ) @@ -126,9 +125,7 @@ def valid(line): return line and not line.startswith('#') -class EntryPoint( - PyPy_repr, collections.namedtuple('EntryPointBase', 'name value group') -): +class EntryPoint: """An entry point as defined by Python packaging conventions. See `the packaging docs on entry points @@ -159,6 +156,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, @@ -185,7 +185,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): @@ -199,16 +199,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 tuple(getattr(self, key) for key in 'name value group'.split()) + + 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): """ @@ -356,15 +371,11 @@ def groups(self): 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 '') ) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 1947d449..765fdeac 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -2,7 +2,7 @@ import platform -__all__ = ['install', 'NullFinder', 'PyPy_repr', 'Protocol'] +__all__ = ['install', 'NullFinder', 'Protocol'] try: @@ -66,27 +66,6 @@ def find_spec(*args, **kwargs): find_module = find_spec -class PyPy_repr: - """ - Override repr for EntryPoint objects on PyPy to avoid __iter__ access. - Ref #97, #102. - """ - - affected = hasattr(sys, 'pypy_version_info') - - def __compat_repr__(self): # pragma: nocover - def make_param(name): - value = getattr(self, name) - return f'{name}={value!r}' - - params = ', '.join(map(make_param, self._fields)) - return f'EntryPoint({params})' - - if affected: # pragma: nocover - __repr__ = __compat_repr__ - del affected - - def pypy_partial(val): """ Adjust for variable stacklevel on partial under PyPy. diff --git a/tests/test_main.py b/tests/test_main.py index f7c9c518..10a6a09b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -223,7 +223,9 @@ def test_discovery(self): class TestEntryPoints(unittest.TestCase): def __init__(self, *args): super(TestEntryPoints, self).__init__(*args) - self.ep = importlib_metadata.EntryPoint('name', 'value', 'group') + self.ep = importlib_metadata.EntryPoint( + name='name', value='value', group='group' + ) def test_entry_point_pickleable(self): revived = pickle.loads(pickle.dumps(self.ep)) @@ -263,8 +265,8 @@ def test_sortable(self): """ sorted( [ - EntryPoint('b', 'val', 'group'), - EntryPoint('a', 'val', 'group'), + EntryPoint(name='b', value='val', group='group'), + EntryPoint(name='a', value='val', group='group'), ] ) From 08bd99f477dd0bef6e85e56eab5827e29c2cfbd5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 14 Aug 2021 18:08:33 -0400 Subject: [PATCH 2/4] Directly return attributes. Co-authored-by: Ronny Pfannschmidt --- importlib_metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d1517fc1..4c10da54 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -204,7 +204,7 @@ def matches(self, **params): return all(map(operator.eq, params.values(), attrs)) def _key(self): - return tuple(getattr(self, key) for key in 'name value group'.split()) + return self.name, self.value, self.group def __lt__(self, other): return self._key() < other._key() From bd25af7a0893ef25f1740bc1adc798a6161fc6f7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 28 Aug 2021 21:52:34 -0400 Subject: [PATCH 3/4] Restore support for EntryPoint construction with positional args. --- importlib_metadata/__init__.py | 2 +- tests/test_main.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6306a0ca..f79a437b 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -156,7 +156,7 @@ class EntryPoint: dist: Optional['Distribution'] = None - def __init__(self, *, name, value, group): + def __init__(self, name, value, group): vars(self).update(name=name, value=value, group=group) def load(self): diff --git a/tests/test_main.py b/tests/test_main.py index 14f7e467..1a64af56 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -232,6 +232,12 @@ def test_entry_point_pickleable(self): revived = pickle.loads(pickle.dumps(self.ep)) assert revived == self.ep + def test_positional_args(self): + """ + Capture legacy (namedtuple) construction, discouraged. + """ + EntryPoint('name', 'value', 'group') + def test_immutable(self): """EntryPoints should be immutable""" with self.assertRaises(AttributeError): From 6bdddca63c01a5cfd260b14cdcdf116f37aa8092 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 28 Aug 2021 22:02:07 -0400 Subject: [PATCH 4/4] Update changelog. --- CHANGES.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d5d1d016..d5398ccc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +v4.8.0 +====== + +* #337: Rewrote ``EntryPoint`` as a simple class, still + immutable and still with the attributes, but without any + expectation for ``namedtuple`` functionality such as + ``_asdict``. + v4.7.1 ======