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
''', 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)
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):
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
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`']
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')