Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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
======

Expand Down
47 changes: 29 additions & 18 deletions importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ._collections import FreezableDefaultDict, Pair
from ._compat import (
NullFinder,
PyPy_repr,
install,
pypy_partial,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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):
Expand All @@ -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):
Comment thread
jaraco marked this conversation as resolved.
return self._key() < other._key()

def __eq__(self, other):
return self._key() == other._key()

def __setattr__(self, name, value):
Comment thread
jaraco marked this conversation as resolved.
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):
"""
Expand Down Expand Up @@ -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 '')
)


Expand Down
23 changes: 1 addition & 22 deletions importlib_metadata/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import platform


__all__ = ['install', 'NullFinder', 'PyPy_repr', 'Protocol']
__all__ = ['install', 'NullFinder', 'Protocol']


try:
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 11 additions & 3 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'),
]
)

Expand Down