From 55c6070ad7f337a423962698d3e02c62a8e1b10e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:38:56 -0400 Subject: [PATCH 1/4] Refactored parsing and handling of EntryPoint.value. --- importlib_metadata/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 275c7106..849ce068 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,6 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from re import Match from typing import Any, cast from . import _meta @@ -135,6 +134,12 @@ def valid(line: str): return line and not line.startswith('#') +class _EntryPointMatch(types.SimpleNamespace): + module: str + attr: str + extras: str + + class EntryPoint: """An entry point as defined by Python packaging conventions. @@ -187,28 +192,27 @@ def load(self) -> Any: is indicated by the value, return that module. Otherwise, return the named object. """ - match = cast(Match, self.pattern.match(self.value)) - module = import_module(match.group('module')) - attrs = filter(None, (match.group('attr') or '').split('.')) + module = import_module(self.module) + attrs = filter(None, (self.attr or '').split('.')) return functools.reduce(getattr, attrs, module) @property def module(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('module') + return self._match.module @property def attr(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('attr') + return self._match.attr @property def extras(self) -> list[str]: + return re.findall(r'\w+', self._match.extras or '') + + @property + def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) assert match is not None - return re.findall(r'\w+', match.group('extras') or '') + return _EntryPointMatch(**match.groupdict()) def _for(self, dist): vars(self).update(dist=dist) From eae6a754d004e8ea72d5d07b7dc3733a6be71f1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:41:26 -0400 Subject: [PATCH 2/4] Raise a ValueError if no match. Closes #488 --- importlib_metadata/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 849ce068..d527e403 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -155,6 +155,22 @@ class EntryPoint: 'attr' >>> ep.extras ['extra1', 'extra2'] + + If the value package or module are not valid identifiers, a + ValueError is raised on access. + + >>> EntryPoint(name=None, group=None, value='invalid-name').module + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').attr + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').extras + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... """ pattern = re.compile( @@ -211,7 +227,13 @@ def extras(self) -> list[str]: @property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) - assert match is not None + if not match: + raise ValueError( + 'Invalid object reference. ' + 'See https://packaging.python.org' + '/en/latest/specifications/entry-points/#data-model', + self.value, + ) return _EntryPointMatch(**match.groupdict()) def _for(self, dist): From f179e28888b2c6caf12baaf5449ff1cd82513dfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:45:56 -0400 Subject: [PATCH 3/4] Also raise ValueError on construction if the value is invalid. --- importlib_metadata/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d527e403..ff3c2a44 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -171,6 +171,14 @@ class EntryPoint: Traceback (most recent call last): ... ValueError: ('Invalid object reference...invalid-name... + + The same thing happens on construction. + + >>> EntryPoint(name=None, group=None, value='invalid-name') + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + """ pattern = re.compile( @@ -202,6 +210,7 @@ class EntryPoint: def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) + self.module def load(self) -> Any: """Load the entry point from its definition. If only a module From 9f8af013635833cf3ac348413c9ac63b37caa3dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:50:19 -0400 Subject: [PATCH 4/4] Prefer a cached property, as the property is likely to be retrieved at least 3 times (on construction and for module:attr access). --- 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 ff3c2a44..157b2c6f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -233,7 +233,7 @@ def attr(self) -> str: def extras(self) -> list[str]: return re.findall(r'\w+', self._match.extras or '') - @property + @functools.cached_property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) if not match: