1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
Subject: Add support for Python 3.14
Author: Alexander Sulfrian <alexander@sulfrian.net>
Forwarded: https://github.com/we-like-parsers/pegen/pull/112
This adjusts the grammar and tests to support the changes introduced
with Python 3.14.
Index: python-pegen/data/python.gram
===================================================================
--- python-pegen.orig/data/python.gram
+++ python-pegen/data/python.gram
@@ -1047,13 +1047,29 @@ try_stmt[ast.Try]:
except_block[ast.ExceptHandler]:
| invalid_except_stmt_indent
+ | 'except' e=expressions ':' b=block {
+ PY_VERSION >= (3, 14);
+ ast.ExceptHandler(type=e, name=None, body=b, LOCATIONS) }
+ | 'except' e=expression 'as' t=NAME ':' b=block {
+ PY_VERSION >= (3, 14);
+ ast.ExceptHandler(type=e, name=t.string, body=b, LOCATIONS) }
| 'except' e=expression t=['as' z=NAME { z.string }] ':' b=block {
+ PY_VERSION < (3, 14);
ast.ExceptHandler(type=e, name=t, body=b, LOCATIONS) }
| 'except' ':' b=block { ast.ExceptHandler(type=None, name=None, body=b, LOCATIONS) }
| invalid_except_stmt
except_star_block[ast.ExceptHandler]:
| invalid_except_star_stmt_indent
+ | 'except' '*' e=expressions ':' b=block {
+ PY_VERSION >= (3, 14);
+ ast.ExceptHandler(type=e, name=None, body=b, LOCATIONS)
+ }
+ | 'except' '*' e=expression 'as' t=NAME ':' b=block {
+ PY_VERSION >= (3, 14);
+ ast.ExceptHandler(type=e, name=t.string, body=b, LOCATIONS)
+ }
| 'except' '*' e=expression t=['as' z=NAME { z.string }] ':' b=block {
+ PY_VERSION < (3, 14);
ast.ExceptHandler(type=e, name=t, body=b, LOCATIONS)
}
| invalid_except_stmt
@@ -2256,7 +2272,12 @@ invalid_try_stmt[NoReturn]:
)
}
invalid_except_stmt[None]:
+ | 'except' '*'? a=expression ',' expressions 'as' NAME ':' {
+ PY_VERSION >= (3, 14);
+ self.raise_syntax_error_starting_from("multiple exception types must be parenthesized when using 'as'", a)
+ }
| 'except' '*'? a=expression ',' expressions ['as' NAME ] ':' {
+ PY_VERSION < (3, 14);
self.raise_syntax_error_starting_from("multiple exception types must be parenthesized", a)
}
| a='except' '*'? expression ['as' NAME ] NEWLINE { self.raise_syntax_error("expected ':'") }
@@ -2315,8 +2336,11 @@ invalid_as_pattern[NoReturn]:
| or_pattern 'as' a="_" {
self.raise_syntax_error_known_location("cannot use '_' as a target", a)
}
- | or_pattern 'as' !NAME a=expression {
- self.raise_syntax_error_known_location("invalid pattern target", a)
+ | or_pattern 'as' a=expression {
+ self.raise_syntax_error_known_location(
+ f"cannot use {self.get_expr_name(a)} as pattern target", a)
+ if self.py_version >= (3, 14)
+ else self.raise_syntax_error_known_location("invalid pattern target", a)
}
invalid_class_pattern[NoReturn]:
| name_or_attr '(' a=invalid_class_argument_pattern {
Index: python-pegen/tests/python_parser/test_syntax_error_handling.py
===================================================================
--- python-pegen.orig/tests/python_parser/test_syntax_error_handling.py
+++ python-pegen/tests/python_parser/test_syntax_error_handling.py
@@ -976,26 +976,32 @@ def test_invalid_try_stmt(
sys.version_info < (3, 11), reason="Syntax unsupported before 3.11+"
),
),
- (
+ pytest.param(
"try:\n\tpass\nexcept ValueError, IndexError:",
SyntaxError,
"multiple exception types must be parenthesized",
(3, 8),
(3, 30),
+ marks=pytest.mark.skipif(
+ sys.version_info >= (3, 14), reason="PEP 758 allows unparenthesized except and except* blocks"
+ ),
),
- (
+ pytest.param(
"try:\n\tpass\nexcept ValueError, IndexError,:",
SyntaxError,
"multiple exception types must be parenthesized",
(3, 8),
(3, 31),
+ marks=pytest.mark.skipif(
+ sys.version_info >= (3, 14), reason="PEP 758 allows unparenthesized except and except* blocks"
+ ),
),
(
"try:\n\tpass\nexcept ValueError, IndexError, a=1:",
SyntaxError,
"invalid syntax",
- (3, 18),
- (3, 19),
+ (3, 33) if sys.version_info >= (3, 14) else (3, 18),
+ (3, 34) if sys.version_info >= (3, 14) else (3, 19),
),
(
"try:\n\tpass\nexcept Exception\npass",
@@ -1156,7 +1162,7 @@ def test_invalid_case_stmt(
(
"match a:\n\tcase 1 as 1+1:\n\t\tpass",
SyntaxError,
- "invalid pattern target",
+ "cannot use expression as pattern target" if sys.version_info >= (3, 14) else "invalid pattern target",
(2, 12),
(2, 15),
),
Index: python-pegen/src/pegen/python_generator.py
===================================================================
--- python-pegen.orig/src/pegen/python_generator.py
+++ python-pegen/src/pegen/python_generator.py
@@ -397,6 +397,7 @@ class PythonParserGenerator(ParserGenera
locations = False
unreachable = False
used = None
+ version_check = None
if action:
# Replace magic name in the action rule
if "LOCATIONS" in action:
@@ -405,6 +406,11 @@ class PythonParserGenerator(ParserGenera
if "UNREACHABLE" in action:
unreachable = True
action = action.replace("UNREACHABLE", self.unreachable_formatting)
+ if ";" in action:
+ parts = action.split(";", 1)
+ if parts[0].startswith("PY_VERSION"):
+ action = parts[1].lstrip()
+ version_check = parts[0].replace("PY_VERSION", "self.py_version")
# Extract the names actually used in the action.
used = self.usednamesvisitor.visit(ast.parse(action))
@@ -423,6 +429,11 @@ class PythonParserGenerator(ParserGenera
if has_invalid:
self.print("self.call_invalid_rules")
first = False
+ if version_check:
+ if not first:
+ self.print("and")
+ self.print(f"({version_check})")
+ first = False
for item in node.items:
if first:
first = False
|