Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions babel/messages/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from difflib import SequenceMatcher
from email import message_from_string
from heapq import nlargest
from string import Formatter
from typing import TYPE_CHECKING

from babel import __version__ as VERSION
Expand Down Expand Up @@ -69,6 +70,25 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6):
''', re.VERBOSE)


def _has_python_brace_format(string: str) -> bool:
if "{" not in string:
return False
fmt = Formatter()
try:
# `fmt.parse` returns 3-or-4-tuples of the form
# `(literal_text, field_name, format_spec, conversion)`;
# if `field_name` is set, this smells like brace format
field_name_seen = False
for t in fmt.parse(string):
if t[1] is not None:
field_name_seen = True
# We cannot break here, as we need to consume the whole string
# to ensure that it is a valid format string.
except ValueError:
return False
return field_name_seen


def _parse_datetime_header(value: str) -> datetime.datetime:
match = re.match(r'^(?P<datetime>.*?)(?P<tzoffset>[+-]\d{4})?$', value)

Expand Down Expand Up @@ -140,6 +160,10 @@ def __init__(
self.flags.add('python-format')
else:
self.flags.discard('python-format')
if id and self.python_brace_format:
self.flags.add('python-brace-format')
else:
self.flags.discard('python-brace-format')
self.auto_comments = list(distinct(auto_comments))
self.user_comments = list(distinct(user_comments))
if isinstance(previous_id, str):
Expand Down Expand Up @@ -252,6 +276,21 @@ def python_format(self) -> bool:
ids = [ids]
return any(PYTHON_FORMAT.search(id) for id in ids)

@property
def python_brace_format(self) -> bool:
"""Whether the message contains Python f-string parameters.

>>> Message('Hello, {name}!').python_brace_format
True
>>> Message(('One apple', '{count} apples')).python_brace_format
True

:type: `bool`"""
ids = self.id
if not isinstance(ids, (list, tuple)):
ids = [ids]
return any(_has_python_brace_format(id) for id in ids)


class TranslationError(Exception):
"""Exception thrown by translation checkers when invalid message
Expand Down
27 changes: 27 additions & 0 deletions tests/messages/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def test_python_format(self):
assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f')
assert catalog.PYTHON_FORMAT.search('foo %()s')

def test_python_brace_format(self):
assert not catalog._has_python_brace_format('')
assert not catalog._has_python_brace_format('foo')
assert not catalog._has_python_brace_format('{')
assert not catalog._has_python_brace_format('}')
assert not catalog._has_python_brace_format('{} {')
assert not catalog._has_python_brace_format('{{}}')
assert catalog._has_python_brace_format('{}')
assert catalog._has_python_brace_format('foo {name}')
assert catalog._has_python_brace_format('foo {name!s}')
assert catalog._has_python_brace_format('foo {name!r}')
assert catalog._has_python_brace_format('foo {name!a}')
assert catalog._has_python_brace_format('foo {name!r:10}')
assert catalog._has_python_brace_format('foo {name!r:10.2}')
assert catalog._has_python_brace_format('foo {name!r:10.2f}')
assert catalog._has_python_brace_format('foo {name!r:10.2f} {name!r:10.2f}')
assert catalog._has_python_brace_format('foo {name!r:10.2f=}')

def test_translator_comments(self):
mess = catalog.Message('foo', user_comments=['Comment About `foo`'])
assert mess.user_comments == ['Comment About `foo`']
Expand Down Expand Up @@ -342,10 +360,19 @@ def test_message_pluralizable():


def test_message_python_format():
assert not catalog.Message('foo').python_format
assert not catalog.Message(('foo', 'foo')).python_format
assert catalog.Message('foo %(name)s bar').python_format
assert catalog.Message(('foo %(name)s', 'foo %(name)s')).python_format


def test_message_python_brace_format():
assert not catalog.Message('foo').python_brace_format
assert not catalog.Message(('foo', 'foo')).python_brace_format
assert catalog.Message('foo {name} bar').python_brace_format
assert catalog.Message(('foo {name}', 'foo {name}')).python_brace_format


def test_catalog():
cat = catalog.Catalog(project='Foobar', version='1.0',
copyright_holder='Foo Company')
Expand Down
Loading