| import sysconfig |
| import textwrap |
| import unittest |
| from distutils.tests.support import TempdirManager |
| from pathlib import Path |
| |
| from test import test_tools |
| from test import support |
| from test.support.script_helper import assert_python_ok |
| |
| _py_cflags_nodist = sysconfig.get_config_var('PY_CFLAGS_NODIST') |
| _pgo_flag = sysconfig.get_config_var('PGO_PROF_USE_FLAG') |
| if _pgo_flag and _py_cflags_nodist and _pgo_flag in _py_cflags_nodist: |
| raise unittest.SkipTest("peg_generator test disabled under PGO build") |
| |
| test_tools.skip_if_missing("peg_generator") |
| with test_tools.imports_under_tool("peg_generator"): |
| from pegen.grammar_parser import GeneratedParser as GrammarParser |
| from pegen.testutil import ( |
| parse_string, |
| generate_parser_c_extension, |
| generate_c_parser_source, |
| ) |
| from pegen.ast_dump import ast_dump |
| |
| |
| TEST_TEMPLATE = """ |
| tmp_dir = {extension_path!r} |
| |
| import ast |
| import traceback |
| import sys |
| import unittest |
| |
| from test import test_tools |
| with test_tools.imports_under_tool("peg_generator"): |
| from pegen.ast_dump import ast_dump |
| |
| sys.path.insert(0, tmp_dir) |
| import parse |
| |
| class Tests(unittest.TestCase): |
| |
| def check_input_strings_for_grammar( |
| self, |
| valid_cases = (), |
| invalid_cases = (), |
| ): |
| if valid_cases: |
| for case in valid_cases: |
| parse.parse_string(case, mode=0) |
| |
| if invalid_cases: |
| for case in invalid_cases: |
| with self.assertRaises(SyntaxError): |
| parse.parse_string(case, mode=0) |
| |
| def verify_ast_generation(self, stmt): |
| expected_ast = ast.parse(stmt) |
| actual_ast = parse.parse_string(stmt, mode=1) |
| self.assertEqual(ast_dump(expected_ast), ast_dump(actual_ast)) |
| |
| def test_parse(self): |
| {test_source} |
| |
| unittest.main() |
| """ |
| |
| |
| class TestCParser(TempdirManager, unittest.TestCase): |
| def setUp(self): |
| cmd = support.missing_compiler_executable() |
| if cmd is not None: |
| self.skipTest("The %r command is not found" % cmd) |
| super(TestCParser, self).setUp() |
| self.tmp_path = self.mkdtemp() |
| change_cwd = support.change_cwd(self.tmp_path) |
| change_cwd.__enter__() |
| self.addCleanup(change_cwd.__exit__, None, None, None) |
| |
| def tearDown(self): |
| super(TestCParser, self).tearDown() |
| |
| def build_extension(self, grammar_source): |
| grammar = parse_string(grammar_source, GrammarParser) |
| generate_parser_c_extension(grammar, Path(self.tmp_path)) |
| |
| def run_test(self, grammar_source, test_source): |
| self.build_extension(grammar_source) |
| test_source = textwrap.indent(textwrap.dedent(test_source), 8 * " ") |
| assert_python_ok( |
| "-c", |
| TEST_TEMPLATE.format(extension_path=self.tmp_path, test_source=test_source), |
| ) |
| |
| def test_c_parser(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=stmt* $ { Module(a, NULL, p->arena) } |
| stmt[stmt_ty]: a=expr_stmt { a } |
| expr_stmt[stmt_ty]: a=expression NEWLINE { _Py_Expr(a, EXTRA) } |
| expression[expr_ty]: ( l=expression '+' r=term { _Py_BinOp(l, Add, r, EXTRA) } |
| | l=expression '-' r=term { _Py_BinOp(l, Sub, r, EXTRA) } |
| | t=term { t } |
| ) |
| term[expr_ty]: ( l=term '*' r=factor { _Py_BinOp(l, Mult, r, EXTRA) } |
| | l=term '/' r=factor { _Py_BinOp(l, Div, r, EXTRA) } |
| | f=factor { f } |
| ) |
| factor[expr_ty]: ('(' e=expression ')' { e } |
| | a=atom { a } |
| ) |
| atom[expr_ty]: ( n=NAME { n } |
| | n=NUMBER { n } |
| | s=STRING { s } |
| ) |
| """ |
| test_source = """ |
| expressions = [ |
| "4+5", |
| "4-5", |
| "4*5", |
| "1+4*5", |
| "1+4/5", |
| "(1+1) + (1+1)", |
| "(1+1) - (1+1)", |
| "(1+1) * (1+1)", |
| "(1+1) / (1+1)", |
| ] |
| |
| for expr in expressions: |
| the_ast = parse.parse_string(expr, mode=1) |
| expected_ast = ast.parse(expr) |
| self.assertEqual(ast_dump(the_ast), ast_dump(expected_ast)) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_lookahead(self) -> None: |
| grammar_source = """ |
| start: NAME &NAME expr NEWLINE? ENDMARKER |
| expr: NAME | NUMBER |
| """ |
| test_source = """ |
| valid_cases = ["foo bar"] |
| invalid_cases = ["foo 34"] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_negative_lookahead(self) -> None: |
| grammar_source = """ |
| start: NAME !NAME expr NEWLINE? ENDMARKER |
| expr: NAME | NUMBER |
| """ |
| test_source = """ |
| valid_cases = ["foo 34"] |
| invalid_cases = ["foo bar"] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_cut(self) -> None: |
| grammar_source = """ |
| start: X ~ Y Z | X Q S |
| X: 'x' |
| Y: 'y' |
| Z: 'z' |
| Q: 'q' |
| S: 's' |
| """ |
| test_source = """ |
| valid_cases = ["x y z"] |
| invalid_cases = ["x q s"] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_gather(self) -> None: |
| grammar_source = """ |
| start: ';'.pass_stmt+ NEWLINE |
| pass_stmt: 'pass' |
| """ |
| test_source = """ |
| valid_cases = ["pass", "pass; pass"] |
| invalid_cases = ["pass;", "pass; pass;"] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_left_recursion(self) -> None: |
| grammar_source = """ |
| start: expr NEWLINE |
| expr: ('-' term | expr '+' term | term) |
| term: NUMBER |
| """ |
| test_source = """ |
| valid_cases = ["-34", "34", "34 + 12", "1 + 1 + 2 + 3"] |
| self.check_input_strings_for_grammar(valid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_advanced_left_recursive(self) -> None: |
| grammar_source = """ |
| start: NUMBER | sign start |
| sign: ['-'] |
| """ |
| test_source = """ |
| valid_cases = ["23", "-34"] |
| self.check_input_strings_for_grammar(valid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_mutually_left_recursive(self) -> None: |
| grammar_source = """ |
| start: foo 'E' |
| foo: bar 'A' | 'B' |
| bar: foo 'C' | 'D' |
| """ |
| test_source = """ |
| valid_cases = ["B E", "D A C A E"] |
| self.check_input_strings_for_grammar(valid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_nasty_mutually_left_recursive(self) -> None: |
| grammar_source = """ |
| start: target '=' |
| target: maybe '+' | NAME |
| maybe: maybe '-' | target |
| """ |
| test_source = """ |
| valid_cases = ["x ="] |
| invalid_cases = ["x - + ="] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_return_stmt_noexpr_action(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=[statements] ENDMARKER { Module(a, NULL, p->arena) } |
| statements[asdl_seq*]: a=statement+ { a } |
| statement[stmt_ty]: simple_stmt |
| simple_stmt[stmt_ty]: small_stmt |
| small_stmt[stmt_ty]: return_stmt |
| return_stmt[stmt_ty]: a='return' NEWLINE { _Py_Return(NULL, EXTRA) } |
| """ |
| test_source = """ |
| stmt = "return" |
| self.verify_ast_generation(stmt) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_gather_action_ast(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=';'.pass_stmt+ NEWLINE ENDMARKER { Module(a, NULL, p->arena) } |
| pass_stmt[stmt_ty]: a='pass' { _Py_Pass(EXTRA)} |
| """ |
| test_source = """ |
| stmt = "pass; pass" |
| self.verify_ast_generation(stmt) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_pass_stmt_action(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=[statements] ENDMARKER { Module(a, NULL, p->arena) } |
| statements[asdl_seq*]: a=statement+ { a } |
| statement[stmt_ty]: simple_stmt |
| simple_stmt[stmt_ty]: small_stmt |
| small_stmt[stmt_ty]: pass_stmt |
| pass_stmt[stmt_ty]: a='pass' NEWLINE { _Py_Pass(EXTRA) } |
| """ |
| test_source = """ |
| stmt = "pass" |
| self.verify_ast_generation(stmt) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_if_stmt_action(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=[statements] ENDMARKER { Module(a, NULL, p->arena) } |
| statements[asdl_seq*]: a=statement+ { _PyPegen_seq_flatten(p, a) } |
| statement[asdl_seq*]: a=compound_stmt { _PyPegen_singleton_seq(p, a) } | simple_stmt |
| |
| simple_stmt[asdl_seq*]: a=small_stmt b=further_small_stmt* [';'] NEWLINE { _PyPegen_seq_insert_in_front(p, a, b) } |
| further_small_stmt[stmt_ty]: ';' a=small_stmt { a } |
| |
| block: simple_stmt | NEWLINE INDENT a=statements DEDENT { a } |
| |
| compound_stmt: if_stmt |
| |
| if_stmt: 'if' a=full_expression ':' b=block { _Py_If(a, b, NULL, EXTRA) } |
| |
| small_stmt[stmt_ty]: pass_stmt |
| |
| pass_stmt[stmt_ty]: a='pass' { _Py_Pass(EXTRA) } |
| |
| full_expression: NAME |
| """ |
| test_source = """ |
| stmt = "pass" |
| self.verify_ast_generation(stmt) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_same_name_different_types(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=import_from+ NEWLINE ENDMARKER { Module(a, NULL, p->arena)} |
| import_from[stmt_ty]: ( a='from' !'import' c=simple_name 'import' d=import_as_names_from { |
| _Py_ImportFrom(c->v.Name.id, d, 0, EXTRA) } |
| | a='from' '.' 'import' c=import_as_names_from { |
| _Py_ImportFrom(NULL, c, 1, EXTRA) } |
| ) |
| simple_name[expr_ty]: NAME |
| import_as_names_from[asdl_seq*]: a=','.import_as_name_from+ { a } |
| import_as_name_from[alias_ty]: a=NAME 'as' b=NAME { _Py_alias(((expr_ty) a)->v.Name.id, ((expr_ty) b)->v.Name.id, p->arena) } |
| """ |
| test_source = """ |
| for stmt in ("from a import b as c", "from . import a as b"): |
| expected_ast = ast.parse(stmt) |
| actual_ast = parse.parse_string(stmt, mode=1) |
| self.assertEqual(ast_dump(expected_ast), ast_dump(actual_ast)) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_with_stmt_with_paren(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=[statements] ENDMARKER { Module(a, NULL, p->arena) } |
| statements[asdl_seq*]: a=statement+ { _PyPegen_seq_flatten(p, a) } |
| statement[asdl_seq*]: a=compound_stmt { _PyPegen_singleton_seq(p, a) } |
| compound_stmt[stmt_ty]: with_stmt |
| with_stmt[stmt_ty]: ( |
| a='with' '(' b=','.with_item+ ')' ':' c=block { |
| _Py_With(b, _PyPegen_singleton_seq(p, c), NULL, EXTRA) } |
| ) |
| with_item[withitem_ty]: ( |
| e=NAME o=['as' t=NAME { t }] { _Py_withitem(e, _PyPegen_set_expr_context(p, o, Store), p->arena) } |
| ) |
| block[stmt_ty]: a=pass_stmt NEWLINE { a } | NEWLINE INDENT a=pass_stmt DEDENT { a } |
| pass_stmt[stmt_ty]: a='pass' { _Py_Pass(EXTRA) } |
| """ |
| test_source = """ |
| stmt = "with (\\n a as b,\\n c as d\\n): pass" |
| the_ast = parse.parse_string(stmt, mode=1) |
| self.assertTrue(ast_dump(the_ast).startswith( |
| "Module(body=[With(items=[withitem(context_expr=Name(id='a', ctx=Load()), optional_vars=Name(id='b', ctx=Store())), " |
| "withitem(context_expr=Name(id='c', ctx=Load()), optional_vars=Name(id='d', ctx=Store()))]" |
| )) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_ternary_operator(self) -> None: |
| grammar_source = """ |
| start[mod_ty]: a=expr ENDMARKER { Module(a, NULL, p->arena) } |
| expr[asdl_seq*]: a=listcomp NEWLINE { _PyPegen_singleton_seq(p, _Py_Expr(a, EXTRA)) } |
| listcomp[expr_ty]: ( |
| a='[' b=NAME c=for_if_clauses d=']' { _Py_ListComp(b, c, EXTRA) } |
| ) |
| for_if_clauses[asdl_seq*]: ( |
| a=(y=[ASYNC] 'for' a=NAME 'in' b=NAME c=('if' z=NAME { z })* |
| { _Py_comprehension(_Py_Name(((expr_ty) a)->v.Name.id, Store, EXTRA), b, c, (y == NULL) ? 0 : 1, p->arena) })+ { a } |
| ) |
| """ |
| test_source = """ |
| stmt = "[i for i in a if b]" |
| self.verify_ast_generation(stmt) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_syntax_error_for_string(self) -> None: |
| grammar_source = """ |
| start: expr+ NEWLINE? ENDMARKER |
| expr: NAME |
| """ |
| test_source = r""" |
| for text in ("a b 42 b a", "\u540d \u540d 42 \u540d \u540d"): |
| try: |
| parse.parse_string(text, mode=0) |
| except SyntaxError as e: |
| tb = traceback.format_exc() |
| self.assertTrue('File "<string>", line 1' in tb) |
| self.assertTrue(f"SyntaxError: invalid syntax" in tb) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_headers_and_trailer(self) -> None: |
| grammar_source = """ |
| @header 'SOME HEADER' |
| @subheader 'SOME SUBHEADER' |
| @trailer 'SOME TRAILER' |
| start: expr+ NEWLINE? ENDMARKER |
| expr: x=NAME |
| """ |
| grammar = parse_string(grammar_source, GrammarParser) |
| parser_source = generate_c_parser_source(grammar) |
| |
| self.assertTrue("SOME HEADER" in parser_source) |
| self.assertTrue("SOME SUBHEADER" in parser_source) |
| self.assertTrue("SOME TRAILER" in parser_source) |
| |
| def test_error_in_rules(self) -> None: |
| grammar_source = """ |
| start: expr+ NEWLINE? ENDMARKER |
| expr: NAME {PyTuple_New(-1)} |
| """ |
| # PyTuple_New raises SystemError if an invalid argument was passed. |
| test_source = """ |
| with self.assertRaises(SystemError): |
| parse.parse_string("a", mode=0) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_no_soft_keywords(self) -> None: |
| grammar_source = """ |
| start: expr+ NEWLINE? ENDMARKER |
| expr: 'foo' |
| """ |
| grammar = parse_string(grammar_source, GrammarParser) |
| parser_source = generate_c_parser_source(grammar) |
| assert "expect_soft_keyword" not in parser_source |
| |
| def test_soft_keywords(self) -> None: |
| grammar_source = """ |
| start: expr+ NEWLINE? ENDMARKER |
| expr: "foo" |
| """ |
| grammar = parse_string(grammar_source, GrammarParser) |
| parser_source = generate_c_parser_source(grammar) |
| assert "expect_soft_keyword" in parser_source |
| |
| def test_soft_keywords_parse(self) -> None: |
| grammar_source = """ |
| start: "if" expr '+' expr NEWLINE |
| expr: NAME |
| """ |
| test_source = """ |
| valid_cases = ["if if + if"] |
| invalid_cases = ["if if"] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |
| |
| def test_soft_keywords_lookahead(self) -> None: |
| grammar_source = """ |
| start: &"if" "if" expr '+' expr NEWLINE |
| expr: NAME |
| """ |
| test_source = """ |
| valid_cases = ["if if + if"] |
| invalid_cases = ["if if"] |
| self.check_input_strings_for_grammar(valid_cases, invalid_cases) |
| """ |
| self.run_test(grammar_source, test_source) |