diff --git a/docs/project/changelog.md b/docs/project/changelog.md index 7a25d1f1d..d6ae469ce 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -142,7 +142,10 @@ substitutions: - {{ Fix }} Pyodide now loads correctly with `-OO` option. - Add Gitpod configuration to the repository. - {pr} `3201` + {pr}`3201` + +- {{ Enhancement }} Added a type field to `PythonError` + {pr}`3289` ### Build System / Package Loading diff --git a/docs/sphinx_pyodide/sphinx_pyodide/jsdoc.py b/docs/sphinx_pyodide/sphinx_pyodide/jsdoc.py index f5fc08b30..d9a19dfa7 100644 --- a/docs/sphinx_pyodide/sphinx_pyodide/jsdoc.py +++ b/docs/sphinx_pyodide/sphinx_pyodide/jsdoc.py @@ -24,6 +24,19 @@ from sphinx_js.typedoc import Analyzer as TsAnalyzer _orig_convert_node = TsAnalyzer._convert_node _orig_type_name = TsAnalyzer._type_name +_orig_constructor_and_members = TsAnalyzer._constructor_and_members + + +def _constructor_and_members(self, cls): + result = _orig_constructor_and_members(self, cls) + for tag in cls.get("comment", {}).get("tags", []): + if tag["tag"] == "hideconstructor": + return (None, result[1]) + return result + + +TsAnalyzer._constructor_and_members = _constructor_and_members + def destructure_param(param: dict[str, Any]) -> list[dict[str, Any]]: """We want to document a destructured argument as if it were several diff --git a/src/core/error_handling.c b/src/core/error_handling.c index ecb9c38ad..d6612b34d 100644 --- a/src/core/error_handling.c +++ b/src/core/error_handling.c @@ -14,6 +14,7 @@ _Py_IDENTIFIER(format_exception); _Py_IDENTIFIER(last_type); _Py_IDENTIFIER(last_value); _Py_IDENTIFIER(last_traceback); +_Py_IDENTIFIER(__qualname__); void _Py_DumpTraceback(int fd, PyThreadState* tstate); @@ -55,9 +56,16 @@ set_error(PyObject* err) * msg - the Python traceback + error message * err - The error object */ -EM_JS_REF(JsRef, new_error, (const char* msg, PyObject* err), { - return Hiwire.new_value(new API.PythonError(UTF8ToString(msg), err)); +// clang-format off +EM_JS_REF( +JsRef, +new_error, +(const char* type, const char* msg, PyObject* err), +{ + return Hiwire.new_value( + new API.PythonError(UTF8ToString(type), UTF8ToString(msg), err)); }); +// clang-format on /** * Fetch the exception, normalize it, and ensure that traceback is not NULL. @@ -187,16 +195,21 @@ wrap_exception() PyObject* type = NULL; PyObject* value = NULL; PyObject* traceback = NULL; + PyObject* typestr = NULL; PyObject* pystr = NULL; JsRef jserror = NULL; fetch_and_normalize_exception(&type, &value, &traceback); store_sys_last_exception(type, value, traceback); + typestr = _PyObject_GetAttrId(type, &PyId___qualname__); + FAIL_IF_NULL(typestr); + const char* typestr_utf8 = PyUnicode_AsUTF8(typestr); + FAIL_IF_NULL(typestr_utf8); pystr = format_exception_traceback(type, value, traceback); FAIL_IF_NULL(pystr); const char* pystr_utf8 = PyUnicode_AsUTF8(pystr); FAIL_IF_NULL(pystr_utf8); - jserror = new_error(pystr_utf8, value); + jserror = new_error(typestr_utf8, pystr_utf8, value); FAIL_IF_NULL(jserror); success = true; @@ -210,7 +223,8 @@ finally: PySys_WriteStderr("\nOriginal exception was:\n"); PyErr_Display(type, value, traceback); } - jserror = new_error("Error occurred while formatting traceback", 0); + jserror = new_error( + "PyodideInternalError", "Error occurred while formatting traceback", 0); } Py_CLEAR(type); Py_CLEAR(value); diff --git a/src/core/error_handling.ts b/src/core/error_handling.ts index 73a8391a8..eb8ccc1ff 100644 --- a/src/core/error_handling.ts +++ b/src/core/error_handling.ts @@ -260,7 +260,7 @@ Module.handle_js_error = function (e: any) { * * See :ref:`type-translations-errors` for more information. * - * .. admonition:: Avoid Stack Frames + * .. admonition:: Avoid leaking stack Frames * :class: warning * * If you make a :any:`PyProxy` of ``sys.last_value``, you should be @@ -268,6 +268,8 @@ Module.handle_js_error = function (e: any) { * done. You may leak a large amount of memory including the local * variables of all the stack frames in the traceback if you don't. The * easiest way is to only handle the exception in Python. + * + * @hideconstructor */ export class PythonError extends Error { /** The address of the error we are wrapping. We may later compare this @@ -277,12 +279,16 @@ export class PythonError extends Error { * @private */ __error_address: number; - - constructor(message: string, error_address: number) { + /** + * The Python type, e.g, ``RuntimeError`` or ``KeyError``. + */ + type: string; + constructor(type: string, message: string, error_address: number) { const oldLimit = Error.stackTraceLimit; Error.stackTraceLimit = Infinity; super(message); Error.stackTraceLimit = oldLimit; + this.type = type; this.__error_address = error_address; } } diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py index b3d22eff1..dca47e637 100644 --- a/src/tests/test_pyodide.py +++ b/src/tests/test_pyodide.py @@ -1554,3 +1554,17 @@ def test_static_import( `); """ ) + + +def test_python_error(selenium): + [msg, ty] = selenium.run_js( + """ + try { + pyodide.runPython("raise TypeError('oops')"); + } catch(e) { + return [e.message, e.type]; + } + """ + ) + assert msg.endswith("TypeError: oops\n") + assert ty == "TypeError"