Skip to content

Commit d390b61

Browse files
committed
Add support for Python string formating
Refactor `replace_formatters` to use regex substitution to replace format strings in both C (previously supported) and now Python. Add support for Python str.__mod__() and str.format() notation.
1 parent b97bc16 commit d390b61

3 files changed

Lines changed: 67 additions & 58 deletions

File tree

msgcheck/po.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def check_spelling(self, spelling, checkers):
265265
continue
266266
text = mstr if spelling == 'str' else mid
267267
if self.fmt:
268-
text = replace_formatters(text, ' ', self.fmt)
268+
text = replace_formatters(text, self.fmt)
269269
checkers[0].set_text(text)
270270
misspelled = []
271271
for err in checkers[0]:

msgcheck/utils.py

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,23 @@
2424

2525
from __future__ import print_function
2626

27+
import re
28+
from collections import defaultdict
2729

28-
# TODO: add support for other languages
29-
STR_FORMATTERS = {
30-
'c': ('\\', '%', '#- +\'I.0123456789hlLqjzt', 'diouxXeEfFgGaAcsCSpnm'),
31-
}
30+
31+
STR_FORMATTERS = defaultdict(list)
32+
STR_FORMATTERS.update({
33+
'c': (
34+
(r'[\%]{2}', '%'),
35+
(r'\%([ hlL\d\.\-\+\#\*]+)?[cdieEfgGosuxXpn]', r''),
36+
),
37+
'python': [
38+
(r'[\%]{2}', '%'),
39+
(r'\%([.\d]+)?[bcdeEfFgGnosxX]', r''),
40+
(r'\%(\(([^)]*)\))([.\d]+)?[bcdeEfFgGnosxX]', r'\g<2>'),
41+
(r'\{([^\:\}]*)?(:[^\}]*)?\}', r''),
42+
]
43+
})
3244

3345

3446
def count_lines(string):
@@ -39,47 +51,10 @@ def count_lines(string):
3951
return count
4052

4153

42-
# pylint: disable=too-many-branches
43-
def replace_formatters(string, replace, fmt):
44-
"""
54+
def replace_formatters(string, fmt):
55+
r"""
4556
Replace formatters (like "%s" or "%03d") with a replacement string.
4657
"""
47-
if fmt not in STR_FORMATTERS:
48-
return string
49-
formatters = STR_FORMATTERS[fmt]
50-
formatter, escape = (False, False)
51-
strformat = []
52-
result = []
53-
54-
for char in string:
55-
if formatter:
56-
if char == formatters[1]:
57-
result.append(char)
58-
formatter = False
59-
elif char in formatters[2]:
60-
strformat.append(char)
61-
elif char in formatters[3]:
62-
result.append(replace)
63-
formatter = False
64-
else:
65-
strformat.append(char)
66-
result += strformat
67-
formatter = False
68-
elif escape:
69-
result.append(formatters[0])
70-
result.append(char)
71-
escape = False
72-
elif char == formatters[0]:
73-
escape = True
74-
elif char == formatters[1]:
75-
formatter = True
76-
strformat = [char]
77-
else:
78-
result.append(char)
79-
80-
if escape: # unterminated escaped char?
81-
result.append(formatters[0])
82-
elif formatter: # unterminated formatter?
83-
result.append(replace)
84-
85-
return ''.join(result)
58+
for pattern, repl in STR_FORMATTERS[fmt]:
59+
string = re.sub(pattern, repl, string)
60+
return string

tests/test_msgcheck.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,21 +96,55 @@ def test_checks_fuzzy(self):
9696
# the file has 11 errors (with the fuzzy string)
9797
self.assertEqual(len(result[0][1]), 11)
9898

99-
def test_replace_formatters(self):
100-
"""Test removal of formatters in a string."""
101-
self.assertEqual(replace_formatters('%', '', 'c'), '')
102-
self.assertEqual(replace_formatters('\\', '', 'c'), '\\')
103-
self.assertEqual(replace_formatters('%s', ' ', 'c'), ' ')
104-
self.assertEqual(replace_formatters('%.02f', ' ', 'c'), ' ')
105-
self.assertEqual(replace_formatters('%!%s%!', '', 'c'), '%!%!')
106-
self.assertEqual(replace_formatters('%.02!', ' ', 'c'), '%.02!')
99+
def test_replace_formatters_c(self):
100+
"""Test removal of formatters in a C string."""
101+
self.assertEqual(replace_formatters('%s', 'c'), '')
102+
self.assertEqual(replace_formatters('%%', 'c'), '%')
103+
self.assertEqual(replace_formatters('%.02f', 'c'), '')
104+
self.assertEqual(replace_formatters('%!%s%!', 'c'), '%!%!')
105+
self.assertEqual(replace_formatters('%.02!', 'c'), '%.02!')
107106
self.assertEqual(
108-
replace_formatters('%.3fThis is a %stest', ' ', 'c'),
109-
' This is a test')
107+
replace_formatters('%.3fThis is a %stest', 'c'),
108+
'This is a test')
110109
self.assertEqual(
111-
replace_formatters('%.3fTest%s%d%%%.03f%luhere% s', '', 'c'),
110+
replace_formatters('%.3fTest%s%d%%%.03f%luhere% s', 'c'),
112111
'Test%here')
113112

113+
def test_replace_formatters_python(self):
114+
"""Test removal of formatters in a python string."""
115+
# str.__mod__()
116+
self.assertEqual(replace_formatters('%s', 'python'), '')
117+
self.assertEqual(replace_formatters('%b', 'python'), '')
118+
self.assertEqual(replace_formatters('%%', 'python'), '%')
119+
self.assertEqual(replace_formatters('%.02f', 'python'), '')
120+
self.assertEqual(replace_formatters('%(sth)s', 'python'), 'sth')
121+
self.assertEqual(replace_formatters('%(sth)02f', 'python'), 'sth')
122+
# str.format()
123+
conditions = [
124+
(
125+
'First, thou shalt count to {0}', 'First, thou shalt count to ',
126+
'References first positional argument'),
127+
(
128+
'Bring me a {}', 'Bring me a ',
129+
'Implicitly references the first positional argument'),
130+
('From {} to {}', 'From to ', 'Same as "From {0} to {1}"'),
131+
(
132+
'My quest is {name}', 'My quest is ',
133+
'References keyword argument \'name\''),
134+
(
135+
'Weight in tons {0.weight}', 'Weight in tons ',
136+
'\'weight\' attribute of first positional arg'),
137+
(
138+
'Units destroyed: {players[0]}', 'Units destroyed: ',
139+
'First element of keyword argument \'players\'.'),
140+
]
141+
for condition in conditions:
142+
self.assertEqual(
143+
replace_formatters(condition[0], 'python'),
144+
condition[1],
145+
condition[2],
146+
)
147+
114148
def test_spelling_id(self):
115149
"""Test spelling on source messages (English) of gettext files."""
116150
po_check = PoCheck()

0 commit comments

Comments
 (0)