]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Add basic support for `Message.python_brace_format` (#1169)
authorTomas R. <tomas.roun8@gmail.com>
Thu, 16 Jan 2025 11:09:32 +0000 (12:09 +0100)
committerGitHub <noreply@github.com>
Thu, 16 Jan 2025 11:09:32 +0000 (13:09 +0200)
babel/messages/catalog.py
tests/messages/test_catalog.py

index 060b0eb79d4c6ca4e626d5409fed558edce8952b..aaff8df010eae61c563c1aefd53a625c5db1c37d 100644 (file)
@@ -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<datetime>.*?)(?P<tzoffset>[+-]\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
index 16d36756b02b817e3d1e7b402aa65a3b99aaeb59..64e76e9904ff8617c5a2d6298510b1d304c05fe9 100644 (file)
@@ -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')