Skip to content

Commit 4e96885

Browse files
committed
Share PO message IDs across catalogs on conventional CPython
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
1 parent e2a1791 commit 4e96885

2 files changed

Lines changed: 316 additions & 0 deletions

File tree

‎babel/messages/pofile.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
import os
1515
import re
16+
import sys
17+
import sysconfig
1618
from collections.abc import Iterable
1719
from typing import TYPE_CHECKING, Literal
1820

@@ -28,6 +30,12 @@
2830

2931
_unescape_re = re.compile(r'\\([\\trn"])')
3032

33+
# Free-threaded CPython keeps interned strings alive until shutdown.
34+
# Limit sharing to conventional CPython, even when a free-threaded GIL is enabled.
35+
_SHARE_MESSAGE_IDS = sys.implementation.name == 'cpython' and not sysconfig.get_config_var(
36+
'Py_GIL_DISABLED',
37+
)
38+
3139

3240
def unescape(string: str) -> str:
3341
r"""Reverse `escape` the given string.
@@ -211,6 +219,11 @@ def _add_message(self) -> None:
211219
msgid = self.messages[0].denormalize()
212220
string = self.translations[0][1].denormalize()
213221
msgctxt = self.context.denormalize() if self.context else None
222+
if _SHARE_MESSAGE_IDS and not (self.obsolete and self.ignore_obsolete):
223+
if isinstance(msgid, tuple):
224+
msgid = tuple(sys.intern(part) for part in msgid)
225+
else:
226+
msgid = sys.intern(msgid)
214227
message = Message(
215228
msgid,
216229
string,
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import importlib.util
2+
import sys
3+
import sysconfig
4+
from io import BytesIO, StringIO
5+
from types import ModuleType, SimpleNamespace
6+
from unittest.mock import patch
7+
8+
import pytest
9+
10+
from babel.messages import pofile
11+
12+
13+
@pytest.fixture
14+
def pofile_for_runtime():
15+
def load(implementation, gil_disabled, gil_enabled):
16+
runtime_sys = ModuleType('sys')
17+
runtime_sys.__dict__.update(vars(sys))
18+
runtime_sys.implementation = SimpleNamespace(**vars(sys.implementation))
19+
runtime_sys.implementation.name = implementation
20+
runtime_sys._is_gil_enabled = lambda: gil_enabled
21+
runtime_config = ModuleType('sysconfig')
22+
runtime_config.__dict__.update(vars(sysconfig))
23+
24+
def get_config_var(name):
25+
if name == 'Py_GIL_DISABLED':
26+
return gil_disabled
27+
return sysconfig.get_config_var(name)
28+
29+
runtime_config.get_config_var = get_config_var
30+
interned = []
31+
32+
def intern(value):
33+
interned.append(value)
34+
return sys.intern(value)
35+
36+
runtime_sys.intern = intern
37+
spec = importlib.util.spec_from_file_location(
38+
'babel.messages._pofile_runtime_test',
39+
pofile.__file__,
40+
)
41+
module = importlib.util.module_from_spec(spec)
42+
with patch.dict(sys.modules, {'sys': runtime_sys, 'sysconfig': runtime_config}):
43+
spec.loader.exec_module(module)
44+
interned.clear()
45+
return module, interned
46+
47+
return load
48+
49+
50+
@pytest.fixture
51+
def conventional_pofile(pofile_for_runtime):
52+
module, _ = pofile_for_runtime('cpython', 0, True)
53+
return module
54+
55+
56+
@pytest.mark.parametrize(
57+
('implementation', 'gil_disabled', 'gil_enabled'),
58+
[
59+
('cpython', 1, True),
60+
('cpython', 1, False),
61+
('pypy', None, True),
62+
('pypy', 0, True),
63+
('graalpy', None, True),
64+
],
65+
)
66+
@pytest.mark.parametrize('plural', [False, True])
67+
def test_read_po_avoids_interning_on_other_builds(
68+
pofile_for_runtime,
69+
implementation,
70+
gil_disabled,
71+
gil_enabled,
72+
plural,
73+
):
74+
# A free-threaded build can enable its GIL without changing string lifetime.
75+
module, interned = pofile_for_runtime(implementation, gil_disabled, gil_enabled)
76+
po = '''\
77+
# Translator comment
78+
#. Extracted comment
79+
#: source.py:3
80+
#, fuzzy
81+
msgctxt "menu"
82+
msgid "A Unicode source ID: café, Ω, 日本語."
83+
'''
84+
if plural:
85+
po += '''\
86+
msgid_plural "Several Unicode source IDs: café, Ω, 日本語."
87+
msgstr[0] "Une traduction"
88+
msgstr[1] "Des traductions"
89+
'''
90+
else:
91+
po += 'msgstr "Une traduction"\n'
92+
catalog = module.read_po(BytesIO(po.encode('utf-8')), locale='fr')
93+
message = catalog.get('A Unicode source ID: café, Ω, 日本語.', context='menu')
94+
if plural:
95+
assert message.id == (
96+
'A Unicode source ID: café, Ω, 日本語.',
97+
'Several Unicode source IDs: café, Ω, 日本語.',
98+
)
99+
# French has three plural forms; the omitted translation stays empty.
100+
assert message.string == ('Une traduction', 'Des traductions', '')
101+
else:
102+
assert message.id == 'A Unicode source ID: café, Ω, 日本語.'
103+
assert message.string == 'Une traduction'
104+
assert message.context == 'menu'
105+
assert message.flags == {'fuzzy'}
106+
assert message.locations == [('source.py', 3)]
107+
assert message.user_comments == ['Translator comment']
108+
assert message.auto_comments == ['Extracted comment']
109+
assert interned == []
110+
111+
112+
@pytest.mark.parametrize('gil_disabled', [None, 0])
113+
@pytest.mark.parametrize('plural', [False, True])
114+
def test_read_po_interns_ids_on_conventional_cpython(
115+
pofile_for_runtime,
116+
gil_disabled,
117+
plural,
118+
):
119+
module, interned = pofile_for_runtime('cpython', gil_disabled, True)
120+
po = 'msgid "A shared Unicode source ID: café, Ω, 日本語."\n'
121+
expected = ['A shared Unicode source ID: café, Ω, 日本語.']
122+
if plural:
123+
po += '''\
124+
msgid_plural "Several shared Unicode source IDs: café, Ω, 日本語."
125+
msgstr[0] "Une traduction"
126+
msgstr[1] "Des traductions"
127+
'''
128+
expected.append('Several shared Unicode source IDs: café, Ω, 日本語.')
129+
else:
130+
po += 'msgstr "Une traduction"\n'
131+
first = module.read_po(StringIO(po), locale='fr')
132+
second = module.read_po(BytesIO(po.encode('utf-8')), locale='fr')
133+
assert interned == expected * 2
134+
first_id = first['A shared Unicode source ID: café, Ω, 日本語.'].id
135+
second_id = second['A shared Unicode source ID: café, Ω, 日本語.'].id
136+
if plural:
137+
assert first_id == second_id == tuple(expected)
138+
assert first_id[0] is second_id[0]
139+
assert first_id[1] is second_id[1]
140+
else:
141+
assert first_id == second_id == expected[0]
142+
assert first_id is second_id
143+
144+
145+
def test_read_po_does_not_intern_discarded_obsolete_ids(pofile_for_runtime):
146+
module, interned = pofile_for_runtime('cpython', 0, True)
147+
catalog = module.read_po(
148+
StringIO('''\
149+
#~ msgctxt "menu"
150+
#~ msgid "An obsolete Unicode source ID: café, Ω, 日本語."
151+
#~ msgid_plural "Several obsolete Unicode source IDs: café, Ω, 日本語."
152+
#~ msgstr[0] "Une traduction"
153+
#~ msgstr[1] "Des traductions"
154+
'''),
155+
locale='fr',
156+
ignore_obsolete=True,
157+
)
158+
assert not catalog
159+
assert not catalog.obsolete
160+
assert interned == []
161+
162+
163+
def test_read_po_shares_source_ids_across_catalogs(conventional_pofile):
164+
first = conventional_pofile.read_po(
165+
StringIO('''\
166+
msgid "A source message shared by several independently loaded translation catalogs."
167+
msgstr "First translation"
168+
'''),
169+
)
170+
second = conventional_pofile.read_po(
171+
StringIO('''\
172+
msgid "A source message shared by several independently loaded translation catalogs."
173+
msgstr "Second translation"
174+
'''),
175+
)
176+
identifier = (
177+
"A source message shared by several independently loaded translation catalogs."
178+
)
179+
assert first[identifier].id == second[identifier].id == identifier
180+
assert first[identifier].id is second[identifier].id
181+
assert first[identifier].string == "First translation"
182+
assert second[identifier].string == "Second translation"
183+
184+
185+
def test_read_po_shares_each_plural_source_id(conventional_pofile):
186+
po = '''\
187+
msgid "One source message shared by independently loaded translation catalogs."
188+
msgid_plural "Several source messages shared by independently loaded translation catalogs."
189+
msgstr[0] "One translated message"
190+
msgstr[1] "Several translated messages"
191+
'''
192+
first = conventional_pofile.read_po(StringIO(po))
193+
second = conventional_pofile.read_po(StringIO(po))
194+
identifier = "One source message shared by independently loaded translation catalogs."
195+
expected = (
196+
identifier,
197+
"Several source messages shared by independently loaded translation catalogs.",
198+
)
199+
assert first[identifier].id == second[identifier].id == expected
200+
assert first[identifier].id[0] is second[identifier].id[0]
201+
assert first[identifier].id[1] is second[identifier].id[1]
202+
assert first[identifier].string == (
203+
"One translated message",
204+
"Several translated messages",
205+
)
206+
207+
208+
def test_read_po_shares_denormalized_unicode_ids(conventional_pofile):
209+
first = conventional_pofile.read_po(
210+
StringIO('''\
211+
msgid "Unicode source: 日本語, Ω, café and an escaped \\"quote\\"."
212+
msgstr "first"
213+
'''),
214+
)
215+
second = conventional_pofile.read_po(
216+
StringIO('''\
217+
msgid ""
218+
"Unicode source: 日本語, Ω, "
219+
"café and an escaped \\"quote\\"."
220+
msgstr "second"
221+
'''),
222+
)
223+
identifier = 'Unicode source: 日本語, Ω, café and an escaped "quote".'
224+
assert first[identifier].id == second[identifier].id == identifier
225+
assert first[identifier].id is second[identifier].id
226+
227+
228+
def test_read_po_keeps_messages_and_metadata_independent():
229+
po = '''\
230+
# Translator comment
231+
#. Extracted comment
232+
#: source.py:3
233+
#, fuzzy
234+
msgid "An immutable source ID must not cause mutable message data to be shared."
235+
msgstr "translation"
236+
'''
237+
first = pofile.read_po(StringIO(po))
238+
second = pofile.read_po(StringIO(po))
239+
identifier = "An immutable source ID must not cause mutable message data to be shared."
240+
message = first[identifier]
241+
message.string = "changed"
242+
message.flags.add("custom")
243+
message.locations.append(("other.py", 9))
244+
message.user_comments.append("additional translator comment")
245+
message.auto_comments.append("additional extracted comment")
246+
assert message is not second[identifier]
247+
assert second[identifier].string == "translation"
248+
assert second[identifier].flags == {"fuzzy"}
249+
assert second[identifier].locations == [("source.py", 3)]
250+
assert second[identifier].user_comments == ["Translator comment"]
251+
assert second[identifier].auto_comments == ["Extracted comment"]
252+
253+
254+
def test_read_po_preserves_distinct_contexts():
255+
catalog = pofile.read_po(
256+
StringIO('''\
257+
msgid "A source ID occurring in distinct contexts."
258+
msgstr "without context"
259+
260+
msgctxt ""
261+
msgid "A source ID occurring in distinct contexts."
262+
msgstr "empty context"
263+
264+
msgctxt "menu"
265+
msgid "A source ID occurring in distinct contexts."
266+
msgstr "menu context"
267+
'''),
268+
)
269+
identifier = "A source ID occurring in distinct contexts."
270+
assert len(catalog) == 3
271+
for context, translation in (
272+
(None, "without context"),
273+
("", "empty context"),
274+
("menu", "menu context"),
275+
):
276+
message = catalog.get(identifier, context=context)
277+
assert message.id == identifier
278+
assert message.context == context
279+
assert message.string == translation
280+
281+
282+
def test_read_po_preserves_obsolete_and_duplicate_messages():
283+
po = '''\
284+
#: first.py:1
285+
msgid "An active source ID repeated with a second reference."
286+
msgstr "first translation"
287+
288+
#: second.py:2
289+
msgid "An active source ID repeated with a second reference."
290+
msgstr "second translation"
291+
292+
#~ msgid "An obsolete source ID retained separately from active messages."
293+
#~ msgstr "obsolete translation"
294+
'''
295+
catalog = pofile.read_po(StringIO(po))
296+
ignored = pofile.read_po(StringIO(po), ignore_obsolete=True)
297+
identifier = "An active source ID repeated with a second reference."
298+
assert len(catalog) == len(ignored) == 1
299+
assert catalog[identifier].string == "first translation"
300+
assert catalog[identifier].locations == [("first.py", 1), ("second.py", 2)]
301+
obsolete_id = "An obsolete source ID retained separately from active messages."
302+
assert catalog.obsolete[obsolete_id].string == "obsolete translation"
303+
assert not ignored.obsolete

0 commit comments

Comments
 (0)