1"""Tests to cover the Tools/i18n package""" 2 3import os 4import sys 5import unittest 6from textwrap import dedent 7 8from test.support.script_helper import assert_python_ok 9from test.test_tools import skip_if_missing, toolsdir 10from test.support.os_helper import temp_cwd, temp_dir 11 12 13skip_if_missing() 14 15 16class Test_pygettext(unittest.TestCase): 17 """Tests for the pygettext.py tool""" 18 19 script = os.path.join(toolsdir,'i18n', 'pygettext.py') 20 21 def get_header(self, data): 22 """ utility: return the header of a .po file as a dictionary """ 23 headers = {} 24 for line in data.split('\n'): 25 if not line or line.startswith(('#', 'msgid','msgstr')): 26 continue 27 line = line.strip('"') 28 key, val = line.split(':',1) 29 headers[key] = val.strip() 30 return headers 31 32 def get_msgids(self, data): 33 """ utility: return all msgids in .po file as a list of strings """ 34 msgids = [] 35 reading_msgid = False 36 cur_msgid = [] 37 for line in data.split('\n'): 38 if reading_msgid: 39 if line.startswith('"'): 40 cur_msgid.append(line.strip('"')) 41 else: 42 msgids.append('\n'.join(cur_msgid)) 43 cur_msgid = [] 44 reading_msgid = False 45 continue 46 if line.startswith('msgid '): 47 line = line[len('msgid '):] 48 cur_msgid.append(line.strip('"')) 49 reading_msgid = True 50 else: 51 if reading_msgid: 52 msgids.append('\n'.join(cur_msgid)) 53 54 return msgids 55 56 def extract_docstrings_from_str(self, module_content): 57 """ utility: return all msgids extracted from module_content """ 58 filename = 'test_docstrings.py' 59 with temp_cwd(None) as cwd: 60 with open(filename, 'w') as fp: 61 fp.write(module_content) 62 assert_python_ok(self.script, '-D', filename) 63 with open('messages.pot') as fp: 64 data = fp.read() 65 return self.get_msgids(data) 66 67 def test_header(self): 68 """Make sure the required fields are in the header, according to: 69 http://www.gnu.org/software/gettext/manual/gettext.html#Header-Entry 70 """ 71 with temp_cwd(None) as cwd: 72 assert_python_ok(self.script) 73 with open('messages.pot') as fp: 74 data = fp.read() 75 header = self.get_header(data) 76 77 self.assertIn("Project-Id-Version", header) 78 self.assertIn("POT-Creation-Date", header) 79 self.assertIn("PO-Revision-Date", header) 80 self.assertIn("Last-Translator", header) 81 self.assertIn("Language-Team", header) 82 self.assertIn("MIME-Version", header) 83 self.assertIn("Content-Type", header) 84 self.assertIn("Content-Transfer-Encoding", header) 85 self.assertIn("Generated-By", header) 86 87 # not clear if these should be required in POT (template) files 88 #self.assertIn("Report-Msgid-Bugs-To", header) 89 #self.assertIn("Language", header) 90 91 #"Plural-Forms" is optional 92 93 @unittest.skipIf(sys.platform.startswith('aix'), 94 'bpo-29972: broken test on AIX') 95 def test_POT_Creation_Date(self): 96 """ Match the date format from xgettext for POT-Creation-Date """ 97 from datetime import datetime 98 with temp_cwd(None) as cwd: 99 assert_python_ok(self.script) 100 with open('messages.pot') as fp: 101 data = fp.read() 102 header = self.get_header(data) 103 creationDate = header['POT-Creation-Date'] 104 105 # peel off the escaped newline at the end of string 106 if creationDate.endswith('\\n'): 107 creationDate = creationDate[:-len('\\n')] 108 109 # This will raise if the date format does not exactly match. 110 datetime.strptime(creationDate, '%Y-%m-%d %H:%M%z') 111 112 def test_funcdocstring(self): 113 for doc in ('"""doc"""', "r'''doc'''", "R'doc'", 'u"doc"'): 114 with self.subTest(doc): 115 msgids = self.extract_docstrings_from_str(dedent('''\ 116 def foo(bar): 117 %s 118 ''' % doc)) 119 self.assertIn('doc', msgids) 120 121 def test_funcdocstring_bytes(self): 122 msgids = self.extract_docstrings_from_str(dedent('''\ 123 def foo(bar): 124 b"""doc""" 125 ''')) 126 self.assertFalse([msgid for msgid in msgids if 'doc' in msgid]) 127 128 def test_funcdocstring_fstring(self): 129 msgids = self.extract_docstrings_from_str(dedent('''\ 130 def foo(bar): 131 f"""doc""" 132 ''')) 133 self.assertFalse([msgid for msgid in msgids if 'doc' in msgid]) 134 135 def test_classdocstring(self): 136 for doc in ('"""doc"""', "r'''doc'''", "R'doc'", 'u"doc"'): 137 with self.subTest(doc): 138 msgids = self.extract_docstrings_from_str(dedent('''\ 139 class C: 140 %s 141 ''' % doc)) 142 self.assertIn('doc', msgids) 143 144 def test_classdocstring_bytes(self): 145 msgids = self.extract_docstrings_from_str(dedent('''\ 146 class C: 147 b"""doc""" 148 ''')) 149 self.assertFalse([msgid for msgid in msgids if 'doc' in msgid]) 150 151 def test_classdocstring_fstring(self): 152 msgids = self.extract_docstrings_from_str(dedent('''\ 153 class C: 154 f"""doc""" 155 ''')) 156 self.assertFalse([msgid for msgid in msgids if 'doc' in msgid]) 157 158 def test_msgid(self): 159 msgids = self.extract_docstrings_from_str( 160 '''_("""doc""" r'str' u"ing")''') 161 self.assertIn('docstring', msgids) 162 163 def test_msgid_bytes(self): 164 msgids = self.extract_docstrings_from_str('_(b"""doc""")') 165 self.assertFalse([msgid for msgid in msgids if 'doc' in msgid]) 166 167 def test_msgid_fstring(self): 168 msgids = self.extract_docstrings_from_str('_(f"""doc""")') 169 self.assertFalse([msgid for msgid in msgids if 'doc' in msgid]) 170 171 def test_funcdocstring_annotated_args(self): 172 """ Test docstrings for functions with annotated args """ 173 msgids = self.extract_docstrings_from_str(dedent('''\ 174 def foo(bar: str): 175 """doc""" 176 ''')) 177 self.assertIn('doc', msgids) 178 179 def test_funcdocstring_annotated_return(self): 180 """ Test docstrings for functions with annotated return type """ 181 msgids = self.extract_docstrings_from_str(dedent('''\ 182 def foo(bar) -> str: 183 """doc""" 184 ''')) 185 self.assertIn('doc', msgids) 186 187 def test_funcdocstring_defvalue_args(self): 188 """ Test docstring for functions with default arg values """ 189 msgids = self.extract_docstrings_from_str(dedent('''\ 190 def foo(bar=()): 191 """doc""" 192 ''')) 193 self.assertIn('doc', msgids) 194 195 def test_funcdocstring_multiple_funcs(self): 196 """ Test docstring extraction for multiple functions combining 197 annotated args, annotated return types and default arg values 198 """ 199 msgids = self.extract_docstrings_from_str(dedent('''\ 200 def foo1(bar: tuple=()) -> str: 201 """doc1""" 202 203 def foo2(bar: List[1:2]) -> (lambda x: x): 204 """doc2""" 205 206 def foo3(bar: 'func'=lambda x: x) -> {1: 2}: 207 """doc3""" 208 ''')) 209 self.assertIn('doc1', msgids) 210 self.assertIn('doc2', msgids) 211 self.assertIn('doc3', msgids) 212 213 def test_classdocstring_early_colon(self): 214 """ Test docstring extraction for a class with colons occurring within 215 the parentheses. 216 """ 217 msgids = self.extract_docstrings_from_str(dedent('''\ 218 class D(L[1:2], F({1: 2}), metaclass=M(lambda x: x)): 219 """doc""" 220 ''')) 221 self.assertIn('doc', msgids) 222 223 def test_calls_in_fstrings(self): 224 msgids = self.extract_docstrings_from_str(dedent('''\ 225 f"{_('foo bar')}" 226 ''')) 227 self.assertIn('foo bar', msgids) 228 229 def test_calls_in_fstrings_raw(self): 230 msgids = self.extract_docstrings_from_str(dedent('''\ 231 rf"{_('foo bar')}" 232 ''')) 233 self.assertIn('foo bar', msgids) 234 235 def test_calls_in_fstrings_nested(self): 236 msgids = self.extract_docstrings_from_str(dedent('''\ 237 f"""{f'{_("foo bar")}'}""" 238 ''')) 239 self.assertIn('foo bar', msgids) 240 241 def test_calls_in_fstrings_attribute(self): 242 msgids = self.extract_docstrings_from_str(dedent('''\ 243 f"{obj._('foo bar')}" 244 ''')) 245 self.assertIn('foo bar', msgids) 246 247 def test_calls_in_fstrings_with_call_on_call(self): 248 msgids = self.extract_docstrings_from_str(dedent('''\ 249 f"{type(str)('foo bar')}" 250 ''')) 251 self.assertNotIn('foo bar', msgids) 252 253 def test_calls_in_fstrings_with_format(self): 254 msgids = self.extract_docstrings_from_str(dedent('''\ 255 f"{_('foo {bar}').format(bar='baz')}" 256 ''')) 257 self.assertIn('foo {bar}', msgids) 258 259 def test_calls_in_fstrings_with_wrong_input_1(self): 260 msgids = self.extract_docstrings_from_str(dedent('''\ 261 f"{_(f'foo {bar}')}" 262 ''')) 263 self.assertFalse([msgid for msgid in msgids if 'foo {bar}' in msgid]) 264 265 def test_calls_in_fstrings_with_wrong_input_2(self): 266 msgids = self.extract_docstrings_from_str(dedent('''\ 267 f"{_(1)}" 268 ''')) 269 self.assertNotIn(1, msgids) 270 271 def test_calls_in_fstring_with_multiple_args(self): 272 msgids = self.extract_docstrings_from_str(dedent('''\ 273 f"{_('foo', 'bar')}" 274 ''')) 275 self.assertNotIn('foo', msgids) 276 self.assertNotIn('bar', msgids) 277 278 def test_calls_in_fstring_with_keyword_args(self): 279 msgids = self.extract_docstrings_from_str(dedent('''\ 280 f"{_('foo', bar='baz')}" 281 ''')) 282 self.assertNotIn('foo', msgids) 283 self.assertNotIn('bar', msgids) 284 self.assertNotIn('baz', msgids) 285 286 def test_calls_in_fstring_with_partially_wrong_expression(self): 287 msgids = self.extract_docstrings_from_str(dedent('''\ 288 f"{_(f'foo') + _('bar')}" 289 ''')) 290 self.assertNotIn('foo', msgids) 291 self.assertIn('bar', msgids) 292 293 def test_files_list(self): 294 """Make sure the directories are inspected for source files 295 bpo-31920 296 """ 297 text1 = 'Text to translate1' 298 text2 = 'Text to translate2' 299 text3 = 'Text to ignore' 300 with temp_cwd(None), temp_dir(None) as sdir: 301 os.mkdir(os.path.join(sdir, 'pypkg')) 302 with open(os.path.join(sdir, 'pypkg', 'pymod.py'), 'w') as sfile: 303 sfile.write(f'_({text1!r})') 304 os.mkdir(os.path.join(sdir, 'pkg.py')) 305 with open(os.path.join(sdir, 'pkg.py', 'pymod2.py'), 'w') as sfile: 306 sfile.write(f'_({text2!r})') 307 os.mkdir(os.path.join(sdir, 'CVS')) 308 with open(os.path.join(sdir, 'CVS', 'pymod3.py'), 'w') as sfile: 309 sfile.write(f'_({text3!r})') 310 assert_python_ok(self.script, sdir) 311 with open('messages.pot') as fp: 312 data = fp.read() 313 self.assertIn(f'msgid "{text1}"', data) 314 self.assertIn(f'msgid "{text2}"', data) 315 self.assertNotIn(text3, data) 316