Skip to content

Commit d60457a

Browse files
authored
bpo-45292: [PEP-654] add except* (GH-29581)
1 parent 850aefc commit d60457a

34 files changed

+5641
-1903
lines changed

Doc/library/ast.rst

+31
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,37 @@ Control flow
11671167
type_ignores=[])
11681168

11691169

1170+
.. class:: TryStar(body, handlers, orelse, finalbody)
1171+
1172+
``try`` blocks which are followed by ``except*`` clauses. The attributes are the
1173+
same as for :class:`Try` but the :class:`ExceptHandler` nodes in ``handlers``
1174+
are interpreted as ``except*`` blocks rather then ``except``.
1175+
1176+
.. doctest::
1177+
1178+
>>> print(ast.dump(ast.parse("""
1179+
... try:
1180+
... ...
1181+
... except* Exception:
1182+
... ...
1183+
... """), indent=4))
1184+
Module(
1185+
body=[
1186+
TryStar(
1187+
body=[
1188+
Expr(
1189+
value=Constant(value=Ellipsis))],
1190+
handlers=[
1191+
ExceptHandler(
1192+
type=Name(id='Exception', ctx=Load()),
1193+
body=[
1194+
Expr(
1195+
value=Constant(value=Ellipsis))])],
1196+
orelse=[],
1197+
finalbody=[])],
1198+
type_ignores=[])
1199+
1200+
11701201
.. class:: ExceptHandler(type, name, body)
11711202

11721203
A single ``except`` clause. ``type`` is the exception type it will match,

Doc/library/dis.rst

+26
Original file line numberDiff line numberDiff line change
@@ -872,8 +872,10 @@ All of the following opcodes use their arguments.
872872

873873
.. versionadded:: 3.1
874874

875+
875876
.. opcode:: JUMP_IF_NOT_EXC_MATCH (target)
876877

878+
Performs exception matching for ``except``.
877879
Tests whether the second value on the stack is an exception matching TOS,
878880
and jumps if it is not. Pops one value from the stack.
879881

@@ -883,6 +885,30 @@ All of the following opcodes use their arguments.
883885
This opcode no longer pops the active exception.
884886

885887

888+
.. opcode:: JUMP_IF_NOT_EG_MATCH (target)
889+
890+
Performs exception matching for ``except*``. Applies ``split(TOS)`` on
891+
the exception group representing TOS1. Jumps if no match is found.
892+
893+
Pops one item from the stack. If a match was found, pops the 3 items representing
894+
the exception and pushes the 3 items representing the non-matching part of
895+
the exception group, followed by the 3 items representing the matching part.
896+
In other words, in case of a match it pops 4 items and pushes 6.
897+
898+
.. versionadded:: 3.11
899+
900+
901+
.. opcode:: PREP_RERAISE_STAR
902+
903+
Combines the raised and reraised exceptions list from TOS, into an exception
904+
group to propagate from a try-except* block. Uses the original exception
905+
group from TOS1 to reconstruct the structure of reraised exceptions. Pops
906+
two items from the stack and pushes a triplet representing the exception to
907+
reraise or three ``None`` if there isn't one.
908+
909+
.. versionadded:: 3.11
910+
911+
886912
.. opcode:: JUMP_IF_TRUE_OR_POP (target)
887913

888914
If TOS is true, sets the bytecode counter to *target* and leaves TOS on the

Doc/whatsnew/3.11.rst

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Summary -- Release highlights
6565
6666
.. PEP-sized items next.
6767
68+
PEP-654: Exception Groups and ``except*``.
69+
(Contributed by Irit Katriel in :issue:`45292`.)
6870

6971
New Features
7072
============

Grammar/python.gram

+15-2
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,8 @@ try_stmt[stmt_ty]:
403403
| invalid_try_stmt
404404
| 'try' &&':' b=block f=finally_block { _PyAST_Try(b, NULL, NULL, f, EXTRA) }
405405
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_block+ el=[else_block] f=[finally_block] { _PyAST_Try(b, ex, el, f, EXTRA) }
406+
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_star_block+ el=[else_block] f=[finally_block] { _PyAST_TryStar(b, ex, el, f, EXTRA) }
407+
406408

407409
# Except statement
408410
# ----------------
@@ -413,6 +415,11 @@ except_block[excepthandler_ty]:
413415
_PyAST_ExceptHandler(e, (t) ? ((expr_ty) t)->v.Name.id : NULL, b, EXTRA) }
414416
| 'except' ':' b=block { _PyAST_ExceptHandler(NULL, NULL, b, EXTRA) }
415417
| invalid_except_stmt
418+
except_star_block[excepthandler_ty]:
419+
| invalid_except_star_stmt_indent
420+
| 'except' '*' e=expression t=['as' z=NAME { z }] ':' b=block {
421+
_PyAST_ExceptHandler(e, (t) ? ((expr_ty) t)->v.Name.id : NULL, b, EXTRA) }
422+
| invalid_except_stmt
416423
finally_block[asdl_stmt_seq*]:
417424
| invalid_finally_stmt
418425
| 'finally' &&':' a=block { a }
@@ -1192,18 +1199,24 @@ invalid_try_stmt:
11921199
| a='try' ':' NEWLINE !INDENT {
11931200
RAISE_INDENTATION_ERROR("expected an indented block after 'try' statement on line %d", a->lineno) }
11941201
| 'try' ':' block !('except' | 'finally') { RAISE_SYNTAX_ERROR("expected 'except' or 'finally' block") }
1202+
| 'try' ':' block* ((except_block+ except_star_block) | (except_star_block+ except_block)) block* {
1203+
RAISE_SYNTAX_ERROR("cannot have both 'except' and 'except*' on the same 'try'") }
11951204
invalid_except_stmt:
1196-
| 'except' a=expression ',' expressions ['as' NAME ] ':' {
1205+
| 'except' '*'? a=expression ',' expressions ['as' NAME ] ':' {
11971206
RAISE_SYNTAX_ERROR_STARTING_FROM(a, "multiple exception types must be parenthesized") }
1198-
| a='except' expression ['as' NAME ] NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
1207+
| a='except' '*'? expression ['as' NAME ] NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
11991208
| a='except' NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
1209+
| a='except' '*' (NEWLINE | ':') { RAISE_SYNTAX_ERROR("expected one or more exception types") }
12001210
invalid_finally_stmt:
12011211
| a='finally' ':' NEWLINE !INDENT {
12021212
RAISE_INDENTATION_ERROR("expected an indented block after 'finally' statement on line %d", a->lineno) }
12031213
invalid_except_stmt_indent:
12041214
| a='except' expression ['as' NAME ] ':' NEWLINE !INDENT {
12051215
RAISE_INDENTATION_ERROR("expected an indented block after 'except' statement on line %d", a->lineno) }
12061216
| a='except' ':' NEWLINE !INDENT { RAISE_SYNTAX_ERROR("expected an indented block after except statement on line %d", a->lineno) }
1217+
invalid_except_star_stmt_indent:
1218+
| a='except' '*' expression ['as' NAME ] ':' NEWLINE !INDENT {
1219+
RAISE_INDENTATION_ERROR("expected an indented block after 'except*' statement on line %d", a->lineno) }
12071220
invalid_match_stmt:
12081221
| "match" subject_expr !':' { CHECK_VERSION(void*, 10, "Pattern matching is", RAISE_SYNTAX_ERROR("expected ':'") ) }
12091222
| a="match" subject=subject_expr ':' NEWLINE !INDENT {

Include/internal/pycore_ast.h

+14-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_ast_state.h

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ struct ast_state {
132132
PyObject *Sub_singleton;
133133
PyObject *Sub_type;
134134
PyObject *Subscript_type;
135+
PyObject *TryStar_type;
135136
PyObject *Try_type;
136137
PyObject *Tuple_type;
137138
PyObject *TypeIgnore_type;

Include/internal/pycore_pyerrors.h

+8
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ PyAPI_FUNC(PyObject *) _PyErr_FormatFromCauseTstate(
9292
const char *format,
9393
...);
9494

95+
PyAPI_FUNC(PyObject *) _PyExc_CreateExceptionGroup(
96+
const char *msg,
97+
PyObject *excs);
98+
99+
PyAPI_FUNC(PyObject *) _PyExc_ExceptionGroupProjection(
100+
PyObject *left,
101+
PyObject *right);
102+
95103
PyAPI_FUNC(int) _PyErr_CheckSignalsTstate(PyThreadState *tstate);
96104

97105
PyAPI_FUNC(void) _Py_DumpExtensionModules(int fd, PyInterpreterState *interp);

Include/opcode.h

+4-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/ast.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ def __init__(self, *, _avoid_backslashes=False):
683683
self._type_ignores = {}
684684
self._indent = 0
685685
self._avoid_backslashes = _avoid_backslashes
686+
self._in_try_star = False
686687

687688
def interleave(self, inter, f, seq):
688689
"""Call f on each item in seq, calling inter() in between."""
@@ -953,7 +954,7 @@ def visit_Raise(self, node):
953954
self.write(" from ")
954955
self.traverse(node.cause)
955956

956-
def visit_Try(self, node):
957+
def do_visit_try(self, node):
957958
self.fill("try")
958959
with self.block():
959960
self.traverse(node.body)
@@ -968,8 +969,24 @@ def visit_Try(self, node):
968969
with self.block():
969970
self.traverse(node.finalbody)
970971

972+
def visit_Try(self, node):
973+
prev_in_try_star = self._in_try_star
974+
try:
975+
self._in_try_star = False
976+
self.do_visit_try(node)
977+
finally:
978+
self._in_try_star = prev_in_try_star
979+
980+
def visit_TryStar(self, node):
981+
prev_in_try_star = self._in_try_star
982+
try:
983+
self._in_try_star = True
984+
self.do_visit_try(node)
985+
finally:
986+
self._in_try_star = prev_in_try_star
987+
971988
def visit_ExceptHandler(self, node):
972-
self.fill("except")
989+
self.fill("except*" if self._in_try_star else "except")
973990
if node.type:
974991
self.write(" ")
975992
self.traverse(node.type)

Lib/importlib/_bootstrap_external.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ def _write_atomic(path, data, mode=0o666):
371371
# Python 3.11a3 3464 (bpo-45636: Merge numeric BINARY_*/INPLACE_* into
372372
# BINARY_OP)
373373
# Python 3.11a3 3465 (Add COPY_FREE_VARS opcode)
374+
# Python 3.11a3 3466 (bpo-45292: PEP-654 except*)
374375

375376
#
376377
# MAGIC must change whenever the bytecode emitted by the compiler may no
@@ -380,7 +381,7 @@ def _write_atomic(path, data, mode=0o666):
380381
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
381382
# in PC/launcher.c must also be updated.
382383

383-
MAGIC_NUMBER = (3465).to_bytes(2, 'little') + b'\r\n'
384+
MAGIC_NUMBER = (3466).to_bytes(2, 'little') + b'\r\n'
384385
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
385386

386387
_PYCACHE = '__pycache__'

Lib/opcode.py

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def jabs_op(name, op):
103103
def_op('SETUP_ANNOTATIONS', 85)
104104
def_op('YIELD_VALUE', 86)
105105

106+
def_op('PREP_RERAISE_STAR', 88)
106107
def_op('POP_EXCEPT', 89)
107108

108109
HAVE_ARGUMENT = 90 # Opcodes from here have an argument:
@@ -150,6 +151,8 @@ def jabs_op(name, op):
150151
def_op('DELETE_FAST', 126) # Local variable number
151152
haslocal.append(126)
152153

154+
jabs_op('JUMP_IF_NOT_EG_MATCH', 127)
155+
153156
def_op('GEN_START', 129) # Kind of generator/coroutine
154157
def_op('RAISE_VARARGS', 130) # Number of raise arguments (1, 2, or 3)
155158
def_op('CALL_FUNCTION', 131) # #args

Lib/test/test_ast.py

+23
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def to_tuple(t):
8686
"try:\n pass\nexcept Exception:\n pass",
8787
# TryFinally
8888
"try:\n pass\nfinally:\n pass",
89+
# TryStarExcept
90+
"try:\n pass\nexcept* Exception:\n pass",
8991
# Assert
9092
"assert v",
9193
# Import
@@ -1310,6 +1312,26 @@ def test_try(self):
13101312
t = ast.Try([p], e, [p], [ast.Expr(ast.Name("x", ast.Store()))])
13111313
self.stmt(t, "must have Load context")
13121314

1315+
def test_try_star(self):
1316+
p = ast.Pass()
1317+
t = ast.TryStar([], [], [], [p])
1318+
self.stmt(t, "empty body on TryStar")
1319+
t = ast.TryStar([ast.Expr(ast.Name("x", ast.Store()))], [], [], [p])
1320+
self.stmt(t, "must have Load context")
1321+
t = ast.TryStar([p], [], [], [])
1322+
self.stmt(t, "TryStar has neither except handlers nor finalbody")
1323+
t = ast.TryStar([p], [], [p], [p])
1324+
self.stmt(t, "TryStar has orelse but no except handlers")
1325+
t = ast.TryStar([p], [ast.ExceptHandler(None, "x", [])], [], [])
1326+
self.stmt(t, "empty body on ExceptHandler")
1327+
e = [ast.ExceptHandler(ast.Name("x", ast.Store()), "y", [p])]
1328+
self.stmt(ast.TryStar([p], e, [], []), "must have Load context")
1329+
e = [ast.ExceptHandler(None, "x", [p])]
1330+
t = ast.TryStar([p], e, [ast.Expr(ast.Name("x", ast.Store()))], [p])
1331+
self.stmt(t, "must have Load context")
1332+
t = ast.TryStar([p], e, [p], [ast.Expr(ast.Name("x", ast.Store()))])
1333+
self.stmt(t, "must have Load context")
1334+
13131335
def test_assert(self):
13141336
self.stmt(ast.Assert(ast.Name("x", ast.Store()), None),
13151337
"must have Load context")
@@ -2316,6 +2338,7 @@ def main():
23162338
('Module', [('Raise', (1, 0, 1, 25), ('Call', (1, 6, 1, 25), ('Name', (1, 6, 1, 15), 'Exception', ('Load',)), [('Constant', (1, 16, 1, 24), 'string', None)], []), None)], []),
23172339
('Module', [('Try', (1, 0, 4, 6), [('Pass', (2, 2, 2, 6))], [('ExceptHandler', (3, 0, 4, 6), ('Name', (3, 7, 3, 16), 'Exception', ('Load',)), None, [('Pass', (4, 2, 4, 6))])], [], [])], []),
23182340
('Module', [('Try', (1, 0, 4, 6), [('Pass', (2, 2, 2, 6))], [], [], [('Pass', (4, 2, 4, 6))])], []),
2341+
('Module', [('TryStar', (1, 0, 4, 6), [('Pass', (2, 2, 2, 6))], [('ExceptHandler', (3, 0, 4, 6), ('Name', (3, 8, 3, 17), 'Exception', ('Load',)), None, [('Pass', (4, 2, 4, 6))])], [], [])], []),
23192342
('Module', [('Assert', (1, 0, 1, 8), ('Name', (1, 7, 1, 8), 'v', ('Load',)), None)], []),
23202343
('Module', [('Import', (1, 0, 1, 10), [('alias', (1, 7, 1, 10), 'sys', None)])], []),
23212344
('Module', [('ImportFrom', (1, 0, 1, 17), 'sys', [('alias', (1, 16, 1, 17), 'v', None)], 0)], []),

Lib/test/test_compile.py

+33
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,39 @@ def test_try_except_as(self):
12631263
"""
12641264
self.check_stack_size(snippet)
12651265

1266+
def test_try_except_star_qualified(self):
1267+
snippet = """
1268+
try:
1269+
a
1270+
except* ImportError:
1271+
b
1272+
else:
1273+
c
1274+
"""
1275+
self.check_stack_size(snippet)
1276+
1277+
def test_try_except_star_as(self):
1278+
snippet = """
1279+
try:
1280+
a
1281+
except* ImportError as e:
1282+
b
1283+
else:
1284+
c
1285+
"""
1286+
self.check_stack_size(snippet)
1287+
1288+
def test_try_except_star_finally(self):
1289+
snippet = """
1290+
try:
1291+
a
1292+
except* A:
1293+
b
1294+
finally:
1295+
c
1296+
"""
1297+
self.check_stack_size(snippet)
1298+
12661299
def test_try_finally(self):
12671300
snippet = """
12681301
try:

0 commit comments

Comments
 (0)