From 9580b0311e2575da8bad99843625fc0f51ad19af Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Mon, 29 Jan 2024 09:09:23 -0500 Subject: [PATCH 1/3] knitr eng: format captured exceptions Closes #1520 --- R/knitr-engine.R | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/R/knitr-engine.R b/R/knitr-engine.R index 3e9951fa9..b0f5c59f1 100644 --- a/R/knitr-engine.R +++ b/R/knitr-engine.R @@ -252,8 +252,25 @@ eng_python <- function(options) { compile_mode <- if (suppress) "exec" else "single" # run code and capture output - captured_stdout <- if (capture_errors) - tryCatch(py_compile_eval(snippet, compile_mode), error = identity) + captured_stdout <- if (capture_errors) { + tryCatch( + py_compile_eval(snippet, compile_mode), + error = function(e) { + + # if the chunk option is error = FALSE (the default). + # we'll need to bail and not evaluate to the next python expression. + if (identical(options$error, FALSE)) + had_error <- TRUE + + # format the exception object + etype <- py_get_attr(e, "__class__") + traceback <- import("traceback") + paste0(traceback$format_exception_only(etype, e), + collapse = "") + } + ) + + } else py_compile_eval(snippet, compile_mode) @@ -304,10 +321,9 @@ eng_python <- function(options) { pending_source_index <- range[2] + 1 # bail if we had an error with 'error=FALSE' - if (identical(options$error, FALSE) && inherits(captured, "error")) { - had_error <- TRUE + if (had_error && identical(options$error, FALSE)) break - } + } } From 59a0d01b418ce8a576079dc1934ae46c37315018 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Mon, 29 Jan 2024 09:10:57 -0500 Subject: [PATCH 2/3] add NEWS --- NEWS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index c122adfa4..66fb4ce69 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # reticulate (development version) +- The knitr python engine now formats captured python exceptions to include the + exception type and any exception notes when chunk options + `error = TRUE` is set (reported in #1520, fixed in #1527). + - R external pointers (EXTPTRSXP objects) now round-trip through `py_to_r(r_to_py(x))` successfully. (reported in #1511, fixed in #1519, contributed by @llaniewski). @@ -7,7 +11,8 @@ - Fixed issue where `virtualenv_create()` would error on Ubuntu 22.04 when using the system python as a base. (#1495, fixed in #1496). -- Fixed issue where `csc_matrix` objects with unsorted indices could not be converted to a dgCMatrix. (related to #727, fixed in #1524, contributed by @rcannood). +- Fixed issue where `csc_matrix` objects with unsorted indices could not be + converted to a dgCMatrix. (related to #727, fixed in #1524, contributed by @rcannood). # reticulate 1.34.0 From afee13b142bd26812ab10451cf119f8361ee61a6 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Mon, 29 Jan 2024 10:00:42 -0500 Subject: [PATCH 3/3] Add tests --- .../resources/eng-reticulate-example.Rmd | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/testthat/resources/eng-reticulate-example.Rmd b/tests/testthat/resources/eng-reticulate-example.Rmd index d5da7ff4b..7c0f8dc4d 100644 --- a/tests/testthat/resources/eng-reticulate-example.Rmd +++ b/tests/testthat/resources/eng-reticulate-example.Rmd @@ -8,15 +8,20 @@ library(reticulate) knitr::knit_engines$set(python = eng_python) # use specific environment if available -python <- "~/.virtualenvs/python-3.7.7-venv/bin/python" -if (file.exists(python)) - reticulate::use_python(python, required = TRUE) +# python <- "~/.virtualenvs/python-3.7.7-venv/bin/python" +# if (file.exists(python)) +# reticulate::use_python(python, required = TRUE) # use rlang error handler if (requireNamespace("rlang", quietly = TRUE)) options(error = rlang::entrace) ``` +```{r} +reticulate::py_config() +``` + + Python variables live across chunk boundaries. ```{python} @@ -129,6 +134,25 @@ raise RuntimeError("oops!") print("This line is still reached.") ``` +Ensure that Exceptions are formatted w/ the type and notes. +```{python, error=TRUE} +class CustomException(Exception): + pass + +e = CustomException("oops!") + +# BaseException.add_note() added in Python 3.11 +import sys +if sys.version_info >= (3, 11): + e.add_note("note 1: extra oopsy oops") + e.add_note("note 2: in the future avoid oopsies") +else: + print(sys.version_info) + +raise e +print("This line is still reached.") +``` + Ensure that lines with multiple statements are only run once. ```{python}