From 24d9f764664ec99ceb599ec2b291d1f6427510e0 Mon Sep 17 00:00:00 2001 From: Jovan Vjestica Date: Sun, 19 Jul 2026 12:36:15 +0200 Subject: [PATCH 1/2] Improved test coverage for codeop.py. * 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 --- Lib/test/test_codeop.py | 148 ++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 44 deletions(-) diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index ed10bd3dcb6d2b9..a25c5f9c46ef006 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -7,39 +7,63 @@ from test.support import warnings_helper from textwrap import dedent -from codeop import compile_command, PyCF_DONT_IMPLY_DEDENT +from codeop import compile_command, CommandCompiler, Compile +from codeop import PyCF_DONT_IMPLY_DEDENT, PyCF_ONLY_AST +import ast + class CodeopTests(unittest.TestCase): + def setUp(self): + self.wrapping_compilers = [compile_command, CommandCompiler()] + self.raw_compilers = [Compile()] + self.compilers = self.wrapping_compilers + self.raw_compilers + def assertValid(self, str, symbol='single'): '''succeed iff str is a valid piece of code''' - expected = compile(str, "", symbol, PyCF_DONT_IMPLY_DEDENT) - self.assertEqual(compile_command(str, "", symbol), expected) + for compiler in self.compilers: + expected = compile(str, "", symbol, PyCF_DONT_IMPLY_DEDENT) + self.assertEqual(compiler(str, "", symbol), expected) def assertIncomplete(self, str, symbol='single'): '''succeed iff str is the start of a valid piece of code''' - self.assertEqual(compile_command(str, symbol=symbol), None) + for compiler in self.wrapping_compilers: + self.assertEqual(compiler(str, "", symbol=symbol), None) + + for compiler in self.raw_compilers: + # Compile has should raise like built-in compile + with self.assertRaises(SyntaxError) as cm_original_error: + compile(str, "", symbol, compiler.flags) + expected_error = cm_original_error.exception + with self.assertRaises(type(expected_error)) as cm_wrapped_error: + compiler(str, "", symbol=symbol) + self.assertEqual( + expected_error.args, + cm_wrapped_error.exception.args + ) def assertInvalid(self, str, symbol='single', is_syntax=1): '''succeed iff str is the start of an invalid piece of code''' - try: - compile_command(str,symbol=symbol) - self.fail("No exception raised for invalid code") - except SyntaxError: - self.assertTrue(is_syntax) - except OverflowError: - self.assertTrue(not is_syntax) + for compiler in self.compilers: + try: + compiler(str,"", symbol=symbol) + self.fail("No exception raised for invalid code") + except SyntaxError: + self.assertTrue(is_syntax) + except OverflowError: + self.assertTrue(not is_syntax) def test_valid(self): av = self.assertValid # special case - self.assertEqual(compile_command(""), - compile("pass", "", 'single', - PyCF_DONT_IMPLY_DEDENT)) - self.assertEqual(compile_command("\n"), - compile("pass", "", 'single', - PyCF_DONT_IMPLY_DEDENT)) + for compiler in self.wrapping_compilers: + self.assertEqual( + compiler("", "", 'single'), + compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) + self.assertEqual( + compiler("\n", "", 'single'), + compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) av("a = 1") av("\na = 1") @@ -273,49 +297,80 @@ def test_invalid_exec(self): ai('a await raise b?+1', symbol='exec') def test_filename(self): - self.assertEqual(compile_command("a = 1\n", "abc").co_filename, - compile("a = 1\n", "abc", 'single').co_filename) - self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename, - compile("a = 1\n", "def", 'single').co_filename) + for compiler in self.compilers: + self.assertEqual( + compiler("a = 1\n", "abc", "single").co_filename, + compile("a = 1\n", "abc", 'single').co_filename + ) + self.assertNotEqual( + compiler("a = 1\n", "abc", "single").co_filename, + compile("a = 1\n", "def", 'single').co_filename + ) + + def assertReturnsModule(self, code, compiler): + retval = compiler(code, "", 'exec', PyCF_ONLY_AST) + self.assertIsInstance(retval, ast.Module) + + def test_ast_return_value(self): + for compiler in self.raw_compilers: + validate_ast = self.assertReturnsModule + validate_ast("x = 5", compiler) + validate_ast("\nx = 5", compiler) + validate_ast("x = 5\n", compiler) + validate_ast("x = 5\n\n", compiler) + validate_ast("\n\nx = 5\n\n", compiler) def test_warning(self): # Test that the warning is only returned once. - with warnings_helper.check_warnings( - ('"is" with \'str\' literal', SyntaxWarning), - ('"\\\\e" is an invalid escape sequence', SyntaxWarning), - ) as w: - compile_command(r"'\e' is 0") + for compiler in self.compilers: + with warnings_helper.check_warnings( + ('"is" with \'str\' literal', SyntaxWarning), + ('"\\\\e" is an invalid escape sequence', SyntaxWarning), + ) as w: + compiler(r"'\e' is 0", "", "single") self.assertEqual(len(w.warnings), 2) - # bpo-41520: check SyntaxWarning treated as an SyntaxError - with warnings.catch_warnings(), self.assertRaises(SyntaxError): - warnings.simplefilter('error', SyntaxWarning) - compile_command('1 is 1', symbol='exec') + # bpo-41520: check SyntaxWarning treated as an SyntaxError + with warnings.catch_warnings(), self.assertRaises(SyntaxError): + warnings.simplefilter('error', SyntaxWarning) + compiler('1 is 1', "", 'exec') - # Check SyntaxWarning treated as an SyntaxError - with warnings.catch_warnings(), self.assertRaises(SyntaxError): - warnings.simplefilter('error', SyntaxWarning) - compile_command(r"'\e'", symbol='exec') + # Check SyntaxWarning treated as an SyntaxError + with warnings.catch_warnings(), self.assertRaises(SyntaxError): + warnings.simplefilter('error', SyntaxWarning) + compiler(r"'\e'", "", 'exec') def test_incomplete_warning(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - self.assertIncomplete("'\\e' + (") - self.assertEqual(w, []) + code = "'\\e' + (" + for compiler in self.wrapping_compilers: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + compiler(code) + self.assertEqual(w, []) + for compiler in self.raw_compilers: + warnings_cm = warnings_helper.check_warnings( + ('"\\\\e" is an invalid esceape sequence', SyntaxWarning) + ) + with self.assertRaises(SyntaxError), warnings_cm as w: + compiler = self.compilers[-1] + compiler(code, "", 'single') + self.assertEqual(len(w.warnings), 1) def test_invalid_warning(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') self.assertInvalid("'\\e' 1") - self.assertEqual(len(w), 1) - self.assertEqual(w[0].category, SyntaxWarning) - self.assertRegex(str(w[0].message), 'invalid escape sequence') - self.assertEqual(w[0].filename, '') + self.assertEqual(len(w), len(self.compilers)) + for warning in w: + self.assertEqual(warning.category, SyntaxWarning) + self.assertRegex(str(warning), 'invalid escape sequence') + self.assertEqual(warning.filename, '') def assertSyntaxErrorMatches(self, code, message): with self.subTest(code): - with self.assertRaisesRegex(SyntaxError, message): - compile_command(code, symbol='exec') + for compiler in self.compilers: + with self.assertRaisesRegex(SyntaxError, message): + compiler(code, "", 'exec') def test_syntax_errors(self): self.assertSyntaxErrorMatches( @@ -324,6 +379,11 @@ def foo(x,x): pass """), "duplicate parameter 'x' in function definition") + def test_future_imports(self): + for compiler in self.raw_compilers: + original_flags = compiler.flags + compiler('from __future__ import annotations', "", 'single') + self.assertGreater(compiler.flags, original_flags) if __name__ == "__main__": From 4fe7c3678272067e505224565474300116ca6aae Mon Sep 17 00:00:00 2001 From: Jovan Vjestica Date: Mon, 27 Jul 2026 19:27:01 +0200 Subject: [PATCH 2/2] Use subTests for easier test investigation. * Moved test set up to module scope * Use subTests decorator to inject compiler parameter into tests * Introduced new compiler parameter to assert* family of methods * Used functools to maintain the same API for the assert* methods where applicable --- Lib/test/test_codeop.py | 230 ++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index a25c5f9c46ef006..d57452602ce5574 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -4,33 +4,31 @@ """ import unittest import warnings -from test.support import warnings_helper +from test.support import subTests, warnings_helper from textwrap import dedent +import functools from codeop import compile_command, CommandCompiler, Compile from codeop import PyCF_DONT_IMPLY_DEDENT, PyCF_ONLY_AST import ast -class CodeopTests(unittest.TestCase): +WRAPPING_COMPILERS = [compile_command, CommandCompiler()] +RAW_COMPILERS = [Compile()] +COMPILERS = WRAPPING_COMPILERS + RAW_COMPILERS - def setUp(self): - self.wrapping_compilers = [compile_command, CommandCompiler()] - self.raw_compilers = [Compile()] - self.compilers = self.wrapping_compilers + self.raw_compilers - def assertValid(self, str, symbol='single'): +class CodeopTests(unittest.TestCase): + def assertValid(self, str, symbol='single', *, compiler): '''succeed iff str is a valid piece of code''' - for compiler in self.compilers: - expected = compile(str, "", symbol, PyCF_DONT_IMPLY_DEDENT) - self.assertEqual(compiler(str, "", symbol), expected) + expected = compile(str, "", symbol, PyCF_DONT_IMPLY_DEDENT) + self.assertEqual(compiler(str, "", symbol), expected) - def assertIncomplete(self, str, symbol='single'): + def assertIncomplete(self, str, symbol='single', *, compiler): '''succeed iff str is the start of a valid piece of code''' - for compiler in self.wrapping_compilers: + if compiler in WRAPPING_COMPILERS: self.assertEqual(compiler(str, "", symbol=symbol), None) - - for compiler in self.raw_compilers: + else: # Compile has should raise like built-in compile with self.assertRaises(SyntaxError) as cm_original_error: compile(str, "", symbol, compiler.flags) @@ -42,29 +40,28 @@ def assertIncomplete(self, str, symbol='single'): cm_wrapped_error.exception.args ) - def assertInvalid(self, str, symbol='single', is_syntax=1): + def assertInvalid(self, str, symbol='single', is_syntax=1, *, compiler): '''succeed iff str is the start of an invalid piece of code''' - for compiler in self.compilers: - try: - compiler(str,"", symbol=symbol) - self.fail("No exception raised for invalid code") - except SyntaxError: - self.assertTrue(is_syntax) - except OverflowError: - self.assertTrue(not is_syntax) - - def test_valid(self): - av = self.assertValid - - # special case - for compiler in self.wrapping_compilers: - self.assertEqual( - compiler("", "", 'single'), - compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) - self.assertEqual( - compiler("\n", "", 'single'), - compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) - + try: + compiler(str,"", symbol=symbol) + self.fail("No exception raised for invalid code") + except SyntaxError: + self.assertTrue(is_syntax) + except OverflowError: + self.assertTrue(not is_syntax) + + @subTests('compiler', WRAPPING_COMPILERS) + def test_empty(self, compiler): + self.assertEqual( + compiler("", "", 'single'), + compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) + self.assertEqual( + compiler("\n", "", 'single'), + compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) + + @subTests('compiler', COMPILERS) + def test_valid(self, compiler): + av = functools.partial(self.assertValid, compiler=compiler) av("a = 1") av("\na = 1") av("a = 1\n") @@ -116,8 +113,9 @@ def test_valid(self): av("def f():\n pass\n#foo\n") av("@a.b.c\ndef f():\n pass\n") - def test_incomplete(self): - ai = self.assertIncomplete + @subTests('compiler', COMPILERS) + def test_incomplete(self, compiler): + ai = functools.partial(self.assertIncomplete, compiler=compiler) ai("(a **") ai("(a,b,") @@ -250,8 +248,9 @@ def test_incomplete(self): ai('a = f"""') ai('a = \\') - def test_invalid(self): - ai = self.assertInvalid + @subTests('compiler', COMPILERS) + def test_invalid(self, compiler): + ai = functools.partial(self.assertInvalid, compiler=compiler) ai("a b") ai("a @") @@ -287,8 +286,9 @@ def test_invalid(self): ai("[i for i in range(10)] = (1, 2, 3)") - def test_invalid_exec(self): - ai = self.assertInvalid + @subTests('compiler', COMPILERS) + def test_invalid_exec(self, compiler): + ai = functools.partial(self.assertInvalid, compiler=compiler) ai("raise = 4", symbol="exec") ai('def a-b', symbol='exec') ai('await?', symbol='exec') @@ -296,94 +296,94 @@ def test_invalid_exec(self): ai('a await raise b', symbol='exec') ai('a await raise b?+1', symbol='exec') - def test_filename(self): - for compiler in self.compilers: - self.assertEqual( - compiler("a = 1\n", "abc", "single").co_filename, - compile("a = 1\n", "abc", 'single').co_filename - ) - self.assertNotEqual( - compiler("a = 1\n", "abc", "single").co_filename, - compile("a = 1\n", "def", 'single').co_filename - ) + @subTests('compiler', COMPILERS) + def test_filename(self, compiler): + self.assertEqual( + compiler("a = 1\n", "abc", "single").co_filename, + compile("a = 1\n", "abc", 'single').co_filename + ) + self.assertNotEqual( + compiler("a = 1\n", "abc", "single").co_filename, + compile("a = 1\n", "def", 'single').co_filename + ) def assertReturnsModule(self, code, compiler): retval = compiler(code, "", 'exec', PyCF_ONLY_AST) self.assertIsInstance(retval, ast.Module) - def test_ast_return_value(self): - for compiler in self.raw_compilers: - validate_ast = self.assertReturnsModule - validate_ast("x = 5", compiler) - validate_ast("\nx = 5", compiler) - validate_ast("x = 5\n", compiler) - validate_ast("x = 5\n\n", compiler) - validate_ast("\n\nx = 5\n\n", compiler) - - def test_warning(self): + @subTests('compiler', RAW_COMPILERS) + def test_ast_return_value(self, compiler): + validate_ast = self.assertReturnsModule + validate_ast("x = 5", compiler) + validate_ast("\nx = 5", compiler) + validate_ast("x = 5\n", compiler) + validate_ast("x = 5\n\n", compiler) + validate_ast("\n\nx = 5\n\n", compiler) + + @subTests('compiler', COMPILERS) + def test_warning(self, compiler): # Test that the warning is only returned once. - for compiler in self.compilers: - with warnings_helper.check_warnings( - ('"is" with \'str\' literal', SyntaxWarning), - ('"\\\\e" is an invalid escape sequence', SyntaxWarning), - ) as w: - compiler(r"'\e' is 0", "", "single") - self.assertEqual(len(w.warnings), 2) - - # bpo-41520: check SyntaxWarning treated as an SyntaxError - with warnings.catch_warnings(), self.assertRaises(SyntaxError): - warnings.simplefilter('error', SyntaxWarning) - compiler('1 is 1', "", 'exec') - - # Check SyntaxWarning treated as an SyntaxError - with warnings.catch_warnings(), self.assertRaises(SyntaxError): - warnings.simplefilter('error', SyntaxWarning) - compiler(r"'\e'", "", 'exec') - - def test_incomplete_warning(self): - code = "'\\e' + (" - for compiler in self.wrapping_compilers: - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - compiler(code) - self.assertEqual(w, []) - for compiler in self.raw_compilers: - warnings_cm = warnings_helper.check_warnings( - ('"\\\\e" is an invalid esceape sequence', SyntaxWarning) - ) - with self.assertRaises(SyntaxError), warnings_cm as w: - compiler = self.compilers[-1] - compiler(code, "", 'single') - self.assertEqual(len(w.warnings), 1) - - def test_invalid_warning(self): + with warnings_helper.check_warnings( + ('"is" with \'str\' literal', SyntaxWarning), + ('"\\\\e" is an invalid escape sequence', SyntaxWarning), + ) as w: + compiler(r"'\e' is 0", "", "single") + self.assertEqual(len(w.warnings), 2) + + # bpo-41520: check SyntaxWarning treated as an SyntaxError + with warnings.catch_warnings(), self.assertRaises(SyntaxError): + warnings.simplefilter('error', SyntaxWarning) + compiler('1 is 1', "", 'exec') + + # Check SyntaxWarning treated as an SyntaxError + with warnings.catch_warnings(), self.assertRaises(SyntaxError): + warnings.simplefilter('error', SyntaxWarning) + compiler(r"'\e'", "", 'exec') + + @subTests('compiler', WRAPPING_COMPILERS) + def test_incomplete_warning(self, compiler): with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - self.assertInvalid("'\\e' 1") - self.assertEqual(len(w), len(self.compilers)) + compiler("'\\e' + (") + self.assertEqual(w, []) + + @subTests('compiler', RAW_COMPILERS) + def test_raw_raises_error(self, compiler): + warnings_cm = warnings_helper.check_warnings( + ('"\\\\e" is an invalid esceape sequence', SyntaxWarning) + ) + with self.assertRaises(SyntaxError), warnings_cm as w: + compiler("'\\e' + (", "", 'single') + self.assertEqual(len(w.warnings), 1) + + @subTests('compiler', COMPILERS) + def test_invalid_warning(self, compiler): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.assertInvalid("'\\e' 1", compiler=compiler) + self.assertEqual(len(w), 1) for warning in w: self.assertEqual(warning.category, SyntaxWarning) self.assertRegex(str(warning), 'invalid escape sequence') self.assertEqual(warning.filename, '') - def assertSyntaxErrorMatches(self, code, message): - with self.subTest(code): - for compiler in self.compilers: - with self.assertRaisesRegex(SyntaxError, message): - compiler(code, "", 'exec') - - def test_syntax_errors(self): - self.assertSyntaxErrorMatches( - dedent("""\ + @subTests('compiler', COMPILERS) + def test_syntax_errors(self, compiler): + code = dedent("""\ def foo(x,x): pass - """), "duplicate parameter 'x' in function definition") - - def test_future_imports(self): - for compiler in self.raw_compilers: - original_flags = compiler.flags - compiler('from __future__ import annotations', "", 'single') - self.assertGreater(compiler.flags, original_flags) + """) + message = "duplicate parameter 'x' in function definition" + with self.assertRaisesRegex(SyntaxError, message): + compiler(code, "", 'exec') + + @subTests('compiler', RAW_COMPILERS) + def test_future_imports(self, compiler): + original_flags = compiler.flags + compiler('from __future__ import annotations', "", 'single') + self.assertGreater(compiler.flags, original_flags) + # reset flags to ensure test has no side-effects + compiler.flags = original_flags if __name__ == "__main__":