|
16 | 16 | from difflib import SequenceMatcher
|
17 | 17 | from email import message_from_string
|
18 | 18 | from heapq import nlargest
|
| 19 | +from string import Formatter |
19 | 20 | from typing import TYPE_CHECKING
|
20 | 21 |
|
21 | 22 | from babel import __version__ as VERSION
|
@@ -76,6 +77,25 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6):
|
76 | 77 | ''', re.VERBOSE)
|
77 | 78 |
|
78 | 79 |
|
| 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 | + |
79 | 99 | def _parse_datetime_header(value: str) -> datetime.datetime:
|
80 | 100 | match = re.match(r'^(?P<datetime>.*?)(?P<tzoffset>[+-]\d{4})?$', value)
|
81 | 101 |
|
@@ -147,6 +167,10 @@ def __init__(
|
147 | 167 | self.flags.add('python-format')
|
148 | 168 | else:
|
149 | 169 | 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') |
150 | 174 | self.auto_comments = list(distinct(auto_comments))
|
151 | 175 | self.user_comments = list(distinct(user_comments))
|
152 | 176 | if isinstance(previous_id, str):
|
@@ -259,6 +283,21 @@ def python_format(self) -> bool:
|
259 | 283 | ids = [ids]
|
260 | 284 | return any(PYTHON_FORMAT.search(id) for id in ids)
|
261 | 285 |
|
| 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 | + |
262 | 301 |
|
263 | 302 | class TranslationError(Exception):
|
264 | 303 | """Exception thrown by translation checkers when invalid message
|
|
0 commit comments