diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 7b434c7f27..40313cb399 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -3,13 +3,16 @@ import os import unittest import sys +import ast import _ast import tempfile import types +import textwrap from test import support -from test.support import script_helper +from test.support import script_helper, requires_debug_ranges from test.support.os_helper import FakePath + class TestSpecifics(unittest.TestCase): def compile_single(self, source): @@ -107,7 +110,9 @@ def __getitem__(self, key): @unittest.skip("TODO: RUSTPYTHON; segmentation fault") def test_extended_arg(self): - longexpr = 'x = x or ' + '-x' * 2500 + # default: 1000 * 2.5 = 2500 repetitions + repeat = int(sys.getrecursionlimit() * 2.5) + longexpr = 'x = x or ' + '-x' * repeat g = {} code = ''' def f(x): @@ -158,14 +163,16 @@ def test_indentation(self): def test_leading_newlines(self): s256 = "".join(["\n"] * 256 + ["spam"]) co = compile(s256, 'fn', 'exec') - self.assertEqual(co.co_firstlineno, 257) - self.assertEqual(co.co_lnotab, bytes()) + self.assertEqual(co.co_firstlineno, 1) + lines = list(co.co_lines()) + self.assertEqual(lines[0][2], 0) + self.assertEqual(lines[1][2], 257) def test_literals_with_leading_zeroes(self): for arg in ["077787", "0xj", "0x.", "0e", "090000000000000", "080000000000000", "000000000000009", "000000000000008", "0b42", "0BADCAFE", "0o123456789", "0b1.1", "0o4.2", - "0b101j2", "0o153j2", "0b100e1", "0o777e1", "0777", + "0b101j", "0o153j", "0b100e1", "0o777e1", "0777", "000777", "000000000000007"]: self.assertRaises(SyntaxError, eval, arg) @@ -194,6 +201,19 @@ def test_literals_with_leading_zeroes(self): self.assertEqual(eval("0o777"), 511) self.assertEqual(eval("-0o0000010"), -8) + def test_int_literals_too_long(self): + n = 3000 + source = f"a = 1\nb = 2\nc = {'3'*n}\nd = 4" + with support.adjust_int_max_str_digits(n): + compile(source, "", "exec") # no errors. + with support.adjust_int_max_str_digits(n-1): + with self.assertRaises(SyntaxError) as err_ctx: + compile(source, "", "exec") + exc = err_ctx.exception + self.assertEqual(exc.lineno, 3) + self.assertIn('Exceeds the limit ', str(exc)) + self.assertIn(' Consider hexadecimal ', str(exc)) + def test_unary_minus(self): # Verify treatment of unary minus on negative numbers SF bug #660455 if sys.maxsize == 2147483647: @@ -434,7 +454,7 @@ def test_compile_ast(self): fname = __file__ if fname.lower().endswith('pyc'): fname = fname[:-1] - with open(fname, 'r') as f: + with open(fname, encoding='utf-8') as f: fcontents = f.read() sample_code = [ ['', 'x = 5'], @@ -511,6 +531,7 @@ def test_single_statement(self): self.compile_single("if x:\n f(x)") self.compile_single("if x:\n f(x)\nelse:\n g(x)") self.compile_single("class T:\n pass") + self.compile_single("c = '''\na=1\nb=2\nc=3\n'''") # TODO: RUSTPYTHON @unittest.expectedFailure @@ -523,6 +544,7 @@ def test_bad_single_statement(self): self.assertInvalidSingle('f()\n# blah\nblah()') self.assertInvalidSingle('f()\nxy # blah\nblah()') self.assertInvalidSingle('x = 5 # comment\nx = 6\n') + self.assertInvalidSingle("c = '''\nd=1\n'''\na = 1\n\nb = 2\n") # TODO: RUSTPYTHON @unittest.expectedFailure @@ -558,21 +580,26 @@ def test_compiler_recursion_limit(self): # XXX (ncoghlan): duplicating the scaling factor here is a little # ugly. Perhaps it should be exposed somewhere... fail_depth = sys.getrecursionlimit() * 3 + crash_depth = sys.getrecursionlimit() * 300 success_depth = int(fail_depth * 0.75) - def check_limit(prefix, repeated): + def check_limit(prefix, repeated, mode="single"): expect_ok = prefix + repeated * success_depth - self.compile_single(expect_ok) - broken = prefix + repeated * fail_depth - details = "Compiling ({!r} + {!r} * {})".format( - prefix, repeated, fail_depth) - with self.assertRaises(RecursionError, msg=details): - self.compile_single(broken) + compile(expect_ok, '', mode) + for depth in (fail_depth, crash_depth): + broken = prefix + repeated * depth + details = "Compiling ({!r} + {!r} * {})".format( + prefix, repeated, depth) + with self.assertRaises(RecursionError, msg=details): + compile(broken, '', mode) check_limit("a", "()") check_limit("a", ".b") check_limit("a", "[0]") check_limit("a", "*a") + # XXX Crashes in the parser. + # check_limit("a", " if a else a") + # check_limit("if a: pass", "\nelif a: pass", mode="exec") # TODO: RUSTPYTHON @unittest.expectedFailure @@ -617,7 +644,7 @@ def check_same_constant(const): exec(code, ns) f1 = ns['f1'] f2 = ns['f2'] - self.assertIs(f1.__code__, f2.__code__) + self.assertIs(f1.__code__.co_consts, f2.__code__.co_consts) self.check_constant(f1, const) self.assertEqual(repr(f1()), repr(const)) @@ -630,7 +657,7 @@ def check_same_constant(const): # Note: "lambda: ..." emits "LOAD_CONST Ellipsis", # whereas "lambda: Ellipsis" emits "LOAD_GLOBAL Ellipsis" f1, f2 = lambda: ..., lambda: ... - self.assertIs(f1.__code__, f2.__code__) + self.assertIs(f1.__code__.co_consts, f2.__code__.co_consts) self.check_constant(f1, Ellipsis) self.assertEqual(repr(f1()), repr(Ellipsis)) @@ -645,10 +672,31 @@ def check_same_constant(const): # {0} is converted to a constant frozenset({0}) by the peephole # optimizer f1, f2 = lambda x: x in {0}, lambda x: x in {0} - self.assertIs(f1.__code__, f2.__code__) + self.assertIs(f1.__code__.co_consts, f2.__code__.co_consts) self.check_constant(f1, frozenset({0})) self.assertTrue(f1(0)) + # Merging equal co_linetable is not a strict requirement + # for the Python semantics, it's a more an implementation detail. + @support.cpython_only + def test_merge_code_attrs(self): + # See https://bugs.python.org/issue42217 + f1 = lambda x: x.y.z + f2 = lambda a: a.b.c + + self.assertIs(f1.__code__.co_linetable, f2.__code__.co_linetable) + + # Stripping unused constants is not a strict requirement for the + # Python semantics, it's a more an implementation detail. + @support.cpython_only + def test_strip_unused_consts(self): + # Python 3.10rc1 appended None to co_consts when None is not used + # at all. See bpo-45056. + def f1(): + "docstring" + return 42 + self.assertEqual(f1.__code__.co_consts, ("docstring", 42)) + # This is a regression test for a CPython specific peephole optimizer # implementation bug present in a few releases. It's assertion verifies # that peephole optimization was actually done though that isn't an @@ -749,10 +797,10 @@ def unused_block_while_else(): for func in funcs: opcodes = list(dis.get_instructions(func)) - self.assertEqual(2, len(opcodes)) - self.assertEqual('LOAD_CONST', opcodes[0].opname) - self.assertEqual(None, opcodes[0].argval) - self.assertEqual('RETURN_VALUE', opcodes[1].opname) + self.assertLessEqual(len(opcodes), 4) + self.assertEqual('LOAD_CONST', opcodes[-2].opname) + self.assertEqual(None, opcodes[-2].argval) + self.assertEqual('RETURN_VALUE', opcodes[-1].opname) # TODO: RUSTPYTHON @unittest.expectedFailure @@ -770,10 +818,536 @@ def continue_in_while(): # Check that we did not raise but we also don't generate bytecode for func in funcs: opcodes = list(dis.get_instructions(func)) - self.assertEqual(2, len(opcodes)) - self.assertEqual('LOAD_CONST', opcodes[0].opname) - self.assertEqual(None, opcodes[0].argval) - self.assertEqual('RETURN_VALUE', opcodes[1].opname) + self.assertEqual(3, len(opcodes)) + self.assertEqual('LOAD_CONST', opcodes[1].opname) + self.assertEqual(None, opcodes[1].argval) + self.assertEqual('RETURN_VALUE', opcodes[2].opname) + + def test_consts_in_conditionals(self): + def and_true(x): + return True and x + + def and_false(x): + return False and x + + def or_true(x): + return True or x + + def or_false(x): + return False or x + + funcs = [and_true, and_false, or_true, or_false] + + # Check that condition is removed. + for func in funcs: + with self.subTest(func=func): + opcodes = list(dis.get_instructions(func)) + self.assertLessEqual(len(opcodes), 3) + self.assertIn('LOAD_', opcodes[-2].opname) + self.assertEqual('RETURN_VALUE', opcodes[-1].opname) + + def test_imported_load_method(self): + sources = [ + """\ + import os + def foo(): + return os.uname() + """, + """\ + import os as operating_system + def foo(): + return operating_system.uname() + """, + """\ + from os import path + def foo(x): + return path.join(x) + """, + """\ + from os import path as os_path + def foo(x): + return os_path.join(x) + """ + ] + for source in sources: + namespace = {} + exec(textwrap.dedent(source), namespace) + func = namespace['foo'] + with self.subTest(func=func.__name__): + opcodes = list(dis.get_instructions(func)) + instructions = [opcode.opname for opcode in opcodes] + self.assertNotIn('LOAD_METHOD', instructions) + self.assertIn('LOAD_ATTR', instructions) + self.assertIn('PRECALL', instructions) + + def test_lineno_procedure_call(self): + def call(): + ( + print() + ) + line1 = call.__code__.co_firstlineno + 1 + assert line1 not in [line for (_, _, line) in call.__code__.co_lines()] + + def test_lineno_after_implicit_return(self): + TRUE = True + # Don't use constant True or False, as compiler will remove test + def if1(x): + x() + if TRUE: + pass + def if2(x): + x() + if TRUE: + pass + else: + pass + def if3(x): + x() + if TRUE: + pass + else: + return None + def if4(x): + x() + if not TRUE: + pass + funcs = [ if1, if2, if3, if4] + lastlines = [ 3, 3, 3, 2] + frame = None + def save_caller_frame(): + nonlocal frame + frame = sys._getframe(1) + for func, lastline in zip(funcs, lastlines, strict=True): + with self.subTest(func=func): + func(save_caller_frame) + self.assertEqual(frame.f_lineno-frame.f_code.co_firstlineno, lastline) + + def test_lineno_after_no_code(self): + def no_code1(): + "doc string" + + def no_code2(): + a: int + + for func in (no_code1, no_code2): + with self.subTest(func=func): + code = func.__code__ + lines = list(code.co_lines()) + start, end, line = lines[0] + self.assertEqual(start, 0) + self.assertEqual(line, code.co_firstlineno) + + def get_code_lines(self, code): + last_line = -2 + res = [] + for _, _, line in code.co_lines(): + if line is not None and line != last_line: + res.append(line - code.co_firstlineno) + last_line = line + return res + + def test_lineno_attribute(self): + def load_attr(): + return ( + o. + a + ) + load_attr_lines = [ 0, 2, 3, 1 ] + + def load_method(): + return ( + o. + m( + 0 + ) + ) + load_method_lines = [ 0, 2, 3, 4, 3, 1 ] + + def store_attr(): + ( + o. + a + ) = ( + v + ) + store_attr_lines = [ 0, 5, 2, 3 ] + + def aug_store_attr(): + ( + o. + a + ) += ( + v + ) + aug_store_attr_lines = [ 0, 2, 3, 5, 1, 3 ] + + funcs = [ load_attr, load_method, store_attr, aug_store_attr] + func_lines = [ load_attr_lines, load_method_lines, + store_attr_lines, aug_store_attr_lines] + + for func, lines in zip(funcs, func_lines, strict=True): + with self.subTest(func=func): + code_lines = self.get_code_lines(func.__code__) + self.assertEqual(lines, code_lines) + + def test_line_number_genexp(self): + + def return_genexp(): + return (1 + for + x + in + y) + genexp_lines = [0, 2, 0] + + genexp_code = return_genexp.__code__.co_consts[1] + code_lines = self.get_code_lines(genexp_code) + self.assertEqual(genexp_lines, code_lines) + + def test_line_number_implicit_return_after_async_for(self): + + async def test(aseq): + async for i in aseq: + body + + expected_lines = [0, 1, 2, 1] + code_lines = self.get_code_lines(test.__code__) + self.assertEqual(expected_lines, code_lines) + + def test_big_dict_literal(self): + # The compiler has a flushing point in "compiler_dict" that calls compiles + # a portion of the dictionary literal when the loop that iterates over the items + # reaches 0xFFFF elements but the code was not including the boundary element, + # dropping the key at position 0xFFFF. See bpo-41531 for more information + + dict_size = 0xFFFF + 1 + the_dict = "{" + ",".join(f"{x}:{x}" for x in range(dict_size)) + "}" + self.assertEqual(len(eval(the_dict)), dict_size) + + def test_redundant_jump_in_if_else_break(self): + # Check if bytecode containing jumps that simply point to the next line + # is generated around if-else-break style structures. See bpo-42615. + + def if_else_break(): + val = 1 + while True: + if val > 0: + val -= 1 + else: + break + val = -1 + + INSTR_SIZE = 2 + HANDLED_JUMPS = ( + 'POP_JUMP_IF_FALSE', + 'POP_JUMP_IF_TRUE', + 'JUMP_ABSOLUTE', + 'JUMP_FORWARD', + ) + + for line, instr in enumerate( + dis.Bytecode(if_else_break, show_caches=True) + ): + if instr.opname == 'JUMP_FORWARD': + self.assertNotEqual(instr.arg, 0) + elif instr.opname in HANDLED_JUMPS: + self.assertNotEqual(instr.arg, (line + 1)*INSTR_SIZE) + + def test_no_wraparound_jump(self): + # See https://bugs.python.org/issue46724 + + def while_not_chained(a, b, c): + while not (a < b < c): + pass + + for instr in dis.Bytecode(while_not_chained): + self.assertNotEqual(instr.opname, "EXTENDED_ARG") + + def test_compare_positions(self): + for opname, op in [ + ("COMPARE_OP", "<"), + ("COMPARE_OP", "<="), + ("COMPARE_OP", ">"), + ("COMPARE_OP", ">="), + ("CONTAINS_OP", "in"), + ("CONTAINS_OP", "not in"), + ("IS_OP", "is"), + ("IS_OP", "is not"), + ]: + expr = f'a {op} b {op} c' + expected_positions = 2 * [(2, 2, 0, len(expr))] + for source in [ + f"\\\n{expr}", f'if \\\n{expr}: x', f"x if \\\n{expr} else y" + ]: + code = compile(source, "", "exec") + actual_positions = [ + instruction.positions + for instruction in dis.get_instructions(code) + if instruction.opname == opname + ] + with self.subTest(source): + self.assertEqual(actual_positions, expected_positions) + + +@requires_debug_ranges() +class TestSourcePositions(unittest.TestCase): + # Ensure that compiled code snippets have correct line and column numbers + # in `co_positions()`. + + def check_positions_against_ast(self, snippet): + # Basic check that makes sure each line and column is at least present + # in one of the AST nodes of the source code. + code = compile(snippet, 'test_compile.py', 'exec') + ast_tree = compile(snippet, 'test_compile.py', 'exec', _ast.PyCF_ONLY_AST) + self.assertTrue(type(ast_tree) == _ast.Module) + + # Use an AST visitor that notes all the offsets. + lines, end_lines, columns, end_columns = set(), set(), set(), set() + class SourceOffsetVisitor(ast.NodeVisitor): + def generic_visit(self, node): + super().generic_visit(node) + if not isinstance(node, ast.expr) and not isinstance(node, ast.stmt): + return + lines.add(node.lineno) + end_lines.add(node.end_lineno) + columns.add(node.col_offset) + end_columns.add(node.end_col_offset) + + SourceOffsetVisitor().visit(ast_tree) + + # Check against the positions in the code object. + for (line, end_line, col, end_col) in code.co_positions(): + if line == 0: + continue # This is an artificial module-start line + # If the offset is not None (indicating missing data), ensure that + # it was part of one of the AST nodes. + if line is not None: + self.assertIn(line, lines) + if end_line is not None: + self.assertIn(end_line, end_lines) + if col is not None: + self.assertIn(col, columns) + if end_col is not None: + self.assertIn(end_col, end_columns) + + return code, ast_tree + + def assertOpcodeSourcePositionIs(self, code, opcode, + line, end_line, column, end_column, occurrence=1): + + for instr, position in zip( + dis.Bytecode(code, show_caches=True), code.co_positions(), strict=True + ): + if instr.opname == opcode: + occurrence -= 1 + if not occurrence: + self.assertEqual(position[0], line) + self.assertEqual(position[1], end_line) + self.assertEqual(position[2], column) + self.assertEqual(position[3], end_column) + return + + self.fail(f"Opcode {opcode} not found in code") + + def test_simple_assignment(self): + snippet = "x = 1" + self.check_positions_against_ast(snippet) + + def test_compiles_to_extended_op_arg(self): + # Make sure we still have valid positions when the code compiles to an + # EXTENDED_ARG by performing a loop which needs a JUMP_ABSOLUTE after + # a bunch of opcodes. + snippet = "x = x\n" * 10_000 + snippet += ("while x != 0:\n" + " x -= 1\n" + "while x != 0:\n" + " x += 1\n" + ) + + compiled_code, _ = self.check_positions_against_ast(snippet) + + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_OP', + line=10_000 + 2, end_line=10_000 + 2, + column=2, end_column=8, occurrence=1) + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_OP', + line=10_000 + 4, end_line=10_000 + 4, + column=2, end_column=9, occurrence=2) + + def test_multiline_expression(self): + snippet = """\ +f( + 1, 2, 3, 4 +) +""" + compiled_code, _ = self.check_positions_against_ast(snippet) + self.assertOpcodeSourcePositionIs(compiled_code, 'CALL', + line=1, end_line=3, column=0, end_column=1) + + def test_very_long_line_end_offset(self): + # Make sure we get the correct column offset for offsets + # too large to store in a byte. + long_string = "a" * 1000 + snippet = f"g('{long_string}')" + + compiled_code, _ = self.check_positions_against_ast(snippet) + self.assertOpcodeSourcePositionIs(compiled_code, 'CALL', + line=1, end_line=1, column=0, end_column=1005) + + def test_complex_single_line_expression(self): + snippet = "a - b @ (c * x['key'] + 23)" + + compiled_code, _ = self.check_positions_against_ast(snippet) + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_SUBSCR', + line=1, end_line=1, column=13, end_column=21) + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_OP', + line=1, end_line=1, column=9, end_column=21, occurrence=1) + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_OP', + line=1, end_line=1, column=9, end_column=26, occurrence=2) + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_OP', + line=1, end_line=1, column=4, end_column=27, occurrence=3) + self.assertOpcodeSourcePositionIs(compiled_code, 'BINARY_OP', + line=1, end_line=1, column=0, end_column=27, occurrence=4) + + def test_multiline_assert_rewritten_as_method_call(self): + # GH-94694: Don't crash if pytest rewrites a multiline assert as a + # method call with the same location information: + tree = ast.parse("assert (\n42\n)") + old_node = tree.body[0] + new_node = ast.Expr( + ast.Call( + ast.Attribute( + ast.Name("spam", ast.Load()), + "eggs", + ast.Load(), + ), + [], + [], + ) + ) + ast.copy_location(new_node, old_node) + ast.fix_missing_locations(new_node) + tree.body[0] = new_node + compile(tree, "", "exec") + + def test_push_null_load_global_positions(self): + source_template = """ + import abc, dis + import ast as art + + abc = None + dix = dis + ast = art + + def f(): + {} + """ + for body in [ + " abc.a()", + " art.a()", + " ast.a()", + " dis.a()", + " dix.a()", + " abc[...]()", + " art()()", + " (ast or ...)()", + " [dis]()", + " (dix + ...)()", + ]: + with self.subTest(body): + namespace = {} + source = textwrap.dedent(source_template.format(body)) + exec(source, namespace) + code = namespace["f"].__code__ + self.assertOpcodeSourcePositionIs( + code, + "LOAD_GLOBAL", + line=10, + end_line=10, + column=4, + end_column=7, + ) + + def test_attribute_augassign(self): + source = "(\n lhs \n . \n rhs \n ) += 42" + code = compile(source, "", "exec") + self.assertOpcodeSourcePositionIs( + code, "LOAD_ATTR", line=4, end_line=4, column=5, end_column=8 + ) + self.assertOpcodeSourcePositionIs( + code, "STORE_ATTR", line=4, end_line=4, column=5, end_column=8 + ) + + def test_attribute_del(self): + source = "del (\n lhs \n . \n rhs \n )" + code = compile(source, "", "exec") + self.assertOpcodeSourcePositionIs( + code, "DELETE_ATTR", line=4, end_line=4, column=5, end_column=8 + ) + + def test_attribute_load(self): + source = "(\n lhs \n . \n rhs \n )" + code = compile(source, "", "exec") + self.assertOpcodeSourcePositionIs( + code, "LOAD_ATTR", line=4, end_line=4, column=5, end_column=8 + ) + + def test_attribute_store(self): + source = "(\n lhs \n . \n rhs \n ) = 42" + code = compile(source, "", "exec") + self.assertOpcodeSourcePositionIs( + code, "STORE_ATTR", line=4, end_line=4, column=5, end_column=8 + ) + + def test_method_call(self): + source = "(\n lhs \n . \n rhs \n )()" + code = compile(source, "", "exec") + self.assertOpcodeSourcePositionIs( + code, "LOAD_METHOD", line=4, end_line=4, column=5, end_column=8 + ) + self.assertOpcodeSourcePositionIs( + code, "CALL", line=4, end_line=5, column=5, end_column=10 + ) + + def test_weird_attribute_position_regressions(self): + def f(): + (bar. + baz) + (bar. + baz( + )) + files().setdefault( + 0 + ).setdefault( + 0 + ) + for line, end_line, column, end_column in f.__code__.co_positions(): + self.assertIsNotNone(line) + self.assertIsNotNone(end_line) + self.assertIsNotNone(column) + self.assertIsNotNone(end_column) + self.assertLessEqual((line, column), (end_line, end_column)) + + @support.cpython_only + def test_column_offset_deduplication(self): + # GH-95150: Code with different column offsets shouldn't be merged! + for source in [ + "lambda: a", + "(a for b in c)", + "[a for b in c]", + "{a for b in c}", + "{a: b for c in d}", + ]: + with self.subTest(source): + code = compile(f"{source}, {source}", "", "eval") + self.assertEqual(len(code.co_consts), 2) + self.assertIsInstance(code.co_consts[0], types.CodeType) + self.assertIsInstance(code.co_consts[1], types.CodeType) + self.assertNotEqual(code.co_consts[0], code.co_consts[1]) + self.assertNotEqual( + list(code.co_consts[0].co_positions()), + list(code.co_consts[1].co_positions()), + ) + class TestExpressionStackSize(unittest.TestCase): # These tests check that the computed stack size for a code object @@ -819,6 +1393,32 @@ def test_if_else(self): def test_binop(self): self.check_stack_size("x + " * self.N + "x") + def test_list(self): + self.check_stack_size("[" + "x, " * self.N + "x]") + + def test_tuple(self): + self.check_stack_size("(" + "x, " * self.N + "x)") + + def test_set(self): + self.check_stack_size("{" + "x, " * self.N + "x}") + + def test_dict(self): + self.check_stack_size("{" + "x:x, " * self.N + "x:x}") + + def test_func_args(self): + self.check_stack_size("f(" + "x, " * self.N + ")") + + def test_func_kwargs(self): + kwargs = (f'a{i}=x' for i in range(self.N)) + self.check_stack_size("f(" + ", ".join(kwargs) + ")") + + def test_meth_args(self): + self.check_stack_size("o.m(" + "x, " * self.N + ")") + + def test_meth_kwargs(self): + kwargs = (f'a{i}=x' for i in range(self.N)) + self.check_stack_size("o.m(" + ", ".join(kwargs) + ")") + # TODO: RUSTPYTHON @unittest.expectedFailure def test_func_and(self): @@ -826,6 +1426,19 @@ def test_func_and(self): code += " x and x\n" * self.N self.check_stack_size(code) + def test_stack_3050(self): + M = 3050 + code = "x," * M + "=t" + # This raised on 3.10.0 to 3.10.5 + compile(code, "", "single") + + def test_stack_3050_2(self): + M = 3050 + args = ", ".join(f"arg{i}:type{i}" for i in range(M)) + code = f"def f({args}):\n pass" + # This raised on 3.10.0 to 3.10.5 + compile(code, "", "single") + class TestStackSizeStability(unittest.TestCase): # Check that repeating certain snippets doesn't increase the stack size @@ -902,6 +1515,39 @@ def test_try_except_as(self): """ self.check_stack_size(snippet) + def test_try_except_star_qualified(self): + snippet = """ + try: + a + except* ImportError: + b + else: + c + """ + self.check_stack_size(snippet) + + def test_try_except_star_as(self): + snippet = """ + try: + a + except* ImportError as e: + b + else: + c + """ + self.check_stack_size(snippet) + + def test_try_except_star_finally(self): + snippet = """ + try: + a + except* A: + b + finally: + c + """ + self.check_stack_size(snippet) + def test_try_finally(self): snippet = """ try: