From: Tomas R. Date: Thu, 16 Jan 2025 11:09:32 +0000 (+0100) Subject: Add basic support for `Message.python_brace_format` (#1169) X-Git-Tag: v2.17.0~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=98b9562c05e5276038c27ec12c12f3e92dc027b6;p=thirdparty%2Fbabel.git Add basic support for `Message.python_brace_format` (#1169) --- diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 060b0eb7..aaff8df0 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -16,6 +16,7 @@ from copy import copy 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 @@ -76,6 +77,25 @@ PYTHON_FORMAT = re.compile(r''' ''', 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.*?)(?P[+-]\d{4})?$', value) @@ -147,6 +167,10 @@ class Message: 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): @@ -259,6 +283,21 @@ class Message: 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 diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 16d36756..64e76e99 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -39,6 +39,24 @@ class MessageTestCase(unittest.TestCase): 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`'] @@ -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')