Skip to content

Commit 98b9562

Browse files
authored
Add basic support for Message.python_brace_format (#1169)
1 parent 0c1091c commit 98b9562

File tree

2 files changed

+66
-0
lines changed

2 files changed

+66
-0
lines changed

babel/messages/catalog.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from difflib import SequenceMatcher
1717
from email import message_from_string
1818
from heapq import nlargest
19+
from string import Formatter
1920
from typing import TYPE_CHECKING
2021

2122
from babel import __version__ as VERSION
@@ -76,6 +77,25 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6):
7677
''', re.VERBOSE)
7778

7879

80+
def _has_python_brace_format(string: str) -> bool:
81+
if "{" not in string:
82+
return False
83+
fmt = Formatter()
84+
try:
85+
# `fmt.parse` returns 3-or-4-tuples of the form
86+
# `(literal_text, field_name, format_spec, conversion)`;
87+
# if `field_name` is set, this smells like brace format
88+
field_name_seen = False
89+
for t in fmt.parse(string):
90+
if t[1] is not None:
91+
field_name_seen = True
92+
# We cannot break here, as we need to consume the whole string
93+
# to ensure that it is a valid format string.
94+
except ValueError:
95+
return False
96+
return field_name_seen
97+
98+
7999
def _parse_datetime_header(value: str) -> datetime.datetime:
80100
match = re.match(r'^(?P<datetime>.*?)(?P<tzoffset>[+-]\d{4})?$', value)
81101

@@ -147,6 +167,10 @@ def __init__(
147167
self.flags.add('python-format')
148168
else:
149169
self.flags.discard('python-format')
170+
if id and self.python_brace_format:
171+
self.flags.add('python-brace-format')
172+
else:
173+
self.flags.discard('python-brace-format')
150174
self.auto_comments = list(distinct(auto_comments))
151175
self.user_comments = list(distinct(user_comments))
152176
if isinstance(previous_id, str):
@@ -259,6 +283,21 @@ def python_format(self) -> bool:
259283
ids = [ids]
260284
return any(PYTHON_FORMAT.search(id) for id in ids)
261285

286+
@property
287+
def python_brace_format(self) -> bool:
288+
"""Whether the message contains Python f-string parameters.
289+
290+
>>> Message('Hello, {name}!').python_brace_format
291+
True
292+
>>> Message(('One apple', '{count} apples')).python_brace_format
293+
True
294+
295+
:type: `bool`"""
296+
ids = self.id
297+
if not isinstance(ids, (list, tuple)):
298+
ids = [ids]
299+
return any(_has_python_brace_format(id) for id in ids)
300+
262301

263302
class TranslationError(Exception):
264303
"""Exception thrown by translation checkers when invalid message

tests/messages/test_catalog.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ def test_python_format(self):
3939
assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f')
4040
assert catalog.PYTHON_FORMAT.search('foo %()s')
4141

42+
def test_python_brace_format(self):
43+
assert not catalog._has_python_brace_format('')
44+
assert not catalog._has_python_brace_format('foo')
45+
assert not catalog._has_python_brace_format('{')
46+
assert not catalog._has_python_brace_format('}')
47+
assert not catalog._has_python_brace_format('{} {')
48+
assert not catalog._has_python_brace_format('{{}}')
49+
assert catalog._has_python_brace_format('{}')
50+
assert catalog._has_python_brace_format('foo {name}')
51+
assert catalog._has_python_brace_format('foo {name!s}')
52+
assert catalog._has_python_brace_format('foo {name!r}')
53+
assert catalog._has_python_brace_format('foo {name!a}')
54+
assert catalog._has_python_brace_format('foo {name!r:10}')
55+
assert catalog._has_python_brace_format('foo {name!r:10.2}')
56+
assert catalog._has_python_brace_format('foo {name!r:10.2f}')
57+
assert catalog._has_python_brace_format('foo {name!r:10.2f} {name!r:10.2f}')
58+
assert catalog._has_python_brace_format('foo {name!r:10.2f=}')
59+
4260
def test_translator_comments(self):
4361
mess = catalog.Message('foo', user_comments=['Comment About `foo`'])
4462
assert mess.user_comments == ['Comment About `foo`']
@@ -342,10 +360,19 @@ def test_message_pluralizable():
342360

343361

344362
def test_message_python_format():
363+
assert not catalog.Message('foo').python_format
364+
assert not catalog.Message(('foo', 'foo')).python_format
345365
assert catalog.Message('foo %(name)s bar').python_format
346366
assert catalog.Message(('foo %(name)s', 'foo %(name)s')).python_format
347367

348368

369+
def test_message_python_brace_format():
370+
assert not catalog.Message('foo').python_brace_format
371+
assert not catalog.Message(('foo', 'foo')).python_brace_format
372+
assert catalog.Message('foo {name} bar').python_brace_format
373+
assert catalog.Message(('foo {name}', 'foo {name}')).python_brace_format
374+
375+
349376
def test_catalog():
350377
cat = catalog.Catalog(project='Foobar', version='1.0',
351378
copyright_holder='Foo Company')

0 commit comments

Comments
 (0)