Skip to content

Commit

Permalink
Move all plot watching logic into watchout (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
hadley authored Jun 19, 2024
1 parent 8af29ec commit fe54e17
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 116 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# evaluate (development version)

* `watchout()` is no longer exported; it's really an implementation detail that should never have been leaked to the public interface.
* `evaluate()` gains an output class (`evaluate_evaluation`/`list`) and a basic print method.
* `evaluate()` now correctly captures plots created before messages/warnings/errors (#28).

Expand Down
35 changes: 7 additions & 28 deletions R/eval.R
Original file line number Diff line number Diff line change
Expand Up @@ -69,40 +69,18 @@ evaluate <- function(input,
}
local_inject_funs(envir)

if (new_device) {
# Ensure we have a graphics device available for recording, but choose
# one that's available on all platforms and doesn't write to disk
pdf(file = NULL)
dev.control(displaylist = "enable")
dev <- dev.cur()
on.exit(dev.off(dev))
}
# record the list of current devices
devs <- .env$dev_list; on.exit(.env$dev_list <- devs, add = TRUE)
devn <- length(.env$dev_list <- dev.list())
dev <- dev.cur()

# clean up the last_plot object after an evaluate() call (cf yihui/knitr#722)
on.exit(assign("last_plot", NULL, envir = environment(plot_snapshot)), add = TRUE)

# if this env var is set to true, always bypass messages
if (tolower(Sys.getenv('R_EVALUATE_BYPASS_MESSAGES')) == 'true')
keep_message = keep_warning = NA

# Capture output
watcher <- watchout(output_handler, debug = debug)
watcher <- watchout(output_handler, new_device = new_device, debug = debug)

out <- vector("list", nrow(parsed))
for (i in seq_along(out)) {
if (log_echo || debug) {
cat_line(parsed$src[[i]], file = stderr())
}

# if dev.off() was called, make sure to restore device to the one opened by
# evaluate() or existed before evaluate()
if (length(dev.list()) < devn) dev.set(dev)
devn <- length(dev.list())

out[[i]] <- evaluate_top_level_expression(
exprs = parsed$expr[[i]],
src = parsed$src[[i]],
Expand All @@ -116,6 +94,7 @@ evaluate <- function(input,
output_handler = output_handler,
include_timing = include_timing
)
watcher$check_devices()

if (stop_on_error > 0L) {
errs <- vapply(out[[i]], is.error, logical(1))
Expand Down Expand Up @@ -148,12 +127,12 @@ evaluate_top_level_expression <- function(exprs,
source <- new_source(src, exprs[[1]], output_handler$source)
output <- list(source)

dev <- dev.cur()
handle_output <- function(plot = TRUE, incomplete_plots = FALSE) {
# if dev.cur() has changed, we should not record plots any more
plot <- plot && identical(dev, dev.cur())
out <- watcher(plot, incomplete_plots)
output <<- c(output, out)
out <- list(
if (plot) watcher$capture_plot(incomplete_plots),
watcher$capture_output()
)
output <<- c(output, compact(out))
}

local_output_handler(function() handle_output(FALSE))
Expand Down
42 changes: 0 additions & 42 deletions R/graphics.R
Original file line number Diff line number Diff line change
@@ -1,45 +1,3 @@
#" Capture snapshot of current device.
#"
#" There's currently no way to capture when a graphics device changes,
#" except to check its contents after the evaluation of every expression.
#" This means that only the last plot of a series will be captured.
#"
#" @return \code{NULL} if plot is blank or unchanged, otherwise the output of
#" \code{\link{recordPlot}}.
plot_snapshot <- local({
last_plot <- NULL

function(incomplete = FALSE) {
devs <- dev.list()
# No graphics devices
if (is.null(devs)) {
return()
}

# Current graphics device changed since evaluate started
if (!identical(devs, .env$dev_list)) {
return()
}

# current page is incomplete
if (!par("page") && !incomplete) {
return()
}

plot <- recordPlot()
if (!makes_visual_change(plot[[1]])) {
return()
}

if (!looks_different(last_plot[[1]], plot[[1]])) {
return()
}

last_plot <<- plot
plot
}
})

looks_different <- function(old_dl, new_dl) {
if (identical(old_dl, new_dl)) {
return(FALSE)
Expand Down
82 changes: 62 additions & 20 deletions R/watcher.R
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
#' Watch for changes in output, text and graphics
#'
#' @param debug activate debug mode where output will be both printed to
#' screen and captured.
#' @param handler An ouptut handler object.
#' @param frame When this frame terminates, the watcher will automatically close.`
#' @return list containing four functions: `get_new`, `pause`,
#' `unpause`, `close`.
#' @keywords internal
watchout <- function(handler = new_output_handler(),
new_device = TRUE,
debug = FALSE,
frame = parent.frame()) {
last_plot <- NULL

if (new_device) {
# Ensure we have a graphics device available for recording, but choose
# one that's available on all platforms and doesn't write to disk
pdf(file = NULL)
dev.control(displaylist = "enable")
dev <- dev.cur()
defer(dev.off(dev), frame)
}

# record current devices
devs <- dev.list()
devn <- length(devs)
dev <- dev.cur()

con <- file("", "w+b")
defer(frame = frame, {
if (!test_con(con, isOpen)) {
Expand All @@ -24,20 +32,54 @@ watchout <- function(handler = new_output_handler(),
old <- options(try.outFile = con)
defer(options(old), frame = frame)

function(plot = TRUE, incomplete_plots = FALSE) {
out <- list(
if (plot) plot_snapshot(incomplete_plots),
read_con(con)
)
if (!is.null(out[[1]])) {
handler$graphics(out[[1]])
capture_plot <- function(incomplete = FALSE) {
# only record plots for our graphics device
if (!identical(dev.cur(), dev)) {
return()
}

# current page is incomplete
if (!par("page") && !incomplete) {
return()
}

plot <- recordPlot()
if (!makes_visual_change(plot[[1]])) {
return()
}
if (!is.null(out[[2]])) {
handler$text(out[[2]])

if (!looks_different(last_plot[[1]], plot[[1]])) {
return()
}

last_plot <<- plot
handler$graphics(plot)
plot
}

capture_output <- function() {
out <- read_con(con)
if (!is.null(out)) {
handler$text(out)
}
out
}

check_devices <- function() {
# if dev.off() was called, make sure to restore device to the one opened
# when watchout() was called
if (length(dev.list()) < devn) {
dev.set(dev)
}

compact(out)
devn <<- length(dev.list())
invisible()
}

list(
capture_plot = capture_plot,
capture_output = capture_output,
check_devices = check_devices
)
}

read_con <- function(con, buffer = 32 * 1024) {
Expand Down
24 changes: 0 additions & 24 deletions man/watchout.Rd

This file was deleted.

6 changes: 4 additions & 2 deletions tests/testthat/test-graphics.R
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ test_that("perspective plots are captured", {
})

# a bug report yihui/knitr#722
test_that("repeatedly drawing the same plot does not omit plots randomly", {
expect_true(all(replicate(100, length(evaluate("plot(1:10)"))) == 2))
test_that("plot state doesn't persist over evaluate calls", {
expect_output_types(evaluate("plot(1)"), c("source", "plot"))
expect_output_types(evaluate("plot(1)"), c("source", "plot"))
expect_output_types(evaluate("plot(1)"), c("source", "plot"))
})

test_that("evaluate() doesn't depend on device option", {
Expand Down

0 comments on commit fe54e17

Please sign in to comment.