Skip to content

Commit 4d3fd0e

Browse files
akxpodgorniy94
andauthored
Allow use of importlib.metadata for finding entrypoints (#1102)
* Support importlib.metadata for finding entrypoints * Add a basic interoperability test for Jinja2 extraction * Only use importlib.metadata on Python 3.10+ Co-authored-by: podgorniy94 <[email protected]>
1 parent 42d793c commit 4d3fd0e

File tree

8 files changed

+82
-26
lines changed

8 files changed

+82
-26
lines changed

babel/messages/_compat.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import sys
2+
from functools import partial
3+
4+
5+
def find_entrypoints(group_name: str):
6+
"""
7+
Find entrypoints of a given group using either `importlib.metadata` or the
8+
older `pkg_resources` mechanism.
9+
10+
Yields tuples of the entrypoint name and a callable function that will
11+
load the actual entrypoint.
12+
"""
13+
if sys.version_info >= (3, 10):
14+
# "Changed in version 3.10: importlib.metadata is no longer provisional."
15+
try:
16+
from importlib.metadata import entry_points
17+
except ImportError:
18+
pass
19+
else:
20+
eps = entry_points(group=group_name)
21+
# Only do this if this implementation of `importlib.metadata` is
22+
# modern enough to not return a dict.
23+
if not isinstance(eps, dict):
24+
for entry_point in eps:
25+
yield (entry_point.name, entry_point.load)
26+
return
27+
28+
try:
29+
from pkg_resources import working_set
30+
except ImportError:
31+
pass
32+
else:
33+
for entry_point in working_set.iter_entry_points(group_name):
34+
yield (entry_point.name, partial(entry_point.load, require=True))

babel/messages/checkers.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,11 @@ def _check_positional(results: list[tuple[str, str]]) -> bool:
155155

156156

157157
def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]:
158+
from babel.messages._compat import find_entrypoints
158159
checkers: list[Callable[[Catalog | None, Message], object]] = []
159-
try:
160-
from pkg_resources import working_set
161-
except ImportError:
162-
pass
163-
else:
164-
for entry_point in working_set.iter_entry_points('babel.checkers'):
165-
checkers.append(entry_point.load())
160+
checkers.extend(load() for (name, load) in find_entrypoints('babel.checkers'))
166161
if len(checkers) == 0:
167-
# if pkg_resources is not available or no usable egg-info was found
162+
# if entrypoints are not available or no usable egg-info was found
168163
# (see #230), just resort to hard-coded checkers
169164
return [num_plurals, python_format]
170165
return checkers

babel/messages/extract.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
Mapping,
3131
MutableSequence,
3232
)
33+
from functools import lru_cache
3334
from os.path import relpath
3435
from textwrap import dedent
3536
from tokenize import COMMENT, NAME, OP, STRING, generate_tokens
3637
from typing import TYPE_CHECKING, Any
3738

39+
from babel.messages._compat import find_entrypoints
3840
from babel.util import parse_encoding, parse_future_flags, pathmatch
3941

4042
if TYPE_CHECKING:
@@ -363,6 +365,14 @@ def _match_messages_against_spec(lineno: int, messages: list[str|None], comments
363365
return lineno, translatable, comments, context
364366

365367

368+
@lru_cache(maxsize=None)
369+
def _find_extractor(name: str):
370+
for ep_name, load in find_entrypoints(GROUP_NAME):
371+
if ep_name == name:
372+
return load()
373+
return None
374+
375+
366376
def extract(
367377
method: _ExtractionMethod,
368378
fileobj: _FileObj,
@@ -421,25 +431,11 @@ def extract(
421431
module, attrname = method.split(':', 1)
422432
func = getattr(__import__(module, {}, {}, [attrname]), attrname)
423433
else:
424-
try:
425-
from pkg_resources import working_set
426-
except ImportError:
427-
pass
428-
else:
429-
for entry_point in working_set.iter_entry_points(GROUP_NAME,
430-
method):
431-
func = entry_point.load(require=True)
432-
break
434+
func = _find_extractor(method)
433435
if func is None:
434-
# if pkg_resources is not available or no usable egg-info was found
435-
# (see #230), we resort to looking up the builtin extractors
436-
# directly
437-
builtin = {
438-
'ignore': extract_nothing,
439-
'python': extract_python,
440-
'javascript': extract_javascript,
441-
}
442-
func = builtin.get(method)
436+
# if no named entry point was found,
437+
# we resort to looking up a builtin extractor
438+
func = _BUILTIN_EXTRACTORS.get(method)
443439

444440
if func is None:
445441
raise ValueError(f"Unknown extraction method {method!r}")
@@ -838,3 +834,10 @@ def parse_template_string(
838834
lineno += len(line_re.findall(expression_contents))
839835
expression_contents = ''
840836
prev_character = character
837+
838+
839+
_BUILTIN_EXTRACTORS = {
840+
'ignore': extract_nothing,
841+
'python': extract_python,
842+
'javascript': extract_javascript,
843+
}

tests/interop/__init__.py

Whitespace-only changes.

tests/interop/jinja2_data/hello.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{% trans %}Hello, {{ name }}!{% endtrans %}

tests/interop/jinja2_data/mapping.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[jinja2: *.html]

tests/interop/test_jinja2_interop.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import pathlib
2+
3+
import pytest
4+
5+
from babel.messages import frontend
6+
7+
jinja2 = pytest.importorskip("jinja2")
8+
9+
jinja2_data_path = pathlib.Path(__file__).parent / "jinja2_data"
10+
11+
12+
def test_jinja2_interop(monkeypatch, tmp_path):
13+
"""
14+
Test that babel can extract messages from Jinja2 templates.
15+
"""
16+
monkeypatch.chdir(jinja2_data_path)
17+
cli = frontend.CommandLineInterface()
18+
pot_file = tmp_path / "messages.pot"
19+
cli.run(['pybabel', 'extract', '--mapping', 'mapping.cfg', '-o', str(pot_file), '.'])
20+
assert '"Hello, %(name)s!"' in pot_file.read_text()

tox.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ envlist =
55
pypy3
66
py{38}-pytz
77
py{311,312}-setuptools
8+
py312-jinja
89

910
[testenv]
1011
extras =
@@ -15,6 +16,7 @@ deps =
1516
tzdata;sys_platform == 'win32'
1617
pytz: pytz
1718
setuptools: setuptools
19+
jinja: jinja2>=3.0
1820
allowlist_externals = make
1921
commands = make clean-cldr test
2022
setenv =

0 commit comments

Comments
 (0)