Skip to content

Commit c8390a7

Browse files
authored
Merge pull request #7069 from jepler/exception-chain
Implement chained exceptions
2 parents e82a8bf + dd443ba commit c8390a7

File tree

15 files changed

+327
-30
lines changed

15 files changed

+327
-30
lines changed

locale/circuitpython.pot

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ msgstr ""
175175
msgid "%q must be of type %q"
176176
msgstr ""
177177

178-
#: shared-bindings/digitalio/Pull.c
178+
#: py/objexcept.c shared-bindings/digitalio/Pull.c
179179
msgid "%q must be of type %q or None"
180180
msgstr ""
181181

@@ -894,6 +894,10 @@ msgstr ""
894894
msgid "Drive mode not used when direction is input."
895895
msgstr ""
896896

897+
#: py/obj.c
898+
msgid "During handling of the above exception, another exception occurred:"
899+
msgstr ""
900+
897901
#: shared-bindings/aesio/aes.c
898902
msgid "ECB only operates on 16 bytes at a time"
899903
msgstr ""
@@ -2011,6 +2015,10 @@ msgid ""
20112015
"exit safe mode."
20122016
msgstr ""
20132017

2018+
#: py/obj.c
2019+
msgid "The above exception was the direct cause of the following exception:"
2020+
msgstr ""
2021+
20142022
#: ports/espressif/boards/m5stack_atom_lite/mpconfigboard.h
20152023
msgid "The central button was pressed at start up.\n"
20162024
msgstr ""
@@ -3341,10 +3349,6 @@ msgstr ""
33413349
msgid "invalid syntax for number"
33423350
msgstr ""
33433351

3344-
#: py/objexcept.c
3345-
msgid "invalid traceback"
3346-
msgstr ""
3347-
33483352
#: py/objtype.c
33493353
msgid "issubclass() arg 1 must be a class"
33503354
msgstr ""

ports/unix/variants/coverage/mpconfigvariant.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
#define MICROPY_PY_UCRYPTOLIB (1)
6666
#define MICROPY_PY_UCRYPTOLIB_CTR (1)
6767
#define MICROPY_PY_MICROPYTHON_HEAP_LOCKED (1)
68+
#define MICROPY_CPYTHON_EXCEPTION_CHAIN (1)
6869

6970
// use vfs's functions for import stat and builtin open
7071
#define mp_import_stat mp_vfs_import_stat

py/circuitpy_mpconfig.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ typedef long mp_off_t;
224224
#ifndef MICROPY_CPYTHON_COMPAT
225225
#define MICROPY_CPYTHON_COMPAT (CIRCUITPY_FULL_BUILD)
226226
#endif
227+
#ifndef MICROPY_CPYTHON_EXCEPTION_CHAIN
228+
#define MICROPY_CPYTHON_EXCEPTION_CHAIN (CIRCUITPY_FULL_BUILD)
229+
#endif
227230
#define MICROPY_PY_BUILTINS_POW3 (CIRCUITPY_BUILTINS_POW3)
228231
#define MICROPY_PY_FSTRINGS (1)
229232
#define MICROPY_MODULE_WEAK_LINKS (0)

py/mpconfig.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,16 @@ typedef long long mp_longint_impl_t;
773773
#define MICROPY_WARNINGS (0)
774774
#endif
775775

776+
// Whether to support chained exceptions
777+
#ifndef MICROPY_CPYTHON_EXCEPTION_CHAIN
778+
#define MICROPY_CPYTHON_EXCEPTION_CHAIN (0)
779+
#endif
780+
781+
// Whether the statically allocated GeneratorExit exception may be const
782+
#ifndef MICROPY_CONST_GENERATOREXIT_OBJ
783+
#define MICROPY_CONST_GENERATOREXIT_OBJ (!MICROPY_CPYTHON_EXCEPTION_CHAIN)
784+
#endif
785+
776786
// Whether to support warning categories
777787
#ifndef MICROPY_WARNINGS_CATEGORY
778788
#define MICROPY_WARNINGS_CATEGORY (0)

py/obj.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,33 @@ void mp_obj_print(mp_obj_t o_in, mp_print_kind_t kind) {
142142
mp_obj_print_helper(MP_PYTHON_PRINTER, o_in, kind);
143143
}
144144

145+
static void mp_obj_print_inner_exception(const mp_print_t *print, mp_obj_t self_in, mp_int_t limit) {
146+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
147+
mp_obj_exception_t *self = mp_obj_exception_get_native(self_in);
148+
const compressed_string_t *msg = MP_ERROR_TEXT("During handling of the above exception, another exception occurred:");
149+
mp_obj_exception_t *inner = NULL;
150+
if (self->cause) {
151+
msg = MP_ERROR_TEXT("The above exception was the direct cause of the following exception:");
152+
inner = self->cause;
153+
} else if (!self->suppress_context) {
154+
inner = self->context;
155+
}
156+
if (inner && !inner->marked) {
157+
inner->marked = true;
158+
mp_obj_print_exception_with_limit(print, MP_OBJ_FROM_PTR(inner), limit);
159+
inner->marked = false;
160+
mp_printf(print, "\n");
161+
mp_cprintf(print, msg);
162+
mp_printf(print, "\n\n");
163+
}
164+
#endif
165+
}
166+
145167
// helper function to print an exception with traceback
146168
void mp_obj_print_exception_with_limit(const mp_print_t *print, mp_obj_t exc, mp_int_t limit) {
147169
if (mp_obj_is_exception_instance(exc) && stack_ok()) {
170+
mp_obj_print_inner_exception(print, exc, limit);
171+
148172
size_t n, *values;
149173
mp_obj_exception_get_traceback(exc, &n, &values);
150174
if (n > 0) {

py/obj.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,9 @@ extern const struct _mp_obj_dict_t mp_const_empty_dict_obj;
791791
extern const struct _mp_obj_traceback_t mp_const_empty_traceback_obj;
792792
extern const struct _mp_obj_singleton_t mp_const_ellipsis_obj;
793793
extern const struct _mp_obj_singleton_t mp_const_notimplemented_obj;
794-
extern const struct _mp_obj_exception_t mp_const_GeneratorExit_obj;
794+
#if MICROPY_CONST_GENERATOREXIT_OBJ
795+
extern const struct _mp_obj_exception_t mp_static_GeneratorExit_obj;
796+
#endif
795797

796798
// Fixed empty map. Useful when calling keyword-receiving functions
797799
// without any keywords from C, etc.

py/objexcept.c

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,45 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
218218
mp_obj_exception_t *self = MP_OBJ_TO_PTR(self_in);
219219
if (dest[0] != MP_OBJ_NULL) {
220220
// store/delete attribute
221-
if (self == &mp_const_GeneratorExit_obj) {
221+
#if MICROPY_CONST_GENERATOREXIT_OBJ
222+
if (self == &mp_static_GeneratorExit_obj) {
222223
mp_raise_AttributeError(MP_ERROR_TEXT("can't set attribute"));
223224
}
225+
#endif
224226
if (attr == MP_QSTR___traceback__) {
225227
if (dest[1] == mp_const_none) {
226228
self->traceback = (mp_obj_traceback_t *)&mp_const_empty_traceback_obj;
227229
} else {
228230
if (!mp_obj_is_type(dest[1], &mp_type_traceback)) {
229-
mp_raise_TypeError(MP_ERROR_TEXT("invalid traceback"));
231+
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), MP_QSTR___context__, MP_QSTR_traceback);
230232
}
231233
self->traceback = MP_OBJ_TO_PTR(dest[1]);
232234
}
233235
dest[0] = MP_OBJ_NULL; // indicate success
236+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
237+
} else if (attr == MP_QSTR___cause__) {
238+
if (dest[1] == mp_const_none) {
239+
self->cause = NULL;
240+
} else if (!mp_obj_is_type(dest[1], &mp_type_BaseException)) {
241+
self->cause = dest[1];
242+
} else {
243+
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), attr, MP_QSTR_BaseException);
244+
}
245+
self->suppress_context = true;
246+
dest[0] = MP_OBJ_NULL; // indicate success
247+
} else if (attr == MP_QSTR___context__) {
248+
if (dest[1] == mp_const_none) {
249+
self->context = NULL;
250+
} else if (!mp_obj_is_type(dest[1], &mp_type_BaseException)) {
251+
self->context = dest[1];
252+
} else {
253+
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), attr, MP_QSTR_BaseException);
254+
}
255+
dest[0] = MP_OBJ_NULL; // indicate success
256+
} else if (attr == MP_QSTR___suppress_context__) {
257+
self->suppress_context = mp_obj_is_true(dest[1]);
258+
dest[0] = MP_OBJ_NULL; // indicate success
259+
#endif
234260
}
235261
return;
236262
}
@@ -240,6 +266,14 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
240266
dest[0] = mp_obj_exception_get_value(self_in);
241267
} else if (attr == MP_QSTR___traceback__) {
242268
dest[0] = (self->traceback) ? MP_OBJ_FROM_PTR(self->traceback) : mp_const_none;
269+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
270+
} else if (attr == MP_QSTR___cause__) {
271+
dest[0] = (self->cause) ? MP_OBJ_FROM_PTR(self->cause) : mp_const_none;
272+
} else if (attr == MP_QSTR___context__) {
273+
dest[0] = (self->context) ? MP_OBJ_FROM_PTR(self->context) : mp_const_none;
274+
} else if (attr == MP_QSTR___suppress_context__) {
275+
dest[0] = mp_obj_new_bool(self->suppress_context);
276+
#endif
243277
#if MICROPY_CPYTHON_COMPAT
244278
} else if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(self->base.type), MP_OBJ_FROM_PTR(&mp_type_OSError))) {
245279
if (attr == MP_QSTR_errno) {

py/objexcept.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ typedef struct _mp_obj_exception_t {
3434
mp_obj_base_t base;
3535
mp_obj_tuple_t *args;
3636
mp_obj_traceback_t *traceback;
37+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
38+
struct _mp_obj_exception_t *cause, *context;
39+
bool suppress_context;
40+
bool marked;
41+
#endif
3742
} mp_obj_exception_t;
3843

3944
void mp_obj_exception_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind);

py/objgenerator.c

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@
3838
#include "supervisor/shared/translate/translate.h"
3939

4040
// Instance of GeneratorExit exception - needed by generator.close()
41-
const mp_obj_exception_t mp_const_GeneratorExit_obj = {{&mp_type_GeneratorExit}, (mp_obj_tuple_t *)&mp_const_empty_tuple_obj, (mp_obj_traceback_t *)&mp_const_empty_traceback_obj};
41+
#if MICROPY_CONST_GENERATOREXIT_OBJ
42+
const
43+
#else
44+
static
45+
#endif
46+
mp_obj_exception_t mp_static_GeneratorExit_obj = {{&mp_type_GeneratorExit}, (mp_obj_tuple_t *)&mp_const_empty_tuple_obj, (mp_obj_traceback_t *)&mp_const_empty_traceback_obj};
4247

4348
/******************************************************************************/
4449
/* generator wrapper */
@@ -362,9 +367,19 @@ STATIC mp_obj_t gen_instance_throw(size_t n_args, const mp_obj_t *args) {
362367
}
363368
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(gen_instance_throw_obj, 2, 4, gen_instance_throw);
364369

370+
static mp_obj_t generatorexit(void) {
371+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
372+
MP_STATIC_ASSERT(!MICROPY_CONST_GENERATOREXIT_OBJ);
373+
mp_static_GeneratorExit_obj.context = NULL;
374+
mp_static_GeneratorExit_obj.cause = NULL;
375+
mp_static_GeneratorExit_obj.suppress_context = false;
376+
#endif
377+
return MP_OBJ_FROM_PTR(&mp_static_GeneratorExit_obj);
378+
}
379+
365380
STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
366381
mp_obj_t ret;
367-
switch (mp_obj_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) {
382+
switch (mp_obj_gen_resume(self_in, mp_const_none, generatorexit(), &ret)) {
368383
case MP_VM_RETURN_YIELD:
369384
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("generator ignored GeneratorExit"));
370385

py/vm.c

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,15 @@
183183
#define TRACE_TICK(current_ip, current_sp, is_exception)
184184
#endif // MICROPY_PY_SYS_SETTRACE
185185

186+
STATIC mp_obj_t get_active_exception(mp_exc_stack_t *exc_sp, mp_exc_stack_t *exc_stack) {
187+
for (mp_exc_stack_t *e = exc_sp; e >= exc_stack; --e) {
188+
if (e->prev_exc != NULL) {
189+
return MP_OBJ_FROM_PTR(e->prev_exc);
190+
}
191+
}
192+
return MP_OBJ_NULL;
193+
}
194+
186195
// fastn has items in reverse order (fastn[0] is local[0], fastn[-1] is local[1], etc)
187196
// sp points to bottom of stack which grows up
188197
// returns:
@@ -1129,13 +1138,7 @@ unwind_jump:;
11291138
ENTRY(MP_BC_RAISE_LAST): {
11301139
MARK_EXC_IP_SELECTIVE();
11311140
// search for the inner-most previous exception, to reraise it
1132-
mp_obj_t obj = MP_OBJ_NULL;
1133-
for (mp_exc_stack_t *e = exc_sp; e >= exc_stack; --e) {
1134-
if (e->prev_exc != NULL) {
1135-
obj = MP_OBJ_FROM_PTR(e->prev_exc);
1136-
break;
1137-
}
1138-
}
1141+
mp_obj_t obj = get_active_exception(exc_sp, exc_stack);
11391142
if (obj == MP_OBJ_NULL) {
11401143
obj = mp_obj_new_exception_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("no active exception to reraise"));
11411144
}
@@ -1145,14 +1148,30 @@ unwind_jump:;
11451148
ENTRY(MP_BC_RAISE_OBJ): {
11461149
MARK_EXC_IP_SELECTIVE();
11471150
mp_obj_t obj = mp_make_raise_obj(TOP());
1151+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
1152+
mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack);
1153+
if (active_exception != MP_OBJ_NULL) {
1154+
mp_store_attr(obj, MP_QSTR___context__, active_exception);
1155+
}
1156+
#endif
11481157
RAISE(obj);
11491158
}
11501159

11511160
ENTRY(MP_BC_RAISE_FROM): {
11521161
MARK_EXC_IP_SELECTIVE();
1153-
mp_warning(NULL, "exception chaining not supported");
1154-
sp--; // ignore (pop) "from" argument
1162+
mp_obj_t cause = POP();
11551163
mp_obj_t obj = mp_make_raise_obj(TOP());
1164+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
1165+
// search for the inner-most previous exception, to chain it
1166+
mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack);
1167+
if (active_exception != MP_OBJ_NULL) {
1168+
mp_store_attr(obj, MP_QSTR___context__, active_exception);
1169+
}
1170+
mp_store_attr(obj, MP_QSTR___cause__, cause);
1171+
#else
1172+
(void)cause;
1173+
mp_warning(NULL, "exception chaining not supported");
1174+
#endif
11561175
RAISE(obj);
11571176
}
11581177

@@ -1391,7 +1410,10 @@ unwind_jump:;
13911410
// - constant GeneratorExit object, because it's const
13921411
// - exceptions re-raised by END_FINALLY
13931412
// - exceptions re-raised explicitly by "raise"
1394-
if (nlr.ret_val != &mp_const_GeneratorExit_obj
1413+
if ( true
1414+
#if MICROPY_CONST_GENERATOREXIT_OBJ
1415+
&& nlr.ret_val != &mp_static_GeneratorExit_obj
1416+
#endif
13951417
&& *code_state->ip != MP_BC_END_FINALLY
13961418
&& *code_state->ip != MP_BC_RAISE_LAST) {
13971419
const byte *ip = code_state->fun_bc->bytecode;
@@ -1434,10 +1456,19 @@ unwind_jump:;
14341456
// catch exception and pass to byte code
14351457
code_state->ip = exc_sp->handler;
14361458
mp_obj_t *sp = MP_TAGPTR_PTR(exc_sp->val_sp);
1459+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
1460+
mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack);
1461+
#endif
14371462
// save this exception in the stack so it can be used in a reraise, if needed
14381463
exc_sp->prev_exc = nlr.ret_val;
1464+
mp_obj_t obj = MP_OBJ_FROM_PTR(nlr.ret_val);
1465+
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
1466+
if (active_exception != MP_OBJ_NULL) {
1467+
mp_store_attr(obj, MP_QSTR___context__, active_exception);
1468+
}
1469+
#endif
14391470
// push exception object so it can be handled by bytecode
1440-
PUSH(MP_OBJ_FROM_PTR(nlr.ret_val));
1471+
PUSH(obj);
14411472
code_state->sp = sp;
14421473

14431474
#if MICROPY_STACKLESS

tests/basics/exception_chain.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,54 @@
1-
# Exception chaining is not supported, but check that basic
2-
# exception works as expected.
31
try:
4-
raise Exception from None
5-
except Exception:
6-
print("Caught Exception")
2+
Exception().__cause__
3+
except AttributeError:
4+
print("SKIP")
5+
raise SystemExit
6+
7+
def print_exc_info(e):
8+
print("exception", type(e), e.args)
9+
print("context", type(e.__context__), e.__suppress_context__)
10+
print("cause", type(e.__cause__))
11+
12+
try:
13+
try:
14+
1/0
15+
except Exception as inner:
16+
raise RuntimeError() from inner
17+
except Exception as e:
18+
print_exc_info(e)
19+
print()
20+
21+
try:
22+
try:
23+
1/0
24+
except Exception as inner:
25+
raise RuntimeError() from OSError()
26+
except Exception as e:
27+
print_exc_info(e)
28+
print()
29+
30+
31+
try:
32+
try:
33+
1/0
34+
except Exception as inner:
35+
raise RuntimeError()
36+
except Exception as e:
37+
print_exc_info(e)
38+
print()
39+
40+
try:
41+
try:
42+
1/0
43+
except Exception as inner:
44+
raise RuntimeError() from None
45+
except Exception as e:
46+
print_exc_info(e)
47+
48+
try:
49+
try:
50+
raise RuntimeError()
51+
except Exception as inner:
52+
1/0
53+
except Exception as e:
54+
print_exc_info(e)

tests/basics/exception_chain.py.exp

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)