Skip to content

Commit 405daf5

Browse files
vjabuildstomasr8
andauthored
Improved test coverage for codeop.py. (GH-154100)
* Ported existing tests to cover both the CommandCompiler and Compile classes * Added tests that cover the case when PyCF_ONLY_AST is set * Added tests that cover the case when the compile method returns a code object with flags Co-authored-by: Tomas R. <tomas.roun8@gmail.com>
1 parent 0e54a40 commit 405daf5

1 file changed

Lines changed: 116 additions & 56 deletions

File tree

Lib/test/test_codeop.py

Lines changed: 116 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,64 @@
44
"""
55
import unittest
66
import warnings
7-
from test.support import warnings_helper
7+
from test.support import subTests, warnings_helper
88
from textwrap import dedent
9+
import functools
910

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
1119

12-
class CodeopTests(unittest.TestCase):
1320

14-
def assertValid(self, str, symbol='single'):
21+
class CodeopTests(unittest.TestCase):
22+
def assertValid(self, str, symbol='single', *, compiler):
1523
'''succeed iff str is a valid piece of code'''
1624
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)
1826

19-
def assertIncomplete(self, str, symbol='single'):
27+
def assertIncomplete(self, str, symbol='single', *, compiler):
2028
'''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):
2444
'''succeed iff str is the start of an invalid piece of code'''
2545
try:
26-
compile_command(str,symbol=symbol)
46+
compiler(str,"<input>", symbol=symbol)
2747
self.fail("No exception raised for invalid code")
2848
except SyntaxError:
2949
self.assertTrue(is_syntax)
3050
except OverflowError:
3151
self.assertTrue(not is_syntax)
3252

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)
4465
av("a = 1")
4566
av("\na = 1")
4667
av("a = 1\n")
@@ -92,8 +113,9 @@ def test_valid(self):
92113
av("def f():\n pass\n#foo\n")
93114
av("@a.b.c\ndef f():\n pass\n")
94115

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)
97119

98120
ai("(a **")
99121
ai("(a,b,")
@@ -226,8 +248,9 @@ def test_incomplete(self):
226248
ai('a = f"""')
227249
ai('a = \\')
228250

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)
231254
ai("a b")
232255

233256
ai("a @")
@@ -263,67 +286,104 @@ def test_invalid(self):
263286

264287
ai("[i for i in range(10)] = (1, 2, 3)")
265288

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)
268292
ai("raise = 4", symbol="exec")
269293
ai('def a-b', symbol='exec')
270294
ai('await?', symbol='exec')
271295
ai('=!=', symbol='exec')
272296
ai('a await raise b', symbol='exec')
273297
ai('a await raise b?+1', symbol='exec')
274298

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):
282325
# Test that the warning is only returned once.
283326
with warnings_helper.check_warnings(
284327
('"is" with \'str\' literal', SyntaxWarning),
285328
('"\\\\e" is an invalid escape sequence', SyntaxWarning),
286329
) 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)
289332

290333
# bpo-41520: check SyntaxWarning treated as an SyntaxError
291334
with warnings.catch_warnings(), self.assertRaises(SyntaxError):
292335
warnings.simplefilter('error', SyntaxWarning)
293-
compile_command('1 is 1', symbol='exec')
336+
compiler('1 is 1', "<input>", 'exec')
294337

295338
# Check SyntaxWarning treated as an SyntaxError
296339
with warnings.catch_warnings(), self.assertRaises(SyntaxError):
297340
warnings.simplefilter('error', SyntaxWarning)
298-
compile_command(r"'\e'", symbol='exec')
341+
compiler(r"'\e'", "<input>", 'exec')
299342

300-
def test_incomplete_warning(self):
343+
@subTests('compiler', WRAPPING_COMPILERS)
344+
def test_incomplete_warning(self, compiler):
301345
with warnings.catch_warnings(record=True) as w:
302346
warnings.simplefilter('always')
303-
self.assertIncomplete("'\\e' + (")
347+
compiler("'\\e' + (")
304348
self.assertEqual(w, [])
305349

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):
307361
with warnings.catch_warnings(record=True) as w:
308362
warnings.simplefilter('always')
309-
self.assertInvalid("'\\e' 1")
363+
self.assertInvalid("'\\e' 1", compiler=compiler)
310364
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("""\
323373
def foo(x,x):
324374
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
327387

328388

329389
if __name__ == "__main__":

0 commit comments

Comments
 (0)