-
Notifications
You must be signed in to change notification settings - Fork 239
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
safe expression evaluation with callr #174
Changes from 6 commits
902be58
9fd1520
6d2d5eb
691409f
d55e833
da4e79a
91bd682
04187b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,109 @@ | ||
|
||
#' Run a tutorial | ||
#' | ||
#' | ||
#' Run a tutorial which is contained within an R package. | ||
#' | ||
#' @param name Tutorial name (subdirectory within \code{tutorials/} | ||
#' | ||
#' @param name Tutorial name (subdirectory within \code{tutorials/} | ||
#' directory of installed package). | ||
#' @param package Name of package | ||
#' @param shiny_args Additional arguments to forward to | ||
#' \code{\link[shiny:runApp]{shiny::runApp}}. | ||
#' | ||
#' @param shiny_args Additional arguments to forward to | ||
#' \code{\link[shiny:runApp]{shiny::runApp}}. | ||
#' | ||
#' @details Note that when running a tutorial Rmd file with \code{run_tutorial} | ||
#' the tutorial Rmd should have already been rendered as part of the | ||
#' development of the package (i.e. the correponding tutorial .html file for | ||
#' the tutorial Rmd should have already been rendered as part of the | ||
#' development of the package (i.e. the correponding tutorial .html file for | ||
#' the .Rmd file must exist). | ||
#' | ||
#' | ||
#' @seealso \code{\link{safe}} | ||
#' @export | ||
run_tutorial <- function(name, package, shiny_args = NULL) { | ||
|
||
# get path to tutorial | ||
tutorial_path <- system.file("tutorials", name, package = package) | ||
|
||
# validate that it's a direcotry | ||
if (!utils::file_test("-d", tutorial_path)) | ||
if (!utils::file_test("-d", tutorial_path)) | ||
stop("Tutorial ", name, " was not found in the ", package, " package.") | ||
|
||
# provide launch_browser if it's not specified in the shiny_args | ||
if (is.null(shiny_args)) | ||
shiny_args <- list() | ||
if (is.null(shiny_args$launch.browser)) | ||
shiny_args$launch.browser <- interactive() | ||
|
||
# run within tutorial wd and ensure we don't call rmarkdown::render | ||
withr::with_dir(tutorial_path, { | ||
withr::with_envvar(c(RMARKDOWN_RUN_PRERENDER = "0"), { | ||
rmarkdown::run(file = NULL, dir = tutorial_path, shiny_args = shiny_args) | ||
}) | ||
}) | ||
} | ||
|
||
|
||
#' Safe R CMD environment | ||
#' | ||
#' By default, \code{callr::\link[callr]{rcmd_safe_env}} suppresses the ability | ||
#' to open a browser window. This is the default execution evnironment within | ||
#' \code{callr::\link[callr]{r}}. However, opening a browser is expected | ||
#' behavior within the learnr package and should not be suppressed. | ||
#' @export | ||
safe_env <- function() { | ||
envs <- callr::rcmd_safe_env() | ||
envs[!(names(envs) %in% c("R_BROWSER"))] | ||
} | ||
|
||
|
||
callr_try_catch <- function(...) { | ||
tryCatch( | ||
..., | ||
# TODO when processx 3.2.0 is released, _downgrade_ to "interrupt" call instead of "system_command_interrupt". | ||
# https://github.com/r-lib/processx/issues/148 | ||
|
||
# if a user sends an interrupt, return silently | ||
system_command_interrupt = function() invisible(NULL) | ||
) | ||
} | ||
|
||
|
||
#' Execute R code in a safe R environment | ||
#' | ||
#' When rendering (or running) a document with R markdown, it inherits the | ||
#' current R Global environment. This will produce unexpected behaviors, | ||
#' such as poisoning the R Global environment with existing variables. By | ||
#' rendering the document in a new, safe R environment, a \emph{vanilla}, | ||
#' rendered document is produced. | ||
#' | ||
#' Using \code{safe} should only be necessary when locally deployed. | ||
#' | ||
#' @param expr expression that contains all the necessary library calls to | ||
#' execute. Expressions within callr do not inherit the existing, | ||
#' loaded libraries. | ||
#' @export | ||
#' @examples | ||
#' \dontrun{ | ||
#' # Direct usage | ||
#' safe({learnr::run_tutorial("slidy", package = "learnr")}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think having For this example, I think you also don't want Here's what you'd end up with after those two changes. This will be easier for new users to comprehend: safe(run_tutorial("slidy", package = "learnr")) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem, I can remove the brackets. To get the code to work, I'd need to library the learnr package. Makes sense as it's in the learnr package. |
||
#' | ||
#' # Programmatic usage | ||
#' library(rlang) | ||
#' | ||
#' expr <- quote(learnr::run_tutorial("slidy", package = "learnr")) | ||
#' safe(!!expr) | ||
#' | ||
#' tutorial <- "slidy" | ||
#' safe({learnr::run_tutorial(!!tutorial, package = "learnr")}) | ||
#' } | ||
safe <- function(expr, ..., show = TRUE, env = safe_env()) { | ||
expr <- rlang::enquo(expr) | ||
callr_try_catch({ | ||
callr::r( | ||
function(exp) { | ||
rlang::eval_tidy(exp) | ||
}, | ||
list(exp = expr), | ||
..., | ||
show = show, | ||
env = env | ||
) | ||
}) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
|
||
context("safe r call") | ||
|
||
test_that("safe() executes code expression directly and programmatically", { | ||
library(rlang) | ||
|
||
file = tempfile() | ||
|
||
# Direct usage | ||
safe(cat("1\n", file = file)) | ||
expect_equal(readLines(file), "1") | ||
|
||
# Programmatic usage | ||
exp <- quote(cat("2\n", file = file)) | ||
safe(!!exp) | ||
expect_equal(readLines(file), "2") | ||
|
||
x <- "3\n" | ||
safe(cat(!!x, file = file)) | ||
expect_equal(readLines(file), "3") | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be helpful to have a comment of why this function exists (because it's the same as the default for
callr::r
except that it allows opening a browser).