Giter Club home page Giter Club logo

withr's Introduction

withr - run code ‘with’ modified state

Codecov test coverage CRAN Version R-CMD-check

Overview

A set of functions to run code with safely and temporarily modified global state, withr makes working with the global state, i.e. side effects, less error-prone.

Pure functions, such as the sum() function, are easy to understand and reason about: they always map the same input to the same output and have no other impact on the workspace. In other words, pure functions have no side effects: they are not affected by, nor do they affect, the global state in any way apart from the value they return.

The behavior of some functions is affected by the global state. Consider the read.csv() function: it takes a filename as an input and returns the contents as an output. In this case, the output depends on the contents of the file; i.e. the output is affected by the global state. Functions like this deal with side effects.

The purpose of the withr package is to help you manage side effects in your code. You may want to run code with secret information, such as an API key, that you store as an environment variable. You may also want to run code with certain options, with a given random-seed, or with a particular working-directory.

The withr package helps you manage these situations, and more, by providing functions to modify the global state temporarily, and safely. These functions modify one of the global settings for duration of a block of code, then automatically reset it after the block is completed.

Installation

#Install the latest version with:
install.packages("withr")

Many of these functions were originally a part of the devtools package, this provides a simple package with limited dependencies to provide access to these functions.

  • with_collate() / local_collate() - collation order
  • with_dir() / local_dir() - working directory
  • with_envvar() / local_envvar() - environment variables
  • with_libpaths() / local_libpaths() - library paths
  • with_locale() / local_locale() - any locale setting
  • with_makevars() / local_makevars() / set_makevars() - makevars variables
  • with_options() / local_options() - options
  • with_par() / local_par() - graphics parameters
  • with_path() / local_path() - PATH environment variable
  • with_*() and local_*() functions for the built in R devices, bmp, cairo_pdf, cairo_ps, pdf, postscript, svg, tiff, xfig, png, jpeg.
  • with_connection() / local_connection() - R file connections
  • with_db_connection() / local_db_connection() - DB connections
  • with_package() / local_package(), with_namespace() / local_namespace() and with_environment() / local_environment() - to run code with modified object search paths.
  • with_tempfile() / local_tempfile() - create and clean up a temp file.
  • with_file() / local_file() - create and clean up a normal file.
  • with_message_sink() / local_message_sink() - divert message
  • with_output_sink() / local_output_sink() - divert output
  • with_preserve_seed() / with_seed()- specify seeds
  • with_temp_libpaths() / local_temp_libpaths() - library paths
  • defer() / defer_parent() - defer
  • with_timezone() / local_timezone() - timezones
  • with_rng_version() / local_rng_version() - random number generation version

Usage

There are two sets of functions, those prefixed with with_ and those with local_. The former reset their state as soon as the code argument has been evaluated. The latter reset when they reach the end of their scope, usually at the end of a function body.

par("col" = "black")
my_plot <- function(new) {
  with_par(list(col = "red", pch = 19),
    plot(mtcars$hp, mtcars$wt)
  )
  par("col")
}
my_plot()

#> [1] "black"
par("col")
#> [1] "black"

f <- function(x) {
  local_envvar(c("WITHR" = 2))
  Sys.getenv("WITHR")
}

f()
#> [1] "2"
Sys.getenv("WITHR")
#> [1] ""

There are also with_() and local_() functions to construct new with_* and local_* functions if needed.

Sys.getenv("WITHR")
#> [1] ""
with_envvar(c("WITHR" = 2), Sys.getenv("WITHR"))
#> [1] "2"
Sys.getenv("WITHR")
#> [1] ""

with_envvar(c("A" = 1),
  with_envvar(c("A" = 2), action = "suffix", Sys.getenv("A"))
)
#> [1] "1 2"

See Also

withr's People

Contributors

alexcipro avatar angelinepro avatar ashesitr avatar batpigandme avatar czeildi avatar davisvaughan avatar dragosmg avatar dskard avatar ellessenne avatar gaborcsardi avatar hadley avatar javierluraschi avatar jennybc avatar jimhester avatar jonkeane avatar krlmlr avatar kyleam avatar lauracion avatar lionel- avatar malfaro2 avatar meta00 avatar michaelchirico avatar mlopez-ibanez avatar mpaulacaldas avatar mtmorgan avatar multimeric avatar orichters avatar romainfrancois avatar wendtke avatar zkamvar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

withr's Issues

with_envvar_clear

runs code with all environment variables unset, can be useful for testing.

Logo

Withered vine spelling withr was my thought...

Using with_output_sink() causing unit tests to fail

SWIM has some unit tests that loop over combinations of argument values for a function and call a print method and just check that no errors are thrown. The printing causes a bunch of output I don't particularly want to appear on the console when I use testthat::test_file() and also creates a bunch of output in the travis-CI log files I don't want either. I was hoping I could use with_output_sink() to still run the tests, but without all the annoying output. Unfortunately, this causes an error in a very innocuous looking part of the code that I can't reproduce if I remove the call to with_output_sink(). The test file is here.

with_envvar_unset

  with_envvar_unset <- withr::with_(
    function(x) {
      old <- Sys.getenv(x, names = TRUE)
      Sys.unsetenv(x)
      old
    },
    Sys.setenv
  )

Integrating this into with_envvar() is another option: Unset all elements that are NULL. Interface becomes incompatible with Sys.setenv() but this is not necessarily a bad thing.

Document local_ functions better

Need to mention them in the function level documentation of withr and have examples of local_ in the README.md and in ?withr at least.

with_library()

I'm looking for a clean way to run examples that call other packages without injecting library() calls. Would rather avoid using ::.

Very rough sketch:

with_require <- with_(add_to_search, restore_search)

add_to_search <- function(pkg) {
  old_search <- search()
  pkg_id <- rev(paste0("package:", pkg))
  lapply(pkg_id, add_to_search_one)
  old_search
}

add_to_search_one <- function(pkg) {
  if (pkg %in% search()) {
    detach(pkg)
  }
  require(pkg, quietly = TRUE)
}

restore_search <- function(old_search) {
  ...
}

Possible extension: Generic environments, as in

with_require(list(env1 = new.env()), ...)

Thoughts?

Name change?

Hadley suggested wither, or maybe withr, which is pretty clever!

Tests for `with_par()`, `with_dir()`

Neither of these files have any calls in them, so they are not counted in the coverage.

This is due to covr tracking coverage on objects defined in the package environment rather than parsing the expressions in the file directly.

Regardless we should add tests for them!

Make pkgdown site

If you set CNAME to withr.r-lib.org we have a wildcard DNS to make it work automatically.

use withr::with_options to pass shiny.maxRequestSize to a shiny app

Hi, I have an issue using with_option to launch a shiny app with the correct option

I dont understand why this code doesn't work as expected :

library(shiny)

ui <- fluidPage(
  verbatimTextOutput("option"),
  verbatimTextOutput("option2"),
  actionButton("go","go")
)

server <- function(input, output, session) {
  output$option <- renderText({getOption("repos")})
  output$option2 <- renderText({ getOption("shiny.maxRequestSize") })
}

withr::with_options(list(shiny.maxRequestSize="find_me"), {
  shiny::shinyApp(ui, server)
})

this app should show "find_me" inside a textouput do I make a typo? getOption("shiny.maxRequestSize") have to be 'find_me', why not?

I certainly can add this option inside the server, but I prefere to use with_option.

R 3.1 support

It's a bit unfortunate that the latest version of devtools no longer works on R 3.1.x. Is there a way we can make withr work on R 3.1, at least until R 3.3 is out?

Suggestion: with_temp_dir

For code that needs to create a large number of temporary files, it may be more convenient to create a single temporary directory, create any files it needs to in that directory, and then recursively delete that directory when it finishes. I suggest a function with_temp_dir that encapsulates this pattern. It would work like Python 3's tempfile.TemporaryDirectory context manager.

with_env

Implementation can be simple as

with_env <- function(code, env = new.env()) {
  base::with(new.env(...), code)
}
with_env({a <- 1;print(a)})
#> [1] 1
a
#> Error: object 'a' not found

Useful for test setup, temporary variables you don't want to cleanup ect.

with_seed

To run some code with a temporary random seed. Useful for testing.

on.exit calls

This might seem nitpicking in R, but I think it is important to learn good practice in general. Whenever you write things like

restore <- change_something()
on.exit(restore_something(restore), add = TRUE)
...

you introduce a potential error. If the code is interrupted after change_something, but before on.exit, then restore_something will never happen.

Here is an example from devtools:
https://github.com/hadley/devtools/blob/c2ad0729e56990b5c66a4e1ef50f3aaa2b8b9c5a/R/with.r#L36-L41

This is of course everywhere, in base R and CRAN packages, not just in devtools. I think in most cases you can just do something like:

on.exit(try(restore_something(restore)), add = TRUE)
restore <- change_something()
...

i.e. just switch the order of the two expressions, and add a try(), in case there is an interruption after on.exit(), but before change_something().

More with_* function ideas

with_device

For saving base/lattice graphics. Need to call dev.off on exiting.

I think that you get clearer code by having a different function for each device type, for example

with_png <- function(new, code)
{
  do.call(png, as.list(new))
  on.exit(dev.off())
  force(code)
}

with usage like

with_png("test.png", plot(1:10))
with_png(list("test2.png", width = 800, height = 600), plot(1:10))

You also need with_tiff, with_pdf, etc.

with_connection

For file connections. Need to call close on exiting.

This needs some thinking about. I want to write

with_file <- function(new, code)
{
  conn <- do.call(file, as.list(new))
  on.exit(close(conn))
  # How to pass conn to the code?
  force(code)
}
with_file(list("test.txt", open = "w"), writeLines(LETTERS))

But you need to figure out the best way to pass the connection to writeLines/readLines/cat/whichever other function uses the connection.

with_db_connection

Using dbConnect and on.exit(dbdisconnect), but again you need a way to pass the connection to the code.

Suggestion: literal with_dev function

Based on code I wrote a while ago to accomplish a similar thing, I'd like to suggest an actual with_dev function:

## Returns TRUE if x refers to the device number of a currently active
## graphics device.
library(rlang)
library(assertthat)
is_dev <- function(x) {
    is_scalar_integer(x) && x %in% dev.list()
}

with_dev <- function(dev, code, closedev) {
    orig.device <- dev.cur()
    new.device <- force(dev)
    # Functions that create devices don't generally return them, they
    # just set them as the new current device, so get the actual
    # device from dev.cur() instead.
    if (is.null(new.device)) {
        new.device <- dev.cur()
    }
    assert_that(is_dev(new.device))
    message(glue("Orig device: {deparse(orig.device)}; new device: {deparse(new.device)}"))
    if (missing(closedev)) {
         closedev <- new.device != orig.device
    }
    on.exit({
        if (closedev) {
            dev.off(new.device)
        }
        if (is_dev(orig.device)) {
            dev.set(orig.device)
        }
    })
    force(code)
}

# Usage:
library(ggplot2)
with_dev(pdf(file.path(tempdir(), "test.pdf")), print(qplot(x=1:50, y=rnorm(50))))

The dev argument should either be code that returns a graphics device, or code that creates a new graphics device, sets it as the current device, and returns NULL. (Functions like png, pdf, etc. fall into the latter category.)

The function tries to auto-detect whether or not is should close the device when done with it. The heuristic is that if evaluating dev changes the value of dev.cur(), then the device will be closed on exit. This can be overridden with the closedev argument.

The is_dev function is just a helper here, but it might be worth including in rlang, named something like is_graphics_device.

My original code: https://github.com/DarwinAwardWinner/CD4-csaw/blob/master/scripts/utilities.R#L143-L169

Bug in environment of `defer_parent`

This code errors when run as is, but works when library(withr) is called first. defer, however, does not have this problem.

scoped_temp_file <- function(path) {
    fs::file_create(path) 
    withr::defer_parent(fs::file_delete(path))
}

testthat::test_that("file changes", {
    scoped_temp_file("file.txt")
})
#> Error: Test failed: 'file changes'
#> * could not find function "defer"
#> 1: scoped_temp_file("file.txt") at <text>:7
#> 2: withr::defer_parent(fs::file_delete(path)) at <text>:3
#> 3: eval(substitute(defer(expr, envir, priority), list(expr = substitute(expr), envir = parent.frame(2), 
#>        priority = priority)), envir = parent.frame())
#> 4: eval(substitute(defer(expr, envir, priority), list(expr = substitute(expr), envir = parent.frame(2), 
#>        priority = priority)), envir = parent.frame())

cc @jimhester

local_options() evaluates error handler functions, fails to restore original handler

With

library(withr)
f <- function(...) { cat("f\n"); 1 }
options(error = f)
g <- function() {
    local_options(list( error = function(...) { cat("g\n"); 2 } ))
    3
}

the evaluation of f() can be seen by the side-effect

> g()                                     # side-effect
f
[1] 3

The failure to re-establish the original handler is seen with

> options("error")                        # local option not removed
$error
(function (...) 
{
    cat("g\n")
    2
})()

The problem is in the call to set_options when returning from g()

> withr:::set_options
function (new_options) 
{
    do.call(options, as.list(new_options))
}

where do.call() evaluates as.list(new_options).

The problem came up in the context of RUnit (setting the equivalent of f() to report the traceback on error) and batchtools @mllg playing the role of g()

> head(batchtools:::execJob.Job, 3)
1 function (job)                                            
2 {                                                         
3     local_options(list(error = function(e) traceback(2L)))

Copywrite

So in devtools it has person("RStudio", role = "cph"), which assigns RStudio the copyright as both @hadley and @wch are employed by Rstudio.

While much of the code in this package was originally derived from code in devtools it has been fairly substantially reworked by @krlmlr and I. I think we should definitely keep Hadley and Winston as authors and attribute the original source of the code to devtools, however I am not sure if RStudio should still be listed as the copyright holder as neither Kirill or I is associated with them.

If it is decided that RStudio should remain the copyright holder that is fine with me, but I wanted to get some input before we submit withr to CRAN.

with_tempfile

This takes an idea from bquote() and allows you to specify what variable to assign the temporary filename to in your code by surrounding it in a .() call. Then these files are cleaned up afterwards. The function definition and example usage is below. An alternative interface would be to have a separate argument to specify variable names to use, but I think it is a little nicer to specify them inline. Any thoughts @krlmlr?

with_tempfile <- function(code, envir = parent.frame()) {
  # Adapted from bquote
  vars <- character()
  unquote <- function(e) {
    if (is.pairlist(e))
      as.pairlist(lapply(e, unquote))
    else if (length(e) <= 1) e
    else if (e[[1]] == as.name(".")) {
      vars <<- append(vars, as.character(e[[2]]))
      e[[2]]
    }
    else as.call(lapply(e, unquote))
  }
  code <- unquote(substitute(code))
  if (length(vars) == 0) {
    stop("Must specify a variable to store the temp filename using .() in your
         code", call. = FALSE)
  }

  env <- new.env(parent = envir)
  for (var in vars) {
    assign(var, tempfile(), envir = env)
  }
  on.exit(unlink(mget(vars, envir = env)))

  eval(code, envir = env)
}

files <- character()

with_tempfile({
  writeLines("hi", con = .(tmp))
  writeLines("ho", con = .(tmp2))
  print(readLines(tmp))
  print(readLines(tmp2))
  files <<- append(files, tmp)
  files <<- append(files, tmp2)
})
#> [1] "hi"
#> [1] "ho"

readLines(files[1])
#> Warning in file(con, "r"): cannot open file '/tmp/RtmpjcsoCQ/
#> filecc56011eda5': No such file or directory
#> Error in file(con, "r"): cannot open the connection
readLines(files[2])
#> Warning in file(con, "r"): cannot open file '/tmp/RtmpjcsoCQ/
#> filecc52fad8b97': No such file or directory
#> Error in file(con, "r"): cannot open the connection

local_tempfile

It occurs to me that the first argument could default to tempfile() and the path could be returned

This might be a useful pattern elsewhere - with_*() can't easily pass a value into the block (unless we use NSE or require anonymous functions), but local_ doesn't have the same problem.

R version requirement

Requiring R (>= 3.2.0) in the DESCRIPTION is more restrictive than devtools (currently R (>= 3.0.2)).
Is that really necessary?

New feature: with_cluster()

I am picturing a function that reaps zombie children on error, something like the following.

with_cluster <- function(cl, code){
  withCallingHandlers(
    code,
    error = function(e){
      parallel::stopCluster(cl)
      stop(e)
    }
  )
}

with_options() restores only the options listed in `new`

library(withr)
getOption("undefined")
## NULL
with_options(new = list(some_other_option = 5), {
  options(undefined = "user_defined")
})
getOption("undefined")
## "user_defined"

Do we want to restore all the options, not just those in new?

Think about value stack

For with_tempfile(), with_connection() and with_db_connection(). See also discussion in #29.

Need to find or create a package that implements a mutable stack.

Do we need to distinguish by type, for nested calls, so that we can ask "give me the innermost object of type x"?

with_handlers

The with version is just withCallingHandlers(), but a local_ one would also be interesting...

set_locale doesn't work with LC_ALL

> Sys.getlocale("LC_ALL")
[1] "LC_CTYPE=en_US.UTF-8;LC_NUMERIC=C;LC_TIME=de_CH.UTF-8;LC_COLLATE=en_US.UTF-8;LC_MONETARY=de_CH.UTF-8;LC_MESSAGES=en_US.UTF-8;LC_PAPER=de_CH.UTF-8;LC_NAME=C;LC_ADDRESS=C;LC_TELEPHONE=C;LC_MEASUREMENT=de_CH.UTF-8;LC_IDENTIFICATION=C"

with_makevars / set_makevars improvements

First, on windows, we should use Makevars.win, if it exists. Assuming R can use a user or system level Makevars.win file at all.

Second, if R_USER_MAKEVARS is already set to a file, we should probably use that as the basis.

consider 'with_backup' helper?

I'm imaging a case where you're writing a function that attempts to write a file at some location, but restores the old copy (filesystem state) if something goes wrong during update. E.g.

with_backup <- function(file, expr) {
  
  backup <- tempfile(
    pattern = sprintf(".backup-%s-", tools::file_path_sans_ext(basename(file))),
    tmpdir = dirname(file),
    fileext = paste0(".", tools::file_ext(file))
  )
  
  file.rename(file, backup)
  on.exit({
    if (file.exists(file))
      unlink(backup)
    else
      file.rename(backup, file)
  })
  
  force(expr)
}

Destructor syntax

Something like:

destruct <- function(x){
  UseMethod("destruct")
}

volatile <- function(x){
  defer_parent(destruct(x))
  x
} 

Then a class could define the S3 method for destruct ...

'defer' + 'scoped' side effects

The goal is to enable writing, instead of this,

with_dir(directory, {
    // code
})

to write this:

scoped_dir(directory)
// code

This can be accomplished with a couple tools. The main trick is to use on.exit, but attach its execution to an arbitrary parent frame. We create a function defer that accomplishes that:

defer <- function(expr, envir = parent.frame()) {

  # Create a call that must be evaluated in the parent frame (as
  # that's where functions and symbols need to be resolved)
  call <- substitute(
    evalq(expr, envir = envir),
    list(expr = substitute(expr), envir = parent.frame())
  )

  # Use 'do.call' with 'on.exit' to attach the evaluation to
  # the exit handlers of the selected frame
  do.call("on.exit", list(substitute(call), add = TRUE), envir = envir)

  # TODO: Not sure what we should actually return.
}

And then create functions that provide their 'state' and 'reset' at the exit of that scope:

scoped_dir <- function(directory) {
    owd <- setwd(directory)
    defer(setwd(owd), parent.frame())
}

Any thoughts on whether something like this would be appropriate for withr?


For completeness, this was inspired by the discussion here: https://stat.ethz.ch/pipermail/r-devel/2013-November/067874.html

with_makevars()

The .R/Makevars is useful for setting compile flags; setting them temporarily is a pain.

To avoid overwriting an existing file, we could

  1. create /tmp/home-xxxx and there create a symlink for everything in $HOME except .R/Makevars
  2. create /tmp/home-xxxx/.R/Makevars, with an optional -include /old/home/.R/Makevars
  3. set the HOME environment variable to /tmp/home-xxxx
  4. run the code
  5. undo everything

CC @gaborcsardi

with_sink

> testthat:::with_sink
function(connection, code, ...) {
  sink(connection, ...)
  on.exit(sink())

  code
}

Allowing multiple message sinks to the same file

with_message_sink() deliberately avoids creating two message sink()s on the same file. I am wondering if you might consider relaxing this behavior somehow, maybe with something like with_message_sink(..., allow_multiple = TRUE). See this SO post for a use case that would greatly benefit.

lattice may be unused

lattice was moved from Suggests to Imports in 26a2ddf, but it seems like it may not be used. The CRAN check log says it's not imported from.

A search shows it only used in tests/testthat/test-devices.R, so maybe it was an accident.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.