From 20f95880bda19824bcb37fb122d684b27a534fe9 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 18 Jul 2024 10:41:03 +0300 Subject: [PATCH] Support importlib.metadata for finding entrypoints For Python 3.12 compatibility! Co-authored-by: podgorniy94 --- babel/messages/_compat.py | 32 +++++++++++++++++++++++++++++++ babel/messages/checkers.py | 11 +++-------- babel/messages/extract.py | 39 ++++++++++++++++++++------------------ 3 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 babel/messages/_compat.py diff --git a/babel/messages/_compat.py b/babel/messages/_compat.py new file mode 100644 index 00000000..cf82245e --- /dev/null +++ b/babel/messages/_compat.py @@ -0,0 +1,32 @@ +from functools import partial + + +def find_entrypoints(group_name: str): + """ + Find entrypoints of a given group using either `importlib.metadata` or the + older `pkg_resources` mechanism. + + Yields tuples of the entrypoint name and a callable function that will + load the actual entrypoint. + """ + try: + from importlib.metadata import entry_points + except ImportError: + pass + else: + eps = entry_points() + if isinstance(eps, dict): # Old structure before Python 3.10 + group_eps = eps.get(group_name, []) + else: # New structure in Python 3.10+ + group_eps = (ep for ep in eps if ep.group == group_name) + for entry_point in group_eps: + yield (entry_point.name, entry_point.load) + return + + try: + from pkg_resources import working_set + except ImportError: + pass + else: + for entry_point in working_set.iter_entry_points(group_name): + yield (entry_point.name, partial(entry_point.load, require=True)) diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index df1159de..2889b4e6 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -155,16 +155,11 @@ def _validate_format(format: str, alternative: str) -> None: def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]: + from babel.messages._compat import find_entrypoints checkers: list[Callable[[Catalog | None, Message], object]] = [] - try: - from pkg_resources import working_set - except ImportError: - pass - else: - for entry_point in working_set.iter_entry_points('babel.checkers'): - checkers.append(entry_point.load()) + checkers.extend(load() for (name, load) in find_entrypoints('babel.checkers')) if len(checkers) == 0: - # if pkg_resources is not available or no usable egg-info was found + # if entrypoints are not available or no usable egg-info was found # (see #230), just resort to hard-coded checkers return [num_plurals, python_format] return checkers diff --git a/babel/messages/extract.py b/babel/messages/extract.py index 26d736e7..8d4bbeaf 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -30,11 +30,13 @@ from collections.abc import ( Mapping, MutableSequence, ) +from functools import lru_cache from os.path import relpath from textwrap import dedent from tokenize import COMMENT, NAME, OP, STRING, generate_tokens from typing import TYPE_CHECKING, Any +from babel.messages._compat import find_entrypoints from babel.util import parse_encoding, parse_future_flags, pathmatch if TYPE_CHECKING: @@ -363,6 +365,14 @@ def _match_messages_against_spec(lineno: int, messages: list[str|None], comments return lineno, translatable, comments, context +@lru_cache(maxsize=None) +def _find_extractor(name: str): + for ep_name, load in find_entrypoints(GROUP_NAME): + if ep_name == name: + return load() + return None + + def extract( method: _ExtractionMethod, fileobj: _FileObj, @@ -421,25 +431,11 @@ def extract( module, attrname = method.split(':', 1) func = getattr(__import__(module, {}, {}, [attrname]), attrname) else: - try: - from pkg_resources import working_set - except ImportError: - pass - else: - for entry_point in working_set.iter_entry_points(GROUP_NAME, - method): - func = entry_point.load(require=True) - break + func = _find_extractor(method) if func is None: - # if pkg_resources is not available or no usable egg-info was found - # (see #230), we resort to looking up the builtin extractors - # directly - builtin = { - 'ignore': extract_nothing, - 'python': extract_python, - 'javascript': extract_javascript, - } - func = builtin.get(method) + # if no named entry point was found, + # we resort to looking up a builtin extractor + func = _BUILTIN_EXTRACTORS.get(method) if func is None: raise ValueError(f"Unknown extraction method {method!r}") @@ -838,3 +834,10 @@ def parse_template_string( lineno += len(line_re.findall(expression_contents)) expression_contents = '' prev_character = character + + +_BUILTIN_EXTRACTORS = { + 'ignore': extract_nothing, + 'python': extract_python, + 'javascript': extract_javascript, +} -- 2.47.2