Skip to content

Commit 1082616

Browse files
committed
Implement chained exceptions
This adds the __cause__, __context__ and __suppress_context__ members to exception objects and makes e.g., `raise exc from cause` set them in the same way as standard Python.
1 parent a5d64df commit 1082616

File tree

8 files changed

+118
-19
lines changed

8 files changed

+118
-19
lines changed

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/objexcept.c

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,35 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
228228
self->traceback = (mp_obj_traceback_t *)&mp_const_empty_traceback_obj;
229229
} else {
230230
if (!mp_obj_is_type(dest[1], &mp_type_traceback)) {
231-
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);
232232
}
233233
self->traceback = MP_OBJ_TO_PTR(dest[1]);
234234
}
235235
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
236260
}
237261
return;
238262
}
@@ -242,6 +266,14 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
242266
dest[0] = mp_obj_exception_get_value(self_in);
243267
} else if (attr == MP_QSTR___traceback__) {
244268
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
245277
#if MICROPY_CPYTHON_COMPAT
246278
} else if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(self->base.type), MP_OBJ_FROM_PTR(&mp_type_OSError))) {
247279
if (attr == MP_QSTR_errno) {

py/objexcept.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ 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+
mp_obj_t cause, context;
38+
bool suppress_context;
3739
} mp_obj_exception_t;
3840

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

py/vm.c

Lines changed: 28 additions & 9 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

tests/basics/exception_chain.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,46 @@
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), repr(e))
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)

tests/basics/exception_chain.py.exp

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

tests/run-tests.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,8 +522,12 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1):
522522
skip_tests.add("basics/scope_implicit.py") # requires checking for unbound local
523523
skip_tests.add("basics/try_finally_return2.py") # requires raise_varargs
524524
skip_tests.add("basics/unboundlocal.py") # requires checking for unbound local
525-
skip_tests.add(
526-
"circuitpython/traceback_test.py"
525+
skip_tests.update(
526+
(
527+
"basics/chained_exception.py",
528+
"circuitpython/traceback_test.py",
529+
"circuitpython/traceback_test_chained.py",
530+
)
527531
) # because native doesn't have proper traceback info
528532
skip_tests.add("extmod/uasyncio_event.py") # unknown issue
529533
skip_tests.add("extmod/uasyncio_lock.py") # requires async with

0 commit comments

Comments
 (0)