Skip to content

Defer on globalenv #76

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

Merged
merged 23 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
08e0286
Make withr friendlier to RStudio users
jennybc Jul 1, 2018
d5d8ff4
Allow deferred events on globalenv; add clear()
jennybc Jul 1, 2018
4577257
Rename clear() to flush_global_deferred()
jennybc Jul 2, 2018
c37ab86
Store NULL as execution env for deferred global events
jennybc Jul 2, 2018
a3912b7
Rename flush_global_deferred() to run_global_deferred()
jennybc Jul 2, 2018
ab24f8f
Merge branch 'master' into defer-on-globalenv
jennybc Apr 15, 2020
7075237
Remove `@author`
jennybc Apr 15, 2020
c550a25
Apply house style
jennybc Apr 15, 2020
e14c523
This as.list() call doesn't do anything
jennybc Apr 15, 2020
78cff26
Add clear_global_deferred()
jennybc Apr 15, 2020
78e9e27
Move this to fail early on bad priority
jennybc Apr 15, 2020
066a873
Don't store an env as the execution env for one of it's own handlers
jennybc Apr 15, 2020
163e6b9
Alternative solution to the self-capture problem
jennybc Apr 15, 2020
3c49efa
Add a test
jennybc Apr 15, 2020
2c8ce48
Add NEWS bullet
jennybc Apr 15, 2020
8d8034e
Fix typo
jennybc Apr 15, 2020
51641bf
Explicit line break for the message
jennybc Apr 15, 2020
07b2a39
Message only if no pre-existing handlers; use bullets
jennybc Apr 16, 2020
5148c29
Complete the restyling that I started
jennybc Apr 16, 2020
130bb88
Merge branch 'master' into defer-on-globalenv
jennybc Apr 16, 2020
d47b4d9
Switch to deferred_run() and deferred_clear()
jennybc Apr 16, 2020
c980b09
Don't worry about attaching an env to itself via the "handlers" attri…
jennybc Apr 16, 2020
f22a162
Merge branch 'master' into defer-on-globalenv
jimhester Apr 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .Rbuildignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
^withr\.Rproj$
^\.Rproj\.user$
^.*\.Rproj$
^.*\.tar\.gz$
^withr\.Rcheck$
^\.travis\.yml$
^\.lintr$
^appveyor\.yml$
^\.Rproj\.user$
^man-roxygen$
^README\.Rmd$
^README-.*\.png$
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

export(defer)
export(defer_parent)
export(deferred_clear)
export(deferred_run)
export(local_)
export(local_bmp)
export(local_cairo_pdf)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# withr 2.1.2.9000

- `defer()` can set deferred events on `.GlobalEnv` to facilitate the
interactive development of code inside a function or test. Helpers
`deferred_run()` (and `deferred_clear()`) provide a way to explicity run and
clear (or just clear) deferred events (#76, @jennybc).

- `with_svg()` documentation now is consistent across R versions (#129)

- Remove `help` argument from `with_package()` and `local_package()` (#94, @wendtke).
Expand Down
65 changes: 52 additions & 13 deletions R/defer.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@
#'
#' @details
#'
#' `defer` works by attaching handlers to the requested environment (as an
#' `defer()` works by attaching handlers to the requested environment (as an
#' attribute called `"handlers"`), and registering an exit handler that
#' executes the registered handler when the function associated with the
#' requested environment finishes execution.
#'
#' Deferred events can be set on the global environment, primarily to facilitate
#' the interactive development of code that is intended to be executed inside a
#' function or test. A message alerts the user to the fact that an explicit
#' `deferred_run()` is the only way to trigger these deferred events. Use
#' `deferred_clear()` to clear them without evaluation. The global environment
#' scenario is the main motivation for these functions.
#'
#' @family local-related functions
#' @export
#' @author Kevin Ushey
#' @examples
#' # define a 'local' function that creates a file, and
#' # removes it when the parent function has finished executing
Expand All @@ -49,12 +55,31 @@
#' local_file(path)
#' print(attributes(environment()))
#' })
#'
#' # defer and trigger events on the global environment
#' defer(print("one"))
#' defer(print("two"))
#' deferred_run()
#'
#' defer(print("three"))
#' deferred_clear()
#' deferred_run()
defer <- function(expr, envir = parent.frame(), priority = c("first", "last")) {
if (identical(envir, .GlobalEnv))
stop("attempt to defer event on global environment")
priority <- match.arg(priority)
front <- priority == "first"
invisible(add_handler(envir, list(expr = substitute(expr), envir = parent.frame()), front))
if (identical(envir, .GlobalEnv) && is.null(get_handlers(envir))) {
message(
"Setting deferred event(s) on global environment.\n",
" * Execute (and clear) with `deferred_run()`.\n",
" * Clear (without executing) with `deferred_clear()`."
)
}
invisible(
add_handler(
envir,
handler = list(expr = substitute(expr), envir = parent.frame()),
front = priority == "first"
)
)
}

#' @rdname defer
Expand All @@ -66,13 +91,26 @@ defer_parent <- function(expr, priority = c("first", "last")) {
), envir = parent.frame())
}

#' @rdname defer
#' @export
deferred_run <- function(envir = parent.frame()) {
execute_handlers(envir)
deferred_clear(envir)
}

#' @rdname defer
#' @export
deferred_clear <- function(envir = parent.frame()) {
attr(envir, "handlers") <- NULL
invisible()
}

## Handlers used for 'defer' calls. Attached as a list of expressions for the
## 'handlers' attribute on the environment, with 'on.exit' called to ensure
## those handlers get executed on exit.

get_handlers <- function(envir) {
as.list(attr(envir, "handlers"))
attr(envir, "handlers")
}

set_handlers <- function(envir, handlers) {
Expand All @@ -90,16 +128,17 @@ set_handlers <- function(envir, handlers) {

execute_handlers <- function(envir) {
handlers <- get_handlers(envir)
for (handler in handlers)
for (handler in handlers) {
tryCatch(eval(handler$expr, handler$envir), error = identity)
}
}

add_handler <- function(envir, handler, front) {

handlers <- if (front)
c(list(handler), get_handlers(envir))
else
c(get_handlers(envir), list(handler))
if (front) {
handlers <- c(list(handler), get_handlers(envir))
} else {
handlers <- c(get_handlers(envir), list(handler))
}

set_handlers(envir, handlers)
handler
Expand Down
27 changes: 23 additions & 4 deletions man/defer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions tests/testthat/test-defer.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,30 @@ test_that("defer_parent works", {
# file is deleted as we leave 'local' scope
expect_false(file.exists(path))
})

test_that("defer()'s global env facilities work", {
expect_null(get_handlers(globalenv()))
Sys.setenv(abcdefg = "abcdefg")

expect_message(
defer(print("howdy"), envir = globalenv()),
"Setting deferred event"
)
expect_message(
local_envvar(c(abcdefg = "tuvwxyz"), .local_envir = globalenv()),
NA
)

h <- get_handlers(globalenv())
expect_length(h, 2)
expect_equal(Sys.getenv("abcdefg"), "tuvwxyz")

expect_output(deferred_run(globalenv()), "howdy")
expect_equal(Sys.getenv("abcdefg"), "abcdefg")

defer(print("never going to happen"), envir = globalenv())
deferred_clear(globalenv())

h <- get_handlers(globalenv())
expect_null(h)
})