Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MRG: use Matplotlib to detect new Matplotlib figures #1401

Merged
merged 15 commits into from
Jun 27, 2023
Merged
46 changes: 20 additions & 26 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
# reticulate (development version)

- New optional feature: Reticulate now accepts a new option `jupyter_compat`
set to `FALSE` by default, that changes the default expression output display
behavior of Reticulate chunks, to better match the behavior of Jupyter. In
the Reticulate default, each standalone code expression in the code chunk
that does not end in a semi-colon, generates display of the expression
output. With the `jupyter_compat` option set, no expression in the chunk will
generate output, except if there is a standalone expression as the last code
statement in the chunk, and that expression does not have a semicolon.
A semicolon always suppresses the expression output, for the default and
`jupyter_compat` case. See
[PR](https://github.com/rstudio/reticulate/pull/1394) and [original
issue](https://github.com/rstudio/reticulate/issues/1391) for discussion for
this and the next item.

- Behavior change: Previously, a Matplotlib plot would only be automatically
displayed (without `plt.show()`) if there was a final standalone expression
returning a Matplotlib object, and that expression did not have a final
semicolon. With this update, any standalone expression returning
a Matplotlib object, with or without a semicolon, will cause chunk to display
the plot automatically. See above for discussion.

- Fix: the knitr engine now automatically calls `plt.show()` for matplotlib
bar plots, like it does for other matplotlib plot types (#1391).

- Updated sparse matrix conversion routines for compatibility with scipy 1.11.0.

- The knitr engine gains a `jupyter_compat` option, enabling
reticulate to better match the behavior of Jupyter. When this chunk
option is set to `TRUE`, only the return value from the last
expression in a chunk is auto-printed. (#1391, #1394, contributed by
@matthew-brett)

- The knitr engine now more reliably detects and displays matplotlib
pending plots, without the need for a matplotlib artist object to be
returned as a top-level expression. E.g., the knitr engine will now
display plots when the matplotlib api returns something other than
an artist object, (`plt.bar()`), or the matplotlib return value is
not auto-printed due to being assigned, (`x = plt.plot()`), or
suppressed with a `;`, (`plt.plot();`). (#1391, #1401, contributed
by @matthew-brett)

- Fixed an issue where knitr engine would not respect chunk options
`fig.width` / `fig.height` when rendering matplotlib plots. (#1398)

- Updated sparse matrix conversion routines for compatibility with
scipy 1.11.0.

# reticulate 1.30

Expand Down
58 changes: 29 additions & 29 deletions R/knitr-engine.R
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ eng_python <- function(options) {
outputs_target <- if (is_hold) held_outputs else outputs

# synchronize state R -> Python
eng_python_synchronize_before()
eng_python_synchronize_before(options)

# determine if we should capture errors
# (don't capture errors during knit)
Expand All @@ -184,9 +184,6 @@ eng_python <- function(options) {
}, add = TRUE)
}

# Flag to signal plt command called, but not yet shown.
.engine_context$matplotlib_pending_show <- FALSE

for (i in seq_along(ranges)) {

# extract range
Expand All @@ -205,18 +202,16 @@ eng_python <- function(options) {
else
py_compile_eval(snippet, 'single')

# handle matplotlib and other plot output
# handle matplotlib plots and other special output
captured <- eng_python_autoprint(
captured = captured,
options = options
)

# In all modes, code statements ending in semicolons always suppress repr
# output. In jupyter_compat mode, also suppress repr output for all
# but the final expression.
if ((grepl(";\\s*$", snippet)) | (jupyter_compat & !last_range)) {
captured = ""
}
# A trailing ';' suppresses output.
# In jupyter mode, only the last expression in a chunk has repr() output.
if (grepl(";\\s*$", snippet) | (jupyter_compat & !last_range))
captured <- ""

# emit outputs if we have any
has_outputs <-
Expand Down Expand Up @@ -275,13 +270,19 @@ eng_python <- function(options) {
outputs$push(output)
}

if (.engine_context$matplotlib_pending_show & is_include) {
plt <- import("matplotlib.pyplot", convert = TRUE)
plt$show()
for (plot in .engine_context$pending_plots$data())
outputs_target$push(plot)
# check if we need to call matplotlib.pyplot.show()
# for any pending undisplayed plots
if(isTRUE(.globals$matplotlib_initialized)) {
plt <- import("matplotlib.pyplot")
if(length(plt$get_fignums()))
plt$show()
}

for (plot in .engine_context$pending_plots$data())
outputs_target$push(plot)
.engine_context$pending_plots$clear()


# if we were using held outputs, we just inject the source in now
if (is_hold) {
output <- structure(list(src = code), class = "source")
Expand Down Expand Up @@ -377,7 +378,7 @@ eng_python_matplotlib_show <- function(plt, options) {
# save the current figure
dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE)
plt$savefig(path, dpi = options$dpi)
plt$clf()
plt$close()

# include the requested path
knitr::include_graphics(path)
Expand Down Expand Up @@ -415,12 +416,11 @@ eng_python_initialize_hooks <- function(options, envir) {

eng_python_initialize_matplotlib <- function(options, envir) {

# mark initialization done
# early exit if we already initialized
# (this onload hook is registered for multiple matplotlib submodules)
if (identical(.globals$matplotlib_initialized, TRUE))
return(TRUE)

.globals$matplotlib_initialized <- TRUE

# attempt to enforce a non-Qt matplotlib backend. this is especially important
# with RStudio Desktop as attempting to use a Qt backend will cause issues due
# to mismatched Qt versions between RStudio and Anaconda environments, and
Expand Down Expand Up @@ -455,8 +455,6 @@ eng_python_initialize_matplotlib <- function(options, envir) {
# override show implementation
plt$show <- function(...) {

.engine_context$matplotlib_pending_show = FALSE

# get current chunk options
options <- knitr::opts_current$get()

Expand All @@ -473,8 +471,7 @@ eng_python_initialize_matplotlib <- function(options, envir) {

}

# set up figure dimensions
plt$rc("figure", figsize = tuple(options$fig.width, options$fig.height))
.globals$matplotlib_initialized <- TRUE

}

Expand All @@ -498,8 +495,14 @@ eng_python_initialize_plotly <- function(options, envir) {
}

# synchronize objects R -> Python
eng_python_synchronize_before <- function() {
eng_python_synchronize_before <- function(options) {
py_inject_r()
if(isTRUE(.globals$matplotlib_initialized)) {

# set up figure dimensions
plt <- import("matplotlib.pyplot")
plt$rc("figure", figsize = tuple(options$fig.width, options$fig.height))
}
}

# synchronize objects Python -> R
Expand Down Expand Up @@ -603,10 +606,7 @@ eng_python_autoprint <- function(captured, options) {
isHtml <- knitr::is_html_output()

if (eng_python_is_matplotlib_output(value)) {

# Handle matplotlib output. Note that the default hook for plt.show
# installed by reticulate will update the 'pending_plots' item.
.engine_context$matplotlib_pending_show <- TRUE
# We handle pending Matplotlib plots with fignums check later.

# Always suppress Matplotlib reprs
return("")
Expand Down
16 changes: 15 additions & 1 deletion tests/testthat/resources/eng-reticulate-example.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ plt.plot([1, 2, 3, 4])
plt.show()
```


```{python, fig.width=8, fig.height=3, dev="svg"}
import matplotlib.pyplot as plt

plt.plot(range(10))
```

```{python, fig.width=8, fig.height=3, dev="svg"}
import matplotlib.pyplot as plt

plt.hist(range(10))
```


Python can access objects available in the R environment.

```{r}
Expand Down Expand Up @@ -81,7 +95,7 @@ print(y)
- #130: The `echo` chunk option is respected (for `TRUE` and `FALSE` values). Output, but not source, should show in the following output.

```{python echo=FALSE}
print ("Chunk with echo = FALSE")
print("Chunk with echo = FALSE")
```

- #130: The `results` chunk option is respected for Python outputs. Source, but not output, should show in the following output.
Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test-python-knitr-engine.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test_that("An R Markdown document can be rendered using reticulate", {
}
}

owd <- setwd("resources")
owd <- setwd(test_path("resources"))
status <- rmarkdown::render("eng-reticulate-example.Rmd", quiet = TRUE)
setwd(owd)

Expand Down
Loading