diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 09c7ed61bd..3958d2869a 100755 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -175,7 +175,7 @@ msgstr "" msgid "%q must be of type %q" msgstr "" -#: shared-bindings/digitalio/Pull.c +#: py/objexcept.c shared-bindings/digitalio/Pull.c msgid "%q must be of type %q or None" msgstr "" @@ -894,6 +894,10 @@ msgstr "" msgid "Drive mode not used when direction is input." msgstr "" +#: py/obj.c +msgid "During handling of the above exception, another exception occurred:" +msgstr "" + #: shared-bindings/aesio/aes.c msgid "ECB only operates on 16 bytes at a time" msgstr "" @@ -2011,6 +2015,10 @@ msgid "" "exit safe mode." msgstr "" +#: py/obj.c +msgid "The above exception was the direct cause of the following exception:" +msgstr "" + #: ports/espressif/boards/m5stack_atom_lite/mpconfigboard.h msgid "The central button was pressed at start up.\n" msgstr "" @@ -3341,10 +3349,6 @@ msgstr "" msgid "invalid syntax for number" msgstr "" -#: py/objexcept.c -msgid "invalid traceback" -msgstr "" - #: py/objtype.c msgid "issubclass() arg 1 must be a class" msgstr "" diff --git a/ports/unix/variants/coverage/mpconfigvariant.h b/ports/unix/variants/coverage/mpconfigvariant.h index f6ebc2087b..ba9e941c28 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.h +++ b/ports/unix/variants/coverage/mpconfigvariant.h @@ -65,6 +65,7 @@ #define MICROPY_PY_UCRYPTOLIB (1) #define MICROPY_PY_UCRYPTOLIB_CTR (1) #define MICROPY_PY_MICROPYTHON_HEAP_LOCKED (1) +#define MICROPY_CPYTHON_EXCEPTION_CHAIN (1) // use vfs's functions for import stat and builtin open #define mp_import_stat mp_vfs_import_stat diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index 6ae8275058..d6d8419c8f 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -224,6 +224,9 @@ typedef long mp_off_t; #ifndef MICROPY_CPYTHON_COMPAT #define MICROPY_CPYTHON_COMPAT (CIRCUITPY_FULL_BUILD) #endif +#ifndef MICROPY_CPYTHON_EXCEPTION_CHAIN +#define MICROPY_CPYTHON_EXCEPTION_CHAIN (CIRCUITPY_FULL_BUILD) +#endif #define MICROPY_PY_BUILTINS_POW3 (CIRCUITPY_BUILTINS_POW3) #define MICROPY_PY_FSTRINGS (1) #define MICROPY_MODULE_WEAK_LINKS (0) diff --git a/py/mpconfig.h b/py/mpconfig.h index 386a9765eb..9d68f4ce9d 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -773,6 +773,16 @@ typedef long long mp_longint_impl_t; #define MICROPY_WARNINGS (0) #endif +// Whether to support chained exceptions +#ifndef MICROPY_CPYTHON_EXCEPTION_CHAIN +#define MICROPY_CPYTHON_EXCEPTION_CHAIN (0) +#endif + +// Whether the statically allocated GeneratorExit exception may be const +#ifndef MICROPY_CONST_GENERATOREXIT_OBJ +#define MICROPY_CONST_GENERATOREXIT_OBJ (!MICROPY_CPYTHON_EXCEPTION_CHAIN) +#endif + // Whether to support warning categories #ifndef MICROPY_WARNINGS_CATEGORY #define MICROPY_WARNINGS_CATEGORY (0) diff --git a/py/obj.c b/py/obj.c index af2f4c1b68..72a0647176 100644 --- a/py/obj.c +++ b/py/obj.c @@ -142,9 +142,33 @@ void mp_obj_print(mp_obj_t o_in, mp_print_kind_t kind) { mp_obj_print_helper(MP_PYTHON_PRINTER, o_in, kind); } +static void mp_obj_print_inner_exception(const mp_print_t *print, mp_obj_t self_in, mp_int_t limit) { + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + mp_obj_exception_t *self = mp_obj_exception_get_native(self_in); + const compressed_string_t *msg = MP_ERROR_TEXT("During handling of the above exception, another exception occurred:"); + mp_obj_exception_t *inner = NULL; + if (self->cause) { + msg = MP_ERROR_TEXT("The above exception was the direct cause of the following exception:"); + inner = self->cause; + } else if (!self->suppress_context) { + inner = self->context; + } + if (inner && !inner->marked) { + inner->marked = true; + mp_obj_print_exception_with_limit(print, MP_OBJ_FROM_PTR(inner), limit); + inner->marked = false; + mp_printf(print, "\n"); + mp_cprintf(print, msg); + mp_printf(print, "\n\n"); + } + #endif +} + // helper function to print an exception with traceback void mp_obj_print_exception_with_limit(const mp_print_t *print, mp_obj_t exc, mp_int_t limit) { if (mp_obj_is_exception_instance(exc) && stack_ok()) { + mp_obj_print_inner_exception(print, exc, limit); + size_t n, *values; mp_obj_exception_get_traceback(exc, &n, &values); if (n > 0) { diff --git a/py/obj.h b/py/obj.h index 7fa21f5e38..b7e76a1106 100644 --- a/py/obj.h +++ b/py/obj.h @@ -791,7 +791,9 @@ extern const struct _mp_obj_dict_t mp_const_empty_dict_obj; extern const struct _mp_obj_traceback_t mp_const_empty_traceback_obj; extern const struct _mp_obj_singleton_t mp_const_ellipsis_obj; extern const struct _mp_obj_singleton_t mp_const_notimplemented_obj; -extern const struct _mp_obj_exception_t mp_const_GeneratorExit_obj; +#if MICROPY_CONST_GENERATOREXIT_OBJ +extern const struct _mp_obj_exception_t mp_static_GeneratorExit_obj; +#endif // Fixed empty map. Useful when calling keyword-receiving functions // without any keywords from C, etc. diff --git a/py/objexcept.c b/py/objexcept.c index 32dacbb17a..80da487076 100644 --- a/py/objexcept.c +++ b/py/objexcept.c @@ -218,19 +218,45 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { mp_obj_exception_t *self = MP_OBJ_TO_PTR(self_in); if (dest[0] != MP_OBJ_NULL) { // store/delete attribute - if (self == &mp_const_GeneratorExit_obj) { + #if MICROPY_CONST_GENERATOREXIT_OBJ + if (self == &mp_static_GeneratorExit_obj) { mp_raise_AttributeError(MP_ERROR_TEXT("can't set attribute")); } + #endif if (attr == MP_QSTR___traceback__) { if (dest[1] == mp_const_none) { self->traceback = (mp_obj_traceback_t *)&mp_const_empty_traceback_obj; } else { if (!mp_obj_is_type(dest[1], &mp_type_traceback)) { - mp_raise_TypeError(MP_ERROR_TEXT("invalid traceback")); + mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), MP_QSTR___context__, MP_QSTR_traceback); } self->traceback = MP_OBJ_TO_PTR(dest[1]); } dest[0] = MP_OBJ_NULL; // indicate success + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + } else if (attr == MP_QSTR___cause__) { + if (dest[1] == mp_const_none) { + self->cause = NULL; + } else if (!mp_obj_is_type(dest[1], &mp_type_BaseException)) { + self->cause = dest[1]; + } else { + mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), attr, MP_QSTR_BaseException); + } + self->suppress_context = true; + dest[0] = MP_OBJ_NULL; // indicate success + } else if (attr == MP_QSTR___context__) { + if (dest[1] == mp_const_none) { + self->context = NULL; + } else if (!mp_obj_is_type(dest[1], &mp_type_BaseException)) { + self->context = dest[1]; + } else { + mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), attr, MP_QSTR_BaseException); + } + dest[0] = MP_OBJ_NULL; // indicate success + } else if (attr == MP_QSTR___suppress_context__) { + self->suppress_context = mp_obj_is_true(dest[1]); + dest[0] = MP_OBJ_NULL; // indicate success + #endif } return; } @@ -240,6 +266,14 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { dest[0] = mp_obj_exception_get_value(self_in); } else if (attr == MP_QSTR___traceback__) { dest[0] = (self->traceback) ? MP_OBJ_FROM_PTR(self->traceback) : mp_const_none; + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + } else if (attr == MP_QSTR___cause__) { + dest[0] = (self->cause) ? MP_OBJ_FROM_PTR(self->cause) : mp_const_none; + } else if (attr == MP_QSTR___context__) { + dest[0] = (self->context) ? MP_OBJ_FROM_PTR(self->context) : mp_const_none; + } else if (attr == MP_QSTR___suppress_context__) { + dest[0] = mp_obj_new_bool(self->suppress_context); + #endif #if MICROPY_CPYTHON_COMPAT } else if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(self->base.type), MP_OBJ_FROM_PTR(&mp_type_OSError))) { if (attr == MP_QSTR_errno) { diff --git a/py/objexcept.h b/py/objexcept.h index f28f50f5dc..77b338e951 100644 --- a/py/objexcept.h +++ b/py/objexcept.h @@ -34,6 +34,11 @@ typedef struct _mp_obj_exception_t { mp_obj_base_t base; mp_obj_tuple_t *args; mp_obj_traceback_t *traceback; + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + struct _mp_obj_exception_t *cause, *context; + bool suppress_context; + bool marked; + #endif } mp_obj_exception_t; void mp_obj_exception_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind); diff --git a/py/objgenerator.c b/py/objgenerator.c index c9af11fbb5..2256911e23 100644 --- a/py/objgenerator.c +++ b/py/objgenerator.c @@ -38,7 +38,12 @@ #include "supervisor/shared/translate/translate.h" // Instance of GeneratorExit exception - needed by generator.close() -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}; +#if MICROPY_CONST_GENERATOREXIT_OBJ +const +#else +static +#endif +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}; /******************************************************************************/ /* generator wrapper */ @@ -362,9 +367,19 @@ STATIC mp_obj_t gen_instance_throw(size_t n_args, const mp_obj_t *args) { } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(gen_instance_throw_obj, 2, 4, gen_instance_throw); +static mp_obj_t generatorexit(void) { + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + MP_STATIC_ASSERT(!MICROPY_CONST_GENERATOREXIT_OBJ); + mp_static_GeneratorExit_obj.context = NULL; + mp_static_GeneratorExit_obj.cause = NULL; + mp_static_GeneratorExit_obj.suppress_context = false; + #endif + return MP_OBJ_FROM_PTR(&mp_static_GeneratorExit_obj); +} + STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) { mp_obj_t ret; - switch (mp_obj_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) { + switch (mp_obj_gen_resume(self_in, mp_const_none, generatorexit(), &ret)) { case MP_VM_RETURN_YIELD: mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("generator ignored GeneratorExit")); diff --git a/py/vm.c b/py/vm.c index 12a0f2d445..9e9dbcaa8d 100644 --- a/py/vm.c +++ b/py/vm.c @@ -183,6 +183,15 @@ #define TRACE_TICK(current_ip, current_sp, is_exception) #endif // MICROPY_PY_SYS_SETTRACE +STATIC mp_obj_t get_active_exception(mp_exc_stack_t *exc_sp, mp_exc_stack_t *exc_stack) { + for (mp_exc_stack_t *e = exc_sp; e >= exc_stack; --e) { + if (e->prev_exc != NULL) { + return MP_OBJ_FROM_PTR(e->prev_exc); + } + } + return MP_OBJ_NULL; +} + // fastn has items in reverse order (fastn[0] is local[0], fastn[-1] is local[1], etc) // sp points to bottom of stack which grows up // returns: @@ -1129,13 +1138,7 @@ unwind_return: ENTRY(MP_BC_RAISE_LAST): { MARK_EXC_IP_SELECTIVE(); // search for the inner-most previous exception, to reraise it - mp_obj_t obj = MP_OBJ_NULL; - for (mp_exc_stack_t *e = exc_sp; e >= exc_stack; --e) { - if (e->prev_exc != NULL) { - obj = MP_OBJ_FROM_PTR(e->prev_exc); - break; - } - } + mp_obj_t obj = get_active_exception(exc_sp, exc_stack); if (obj == MP_OBJ_NULL) { obj = mp_obj_new_exception_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("no active exception to reraise")); } @@ -1145,14 +1148,30 @@ unwind_return: ENTRY(MP_BC_RAISE_OBJ): { MARK_EXC_IP_SELECTIVE(); mp_obj_t obj = mp_make_raise_obj(TOP()); + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack); + if (active_exception != MP_OBJ_NULL) { + mp_store_attr(obj, MP_QSTR___context__, active_exception); + } + #endif RAISE(obj); } ENTRY(MP_BC_RAISE_FROM): { MARK_EXC_IP_SELECTIVE(); - mp_warning(NULL, "exception chaining not supported"); - sp--; // ignore (pop) "from" argument + mp_obj_t cause = POP(); mp_obj_t obj = mp_make_raise_obj(TOP()); + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + // search for the inner-most previous exception, to chain it + mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack); + if (active_exception != MP_OBJ_NULL) { + mp_store_attr(obj, MP_QSTR___context__, active_exception); + } + mp_store_attr(obj, MP_QSTR___cause__, cause); + #else + (void)cause; + mp_warning(NULL, "exception chaining not supported"); + #endif RAISE(obj); } @@ -1391,7 +1410,10 @@ unwind_loop: // - constant GeneratorExit object, because it's const // - exceptions re-raised by END_FINALLY // - exceptions re-raised explicitly by "raise" - if (nlr.ret_val != &mp_const_GeneratorExit_obj + if ( true + #if MICROPY_CONST_GENERATOREXIT_OBJ + && nlr.ret_val != &mp_static_GeneratorExit_obj + #endif && *code_state->ip != MP_BC_END_FINALLY && *code_state->ip != MP_BC_RAISE_LAST) { const byte *ip = code_state->fun_bc->bytecode; @@ -1434,10 +1456,19 @@ unwind_loop: // catch exception and pass to byte code code_state->ip = exc_sp->handler; mp_obj_t *sp = MP_TAGPTR_PTR(exc_sp->val_sp); + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack); + #endif // save this exception in the stack so it can be used in a reraise, if needed exc_sp->prev_exc = nlr.ret_val; + mp_obj_t obj = MP_OBJ_FROM_PTR(nlr.ret_val); + #if MICROPY_CPYTHON_EXCEPTION_CHAIN + if (active_exception != MP_OBJ_NULL) { + mp_store_attr(obj, MP_QSTR___context__, active_exception); + } + #endif // push exception object so it can be handled by bytecode - PUSH(MP_OBJ_FROM_PTR(nlr.ret_val)); + PUSH(obj); code_state->sp = sp; #if MICROPY_STACKLESS diff --git a/tests/basics/exception_chain.py b/tests/basics/exception_chain.py index c3a7d6b113..b84ff19dd6 100644 --- a/tests/basics/exception_chain.py +++ b/tests/basics/exception_chain.py @@ -1,6 +1,54 @@ -# Exception chaining is not supported, but check that basic -# exception works as expected. try: - raise Exception from None -except Exception: - print("Caught Exception") + Exception().__cause__ +except AttributeError: + print("SKIP") + raise SystemExit + +def print_exc_info(e): + print("exception", type(e), e.args) + print("context", type(e.__context__), e.__suppress_context__) + print("cause", type(e.__cause__)) + +try: + try: + 1/0 + except Exception as inner: + raise RuntimeError() from inner +except Exception as e: + print_exc_info(e) +print() + +try: + try: + 1/0 + except Exception as inner: + raise RuntimeError() from OSError() +except Exception as e: + print_exc_info(e) +print() + + +try: + try: + 1/0 + except Exception as inner: + raise RuntimeError() +except Exception as e: + print_exc_info(e) +print() + +try: + try: + 1/0 + except Exception as inner: + raise RuntimeError() from None +except Exception as e: + print_exc_info(e) + +try: + try: + raise RuntimeError() + except Exception as inner: + 1/0 +except Exception as e: + print_exc_info(e) diff --git a/tests/basics/exception_chain.py.exp b/tests/basics/exception_chain.py.exp deleted file mode 100644 index 13635b3cde..0000000000 --- a/tests/basics/exception_chain.py.exp +++ /dev/null @@ -1,2 +0,0 @@ -Warning: exception chaining not supported -Caught Exception diff --git a/tests/circuitpython/traceback_test_chained.py b/tests/circuitpython/traceback_test_chained.py new file mode 100644 index 0000000000..83d0c03466 --- /dev/null +++ b/tests/circuitpython/traceback_test_chained.py @@ -0,0 +1,63 @@ +try: + Exception().__cause__ +except AttributeError: + print("SKIP") + raise SystemExit + +try: + import traceback +except: + print("SKIP") + raise SystemExit + + +def print_exc_info(e): + print("-" * 72) + traceback.print_exception(None, e, e.__traceback__) + print("-" * 72) + print() + + +try: + try: + 1 / 0 + except Exception as inner: + raise RuntimeError() from inner +except Exception as e: + print_exc_info(e) +print() + +try: + try: + 1 / 0 + except Exception as inner: + raise RuntimeError() from OSError() +except Exception as e: + print_exc_info(e) +print() + + +try: + try: + 1 / 0 + except Exception as inner: + raise RuntimeError() +except Exception as e: + print_exc_info(e) +print() + +try: + try: + 1 / 0 + except Exception as inner: + raise RuntimeError() from None +except Exception as e: + print_exc_info(e) + +try: + try: + raise RuntimeError() + except Exception as inner: + 1 / 0 +except Exception as e: + print_exc_info(e) diff --git a/tests/circuitpython/traceback_test_chained.py.exp b/tests/circuitpython/traceback_test_chained.py.exp new file mode 100644 index 0000000000..c874ff707f --- /dev/null +++ b/tests/circuitpython/traceback_test_chained.py.exp @@ -0,0 +1,55 @@ +------------------------------------------------------------------------ +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 23, in +ZeroDivisionError: division by zero + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 25, in +RuntimeError: +------------------------------------------------------------------------ + + +------------------------------------------------------------------------ +OSError: + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 34, in +RuntimeError: +------------------------------------------------------------------------ + + +------------------------------------------------------------------------ +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 42, in +ZeroDivisionError: division by zero + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 44, in +RuntimeError: +------------------------------------------------------------------------ + + +------------------------------------------------------------------------ +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 53, in +RuntimeError: +------------------------------------------------------------------------ + +------------------------------------------------------------------------ +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 59, in +RuntimeError: + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "circuitpython/traceback_test_chained.py", line 61, in +ZeroDivisionError: division by zero +------------------------------------------------------------------------ + diff --git a/tests/run-tests.py b/tests/run-tests.py index ed0cf18e45..7a7b3adbcb 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -522,8 +522,12 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("basics/scope_implicit.py") # requires checking for unbound local skip_tests.add("basics/try_finally_return2.py") # requires raise_varargs skip_tests.add("basics/unboundlocal.py") # requires checking for unbound local - skip_tests.add( - "circuitpython/traceback_test.py" + skip_tests.update( + ( + "basics/chained_exception.py", + "circuitpython/traceback_test.py", + "circuitpython/traceback_test_chained.py", + ) ) # because native doesn't have proper traceback info skip_tests.add("extmod/uasyncio_event.py") # unknown issue skip_tests.add("extmod/uasyncio_lock.py") # requires async with