Skip to content
Open
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
7 changes: 6 additions & 1 deletion babel/messages/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,18 @@ def _parse(string: str) -> list[tuple[str, str]]:
continue
if name is None:
# A star width or precision consumes an integer argument first.
result.extend((None, 'd') for _ in range(format.count('*')))
result.extend((None, '*') for _ in range(format.count('*')))
result.append((name, str(typechar)))
return result

def _compatible(a: str, b: str) -> bool:
if a == b:
return True
# Stars require integers, while decimal conversions also accept floats.
# An integer width/precision can therefore become a decimal conversion,
# but the reverse is not safe for all arguments accepted by the source.
if a == '*' and b in {'d', 'i', 'u'}:
return True
for set in _string_format_compatibilities:
if a in set and b in set:
return True
Expand Down
42 changes: 40 additions & 2 deletions tests/messages/test_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,47 @@ def test_python_format_star_arguments_valid(msgid, msgstr, arguments):
python_format(None, Message(msgid, msgstr))


def test_python_format_star_plural_catalog_check():
@pytest.mark.parametrize('conversion', ['d', 'i', 'u'])
@pytest.mark.parametrize('target', ['%*s', '%.*s'])
def test_python_format_decimal_cannot_become_star(conversion, target):
source = '%' + conversion + '%s'
arguments = (3.5, 'word')
assert isinstance(source % arguments, str)
with pytest.raises(TypeError, match=r'\* wants int'):
target % arguments
with pytest.raises(TranslationError):
python_format(None, Message(source, target))


@pytest.mark.parametrize('conversion', ['d', 'i', 'u'])
@pytest.mark.parametrize('source', ['%*s', '%.*s'])
@pytest.mark.parametrize('width', [-3, 3, True])
def test_python_format_star_can_become_decimal(conversion, source, width):
target = '%' + conversion + '%s'
arguments = (width, 'word')
assert isinstance(source % arguments, str)
assert isinstance(target % arguments, str)
python_format(None, Message(source, target))


@pytest.mark.parametrize('source', ['%d%.*s', '%*d%s'])
def test_python_format_decimal_cannot_become_second_star(source):
arguments = (3.5, 2, 'word') if source == '%d%.*s' else (3, 2.5, 'word')
target = '%*.*s'
assert isinstance(source % arguments, str)
with pytest.raises(TypeError, match=r'\* wants int'):
target % arguments
with pytest.raises(TranslationError):
python_format(None, Message(source, target))


@pytest.mark.parametrize(
('source', 'translation'),
[('%*s', '%s'), ('%d%s', '%*s'), ('%d%s', '%.*s')],
)
def test_python_format_star_plural_catalog_check(source, translation):
catalog = Catalog(locale='en')
message = catalog.add(('%*s', '%*s'), ('%*s', '%s'))
message = catalog.add((source, source), (source, translation))
errors = list(catalog.check())
assert len(errors) == 1
assert errors[0][0] is message
Expand Down