Skip to content

Commit

Permalink
Merge pull request #202 from ropensci/tidy_eval
Browse files Browse the repository at this point in the history
Support tidy evaluation (solve #200)
  • Loading branch information
Will Landau committed Jan 30, 2018
2 parents 7412d4b + c8d52c4 commit 1e50514
Show file tree
Hide file tree
Showing 20 changed files with 373 additions and 59 deletions.
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Package: drake
Title: Data Frames in R for Make
Description: An R-focused pipeline toolkit
for reproducible code and high-performance computing.
Version: 5.0.1.9000
Version: 5.0.1.9001
License: GPL-3
URL: https://github.com/ropensci/drake
BugReports: https://github.com/ropensci/drake/issues
Expand Down Expand Up @@ -59,6 +59,7 @@ Imports:
parallel,
plyr,
R.utils,
rlang,
rprojroot,
stats,
storr (>= 1.1.0),
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ importFrom(parallel,parLapply)
importFrom(parallel,stopCluster)
importFrom(plyr,ddply)
importFrom(plyr,dlply)
importFrom(rlang,expr)
importFrom(rlang,exprs)
importFrom(rprojroot,find_root)
importFrom(stats,coef)
importFrom(stats,complete.cases)
Expand Down
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Version 5.0.2
# Version 5.1.0

- Evaluate the quasiquotation operator `!!` for the `...` argument to `drake_plan()`. Suppress this behavior using `tidy_evaluation = FALSE` or by passing in commands passed through the `list` argument.
- Preprocess workflow plan commands with `rlang::expr()` before evaluating them. That means you can use the quasiquotation operator `!!` in your commands, and `make()` will evaluate them according to the tidy evaluation paradigm.
- Restructure `drake_example("basic")`, `drake_example("gsp")`, and `drake_example("packages")` to demonstrate how to set up the files for serious `drake` projects. More guidance was needed in light of [this issue](https://github.com/ropensci/drake/issues/193).
- Improve the examples of `drake_plan()` in the help file (`?drake_plan`).

Expand Down
3 changes: 1 addition & 2 deletions R/build.R
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ build_in_hook <- function(target, meta, config) {
}

build_target <- function(target, config) {
command <- get_command(target = target, config = config) %>%
functionize
command <- get_evaluation_command(target = target, config = config)
seed <- list(seed = config$seed, target = target) %>%
seed_from_object
value <- run_command(
Expand Down
53 changes: 42 additions & 11 deletions R/dependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ dependency_profile <- function(target, config){
names(hashes_of_dependencies) <- deps
out <- list(
cached_command = meta$command,
current_command = get_command(target = target, config = config),
current_command = get_standardized_command(
target = target, config = config
),
cached_file_modification_time = meta$mtime,
current_file_modification_time = suppressWarnings(
file.mtime(drake::drake_unquote(target))
Expand Down Expand Up @@ -286,7 +288,45 @@ is_not_file <- function(x){
!is_file(x)
}

tidy_command <- function(x) {
braces <- function(x) {
paste("{\n", x, "\n}")
}

# This is the version of the command that is
# actually run in make(), not the version
# that is cached and treated as a dependency.
# It needs to (1) wrap the command in a function
# to protect the user's environment from side effects,
# and (2) call rlang::expr() to enable tidy evaluation
# features such as quasiquotation.
get_evaluation_command <- function(target, config){
raw_command <- config$plan$command[config$plan$target == target] %>%
functionize
unevaluated <- paste0("rlang::expr(", raw_command, ")")
quasiquoted <- eval(parse(text = unevaluated), envir = config$envir)
wide_deparse(quasiquoted)
}

# This version of the command will be hashed and cached
# as a dependency. When the command changes nontrivially,
# drake will react. Otherwise, changes to whitespace or
# comments are just standardized away, and drake
# ignores them. Thus, superfluous builds are not triggered.
get_standardized_command <- function(target, config) {
config$plan$command[config$plan$target == target] %>%
standardize_command
}

# The old standardization command
# that relies on formatR.
# Eventually, we may move to styler,
# since it is now the preferred option for
# text tidying.
# The important thing for drake's standardization of commands
# is to stay stable here, not to be super correct.
# If styler's behavior changes a lot, it will
# put targets out of date.
standardize_command <- function(x) {
formatR::tidy_source(
source = NULL,
comment = FALSE,
Expand All @@ -301,12 +341,3 @@ tidy_command <- function(x) {
paste(collapse = "\n") %>%
braces
}

braces <- function(x) {
paste("{\n", x, "\n}")
}

get_command <- function(target, config) {
config$plan$command[config$plan$target == target] %>%
tidy_command
}
10 changes: 10 additions & 0 deletions R/make.R
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,16 @@
#' # Requires Rtools on Windows.
#' # make(my_plan, parallelism = "Makefile", jobs = 4, # nolint
#' # recipe_command = "R -q -e") # nolint
#' #
#' # make() respects tidy evaluation as implemented in the rlang package.
#' # This workflow plan uses rlang's quasiquotation operator `!!`.
#' my_plan <- drake_plan(list = c(
#' little_b = "\"b\"",
#' letter = "!!little_b"
#' ))
#' my_plan
#' make(my_plan)
#' readd(letter) # "b"
#' })
#' }
make <- function(
Expand Down
4 changes: 2 additions & 2 deletions R/meta.R
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ drake_meta <- function(target, config) {
# fields at the beginning of build_in_hook(),
# but only after drake decides to actually build the target.
if (trigger %in% triggers_with_command()){
meta$command <- get_command(target = target, config = config)
meta$command <- get_standardized_command(target = target, config = config)
}
if (trigger %in% triggers_with_depends()){
meta$depends <- dependency_hash(target = target, config = config)
Expand Down Expand Up @@ -101,7 +101,7 @@ finish_meta <- function(target, meta, config){
meta$file <- file_hash(target = target, config = config)
}
if (is.null(meta$command)){
meta$command <- get_command(target = target, config = config)
meta$command <- get_standardized_command(target = target, config = config)
}
if (is.null(meta$depends)){
meta$depends <- dependency_hash(target = target, config = config)
Expand Down
4 changes: 2 additions & 2 deletions R/migrate.R
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,15 @@ hashes <- function(target, config) {
}

legacy_dependency_hash <- function(target, config) {
command <- legacy_get_command(target = target, config = config)
command <- legacy_get_tidy_command(target = target, config = config)
stopifnot(length(command) == 1)
dependencies(target, config) %>%
legacy_self_hash(config = config) %>%
c(command) %>%
digest::digest(algo = config$long_hash_algo)
}

legacy_get_command <- function(target, config){
legacy_get_tidy_command <- function(target, config){
config$plan$command[config$plan$target == target] %>% legacy_tidy
}

Expand Down
1 change: 1 addition & 0 deletions R/package.R
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
#' mclapply parLapply stopCluster
#' @importFrom plyr ddply dlply
#' @importFrom R.utils isPackageLoaded withTimeout
#' @importFrom rlang expr exprs
#' @importFrom rprojroot find_root
#' @importFrom stats coef complete.cases lm rnorm rpois runif setNames
#' @importFrom storr encode64 storr_environment storr_rds
Expand Down
34 changes: 32 additions & 2 deletions R/workplan.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
#' \code{...} argument. R will either convert all these quotes
#' to single quotes or double quotes. Literal quotes in the
#' \code{list} argument are left alone.
#' @param tidy_evaluation logical, whether to use tidy evaluation
#' such as quasiquotation
#' when evaluating commands passed through the free-form
#' \code{...} argument.
#' @examples
#' # Create example workflow plan data frames for make()
#' drake_plan(small = simulate(5), large = simulate(50))
Expand Down Expand Up @@ -76,14 +80,40 @@
#' )
#' mtcars_plan
#' # make(mtcars_plan) # Would write output_file.csv. # nolint
#' # In the free-form `...` argument
#' # drake_plan() uses tidy evaluation to figure out your commands.
#' # For example, it respects the quasiquotation operator `!!`
#' # when it figures out what your code should be.
#' # Suppress this with `tidy_evaluation = FALSE` or
#' # with the `list` argument.
#' my_variable <- 5
#' drake_plan(
#' a = !!my_variable,
#' b = !!my_variable + 1,
#' list = c(d = "!!my_variable")
#' )
#' drake_plan(
#' a = !!my_variable,
#' b = !!my_variable + 1,
#' list = c(d = "!!my_variable"),
#' tidy_evaluation = FALSE
#' )
#' # For instances of !! that remain unevaluated in the workflow plan,
#' # make() will run these commands in tidy fashion,
#' # evaluating the !! operator using the environment you provided.
drake_plan <- function(
...,
list = character(0),
file_targets = FALSE,
strings_in_dots = c("filenames", "literals")
strings_in_dots = c("filenames", "literals"),
tidy_evaluation = TRUE
){
strings_in_dots <- match.arg(strings_in_dots)
dots <- match.call(expand.dots = FALSE)$...
if (tidy_evaluation){
dots <- rlang::exprs(...) # Enables quasiquotation via rlang.
} else {
dots <- match.call(expand.dots = FALSE)$...
}
commands_dots <- lapply(dots, wide_deparse)
names(commands_dots) <- names(dots)
commands <- c(commands_dots, list)
Expand Down
1 change: 1 addition & 0 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ prework
programmatically
Programmatically
PSOCK
quasiquotation
quickstart
Quickstart
RDS
Expand Down
25 changes: 25 additions & 0 deletions inst/examples/basic/interactive-tutorial.R
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,31 @@ report <- drake_plan(
my_plan <- rbind(report, my_datasets, my_analyses, results)


# For the commands you specify the free-form `...` argument,
# `drake_plan()` also supports tidy evaluation.
# For example, it supports quasiquotation with the `!!` argument.
# Use `tidy_evaluation = FALSE` or the `list` argument
# to suppress this behavior.

my_variable <- 5

drake_plan(
a = !!my_variable,
b = !!my_variable + 1,
list = c(d = "!!my_variable")
)

drake_plan(
a = !!my_variable,
b = !!my_variable + 1,
list = c(d = "!!my_variable"),
tidy_evaluation = FALSE
)

# For instances of !! that remain in the workflow plan,
# make() will run these commands in tidy fashion,
# evaluating the !! operator using the environment you provided.

#####################################
### CHECK AND DEBUG WORKFLOW PLAN ###
#####################################
Expand Down
28 changes: 27 additions & 1 deletion man/drake_plan.Rd

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

10 changes: 10 additions & 0 deletions man/make.Rd

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

45 changes: 45 additions & 0 deletions tests/testthat/test-tidy-eval.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
drake_context("tidy eval")

# From Kendon Bell: https://github.com/ropensci/drake/issues/200
test_with_dir("drake_plan does tidy eval in `...` argument", {
my_variable <- 5
plan1 <- drake_plan(
a = !!my_variable,
b = !!my_variable + 1,
list = c(d = "!!my_variable")
)
plan2 <- data.frame(
target = c("a", "b", "d"),
command = c("5", "6", "!!my_variable"),
stringsAsFactors = FALSE
)
expect_equal(plan1, plan2)
})

# From Alex Axthelm: https://github.com/ropensci/drake/issues/200
test_with_dir("drake_plan tidy eval can be disabled", {
plan1 <- drake_plan(
a = !!my_variable,
b = !!my_variable + 1,
list = c(d = "!!my_variable"),
tidy_evaluation = FALSE
)
plan2 <- data.frame(
target = c("a", "b", "d"),
command = c("!(!my_variable)", "!(!my_variable + 1)", "!!my_variable"),
stringsAsFactors = FALSE
)
expect_equal(plan1, plan2)
})

# From Kendon Bell: https://github.com/ropensci/drake/issues/200
test_with_dir("make() does tidy eval in commands", {
con <- dbug()
con$plan <- drake_plan(list = c(
little_b = "\"b\"",
letter = "!!little_b"
))
con$targets <- con$plan$target
testrun(con)
expect_equal(readd(letter), "b")
})

0 comments on commit 1e50514

Please sign in to comment.