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 ====== diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6c554558..f79a437b 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 self.name, self.value, self.group + + 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 e73af818..1a64af56 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -224,12 +224,20 @@ def test_discovery(self): class TestEntryPoints(unittest.TestCase): def __init__(self, *args): super().__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)) 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): @@ -264,8 +272,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'), ] )