|
4 | 4 | """ |
5 | 5 | import unittest |
6 | 6 | import warnings |
7 | | -from test.support import warnings_helper |
| 7 | +from test.support import subTests, warnings_helper |
8 | 8 | from textwrap import dedent |
| 9 | +import functools |
9 | 10 |
|
10 | | -from codeop import compile_command, PyCF_DONT_IMPLY_DEDENT |
| 11 | +from codeop import compile_command, CommandCompiler, Compile |
| 12 | +from codeop import PyCF_DONT_IMPLY_DEDENT, PyCF_ONLY_AST |
| 13 | +import ast |
| 14 | + |
| 15 | + |
| 16 | +WRAPPING_COMPILERS = [compile_command, CommandCompiler()] |
| 17 | +RAW_COMPILERS = [Compile()] |
| 18 | +COMPILERS = WRAPPING_COMPILERS + RAW_COMPILERS |
11 | 19 |
|
12 | | -class CodeopTests(unittest.TestCase): |
13 | 20 |
|
14 | | - def assertValid(self, str, symbol='single'): |
| 21 | +class CodeopTests(unittest.TestCase): |
| 22 | + def assertValid(self, str, symbol='single', *, compiler): |
15 | 23 | '''succeed iff str is a valid piece of code''' |
16 | 24 | expected = compile(str, "<input>", symbol, PyCF_DONT_IMPLY_DEDENT) |
17 | | - self.assertEqual(compile_command(str, "<input>", symbol), expected) |
| 25 | + self.assertEqual(compiler(str, "<input>", symbol), expected) |
18 | 26 |
|
19 | | - def assertIncomplete(self, str, symbol='single'): |
| 27 | + def assertIncomplete(self, str, symbol='single', *, compiler): |
20 | 28 | '''succeed iff str is the start of a valid piece of code''' |
21 | | - self.assertEqual(compile_command(str, symbol=symbol), None) |
22 | | - |
23 | | - def assertInvalid(self, str, symbol='single', is_syntax=1): |
| 29 | + if compiler in WRAPPING_COMPILERS: |
| 30 | + self.assertEqual(compiler(str, "<input>", symbol=symbol), None) |
| 31 | + else: |
| 32 | + # Compile has should raise like built-in compile |
| 33 | + with self.assertRaises(SyntaxError) as cm_original_error: |
| 34 | + compile(str, "<input>", symbol, compiler.flags) |
| 35 | + expected_error = cm_original_error.exception |
| 36 | + with self.assertRaises(type(expected_error)) as cm_wrapped_error: |
| 37 | + compiler(str, "<input>", symbol=symbol) |
| 38 | + self.assertEqual( |
| 39 | + expected_error.args, |
| 40 | + cm_wrapped_error.exception.args |
| 41 | + ) |
| 42 | + |
| 43 | + def assertInvalid(self, str, symbol='single', is_syntax=1, *, compiler): |
24 | 44 | '''succeed iff str is the start of an invalid piece of code''' |
25 | 45 | try: |
26 | | - compile_command(str,symbol=symbol) |
| 46 | + compiler(str,"<input>", symbol=symbol) |
27 | 47 | self.fail("No exception raised for invalid code") |
28 | 48 | except SyntaxError: |
29 | 49 | self.assertTrue(is_syntax) |
30 | 50 | except OverflowError: |
31 | 51 | self.assertTrue(not is_syntax) |
32 | 52 |
|
33 | | - def test_valid(self): |
34 | | - av = self.assertValid |
35 | | - |
36 | | - # special case |
37 | | - self.assertEqual(compile_command(""), |
38 | | - compile("pass", "<input>", 'single', |
39 | | - PyCF_DONT_IMPLY_DEDENT)) |
40 | | - self.assertEqual(compile_command("\n"), |
41 | | - compile("pass", "<input>", 'single', |
42 | | - PyCF_DONT_IMPLY_DEDENT)) |
43 | | - |
| 53 | + @subTests('compiler', WRAPPING_COMPILERS) |
| 54 | + def test_empty(self, compiler): |
| 55 | + self.assertEqual( |
| 56 | + compiler("", "<input>", 'single'), |
| 57 | + compile("pass", "<input>", 'single', PyCF_DONT_IMPLY_DEDENT)) |
| 58 | + self.assertEqual( |
| 59 | + compiler("\n", "<input>", 'single'), |
| 60 | + compile("pass", "<input>", 'single', PyCF_DONT_IMPLY_DEDENT)) |
| 61 | + |
| 62 | + @subTests('compiler', COMPILERS) |
| 63 | + def test_valid(self, compiler): |
| 64 | + av = functools.partial(self.assertValid, compiler=compiler) |
44 | 65 | av("a = 1") |
45 | 66 | av("\na = 1") |
46 | 67 | av("a = 1\n") |
@@ -92,8 +113,9 @@ def test_valid(self): |
92 | 113 | av("def f():\n pass\n#foo\n") |
93 | 114 | av("@a.b.c\ndef f():\n pass\n") |
94 | 115 |
|
95 | | - def test_incomplete(self): |
96 | | - ai = self.assertIncomplete |
| 116 | + @subTests('compiler', COMPILERS) |
| 117 | + def test_incomplete(self, compiler): |
| 118 | + ai = functools.partial(self.assertIncomplete, compiler=compiler) |
97 | 119 |
|
98 | 120 | ai("(a **") |
99 | 121 | ai("(a,b,") |
@@ -226,8 +248,9 @@ def test_incomplete(self): |
226 | 248 | ai('a = f"""') |
227 | 249 | ai('a = \\') |
228 | 250 |
|
229 | | - def test_invalid(self): |
230 | | - ai = self.assertInvalid |
| 251 | + @subTests('compiler', COMPILERS) |
| 252 | + def test_invalid(self, compiler): |
| 253 | + ai = functools.partial(self.assertInvalid, compiler=compiler) |
231 | 254 | ai("a b") |
232 | 255 |
|
233 | 256 | ai("a @") |
@@ -263,67 +286,104 @@ def test_invalid(self): |
263 | 286 |
|
264 | 287 | ai("[i for i in range(10)] = (1, 2, 3)") |
265 | 288 |
|
266 | | - def test_invalid_exec(self): |
267 | | - ai = self.assertInvalid |
| 289 | + @subTests('compiler', COMPILERS) |
| 290 | + def test_invalid_exec(self, compiler): |
| 291 | + ai = functools.partial(self.assertInvalid, compiler=compiler) |
268 | 292 | ai("raise = 4", symbol="exec") |
269 | 293 | ai('def a-b', symbol='exec') |
270 | 294 | ai('await?', symbol='exec') |
271 | 295 | ai('=!=', symbol='exec') |
272 | 296 | ai('a await raise b', symbol='exec') |
273 | 297 | ai('a await raise b?+1', symbol='exec') |
274 | 298 |
|
275 | | - def test_filename(self): |
276 | | - self.assertEqual(compile_command("a = 1\n", "abc").co_filename, |
277 | | - compile("a = 1\n", "abc", 'single').co_filename) |
278 | | - self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename, |
279 | | - compile("a = 1\n", "def", 'single').co_filename) |
280 | | - |
281 | | - def test_warning(self): |
| 299 | + @subTests('compiler', COMPILERS) |
| 300 | + def test_filename(self, compiler): |
| 301 | + self.assertEqual( |
| 302 | + compiler("a = 1\n", "abc", "single").co_filename, |
| 303 | + compile("a = 1\n", "abc", 'single').co_filename |
| 304 | + ) |
| 305 | + self.assertNotEqual( |
| 306 | + compiler("a = 1\n", "abc", "single").co_filename, |
| 307 | + compile("a = 1\n", "def", 'single').co_filename |
| 308 | + ) |
| 309 | + |
| 310 | + def assertReturnsModule(self, code, compiler): |
| 311 | + retval = compiler(code, "<input>", 'exec', PyCF_ONLY_AST) |
| 312 | + self.assertIsInstance(retval, ast.Module) |
| 313 | + |
| 314 | + @subTests('compiler', RAW_COMPILERS) |
| 315 | + def test_ast_return_value(self, compiler): |
| 316 | + validate_ast = self.assertReturnsModule |
| 317 | + validate_ast("x = 5", compiler) |
| 318 | + validate_ast("\nx = 5", compiler) |
| 319 | + validate_ast("x = 5\n", compiler) |
| 320 | + validate_ast("x = 5\n\n", compiler) |
| 321 | + validate_ast("\n\nx = 5\n\n", compiler) |
| 322 | + |
| 323 | + @subTests('compiler', COMPILERS) |
| 324 | + def test_warning(self, compiler): |
282 | 325 | # Test that the warning is only returned once. |
283 | 326 | with warnings_helper.check_warnings( |
284 | 327 | ('"is" with \'str\' literal', SyntaxWarning), |
285 | 328 | ('"\\\\e" is an invalid escape sequence', SyntaxWarning), |
286 | 329 | ) as w: |
287 | | - compile_command(r"'\e' is 0") |
288 | | - self.assertEqual(len(w.warnings), 2) |
| 330 | + compiler(r"'\e' is 0", "<input>", "single") |
| 331 | + self.assertEqual(len(w.warnings), 2) |
289 | 332 |
|
290 | 333 | # bpo-41520: check SyntaxWarning treated as an SyntaxError |
291 | 334 | with warnings.catch_warnings(), self.assertRaises(SyntaxError): |
292 | 335 | warnings.simplefilter('error', SyntaxWarning) |
293 | | - compile_command('1 is 1', symbol='exec') |
| 336 | + compiler('1 is 1', "<input>", 'exec') |
294 | 337 |
|
295 | 338 | # Check SyntaxWarning treated as an SyntaxError |
296 | 339 | with warnings.catch_warnings(), self.assertRaises(SyntaxError): |
297 | 340 | warnings.simplefilter('error', SyntaxWarning) |
298 | | - compile_command(r"'\e'", symbol='exec') |
| 341 | + compiler(r"'\e'", "<input>", 'exec') |
299 | 342 |
|
300 | | - def test_incomplete_warning(self): |
| 343 | + @subTests('compiler', WRAPPING_COMPILERS) |
| 344 | + def test_incomplete_warning(self, compiler): |
301 | 345 | with warnings.catch_warnings(record=True) as w: |
302 | 346 | warnings.simplefilter('always') |
303 | | - self.assertIncomplete("'\\e' + (") |
| 347 | + compiler("'\\e' + (") |
304 | 348 | self.assertEqual(w, []) |
305 | 349 |
|
306 | | - def test_invalid_warning(self): |
| 350 | + @subTests('compiler', RAW_COMPILERS) |
| 351 | + def test_raw_raises_error(self, compiler): |
| 352 | + warnings_cm = warnings_helper.check_warnings( |
| 353 | + ('"\\\\e" is an invalid esceape sequence', SyntaxWarning) |
| 354 | + ) |
| 355 | + with self.assertRaises(SyntaxError), warnings_cm as w: |
| 356 | + compiler("'\\e' + (", "<input>", 'single') |
| 357 | + self.assertEqual(len(w.warnings), 1) |
| 358 | + |
| 359 | + @subTests('compiler', COMPILERS) |
| 360 | + def test_invalid_warning(self, compiler): |
307 | 361 | with warnings.catch_warnings(record=True) as w: |
308 | 362 | warnings.simplefilter('always') |
309 | | - self.assertInvalid("'\\e' 1") |
| 363 | + self.assertInvalid("'\\e' 1", compiler=compiler) |
310 | 364 | self.assertEqual(len(w), 1) |
311 | | - self.assertEqual(w[0].category, SyntaxWarning) |
312 | | - self.assertRegex(str(w[0].message), 'invalid escape sequence') |
313 | | - self.assertEqual(w[0].filename, '<input>') |
314 | | - |
315 | | - def assertSyntaxErrorMatches(self, code, message): |
316 | | - with self.subTest(code): |
317 | | - with self.assertRaisesRegex(SyntaxError, message): |
318 | | - compile_command(code, symbol='exec') |
319 | | - |
320 | | - def test_syntax_errors(self): |
321 | | - self.assertSyntaxErrorMatches( |
322 | | - dedent("""\ |
| 365 | + for warning in w: |
| 366 | + self.assertEqual(warning.category, SyntaxWarning) |
| 367 | + self.assertRegex(str(warning), 'invalid escape sequence') |
| 368 | + self.assertEqual(warning.filename, '<input>') |
| 369 | + |
| 370 | + @subTests('compiler', COMPILERS) |
| 371 | + def test_syntax_errors(self, compiler): |
| 372 | + code = dedent("""\ |
323 | 373 | def foo(x,x): |
324 | 374 | pass |
325 | | - """), "duplicate parameter 'x' in function definition") |
326 | | - |
| 375 | + """) |
| 376 | + message = "duplicate parameter 'x' in function definition" |
| 377 | + with self.assertRaisesRegex(SyntaxError, message): |
| 378 | + compiler(code, "<input>", 'exec') |
| 379 | + |
| 380 | + @subTests('compiler', RAW_COMPILERS) |
| 381 | + def test_future_imports(self, compiler): |
| 382 | + original_flags = compiler.flags |
| 383 | + compiler('from __future__ import annotations', "<input>", 'single') |
| 384 | + self.assertGreater(compiler.flags, original_flags) |
| 385 | + # reset flags to ensure test has no side-effects |
| 386 | + compiler.flags = original_flags |
327 | 387 |
|
328 | 388 |
|
329 | 389 | if __name__ == "__main__": |
|
0 commit comments