diff --git a/NEWS.md b/NEWS.md index 038c4dc3..fa6d45a5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,10 @@ * Fix a problem detecting co-occurrences when expression has multiple lines. +### Miscellaneous + +* Refactor `eval_code` method signature to allow for more flexibility when extending the `eval_code`/`within` functions. + # teal.code 0.6.1 ### Bug fixes diff --git a/R/qenv-eval_code.R b/R/qenv-eval_code.R index 14af1ae7..878f5eca 100644 --- a/R/qenv-eval_code.R +++ b/R/qenv-eval_code.R @@ -9,6 +9,7 @@ #' @param code (`character`, `language` or `expression`) code to evaluate. #' It is possible to preserve original formatting of the `code` by providing a `character` or an #' `expression` being a result of `parse(keep.source = TRUE)`. +#' @param ... ([`dots`]) additional arguments passed to future methods. #' #' @return #' `qenv` environment with `code/expr` evaluated or `qenv.error` if evaluation fails. @@ -21,15 +22,28 @@ #' q <- eval_code(q, quote(library(checkmate))) #' q <- eval_code(q, expression(assert_number(a))) #' -#' @aliases eval_code,qenv,character-method -#' @aliases eval_code,qenv,language-method -#' @aliases eval_code,qenv,expression-method -#' @aliases eval_code,qenv.error,ANY-method -#' +#' @aliases eval_code,qenv-method +#' @aliases eval_code,qenv.error-method +#' @seealso [within.qenv] #' @export -setGeneric("eval_code", function(object, code) standardGeneric("eval_code")) +setGeneric("eval_code", function(object, code, ...) standardGeneric("eval_code")) + +setMethod("eval_code", signature = c(object = "qenv"), function(object, code, ...) { + if (!is.language(code) && !is.character(code)) { + stop("eval_code accepts code being language or character") + } + code <- .preprocess_code(code) + # preprocess code to ensure it is a character vector + .eval_code(object = object, code = code, ...) +}) + +setMethod("eval_code", signature = c(object = "qenv.error"), function(object, code, ...) object) -setMethod("eval_code", signature = c("qenv", "character"), function(object, code) { +#' @keywords internal +.eval_code <- function(object, code, ...) { + if (identical(trimws(code), "") || length(code) == 0) { + return(object) + } parsed_code <- parse(text = code, keep.source = TRUE) object@.xData <- rlang::env_clone(object@.xData, parent = parent.env(.GlobalEnv)) if (length(parsed_code) == 0) { @@ -42,7 +56,6 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code for (i in seq_along(code_split)) { current_code <- code_split[[i]] current_call <- parse(text = current_code, keep.source = TRUE) - # Using withCallingHandlers to capture warnings and messages. # Using tryCatch to capture the error and abort further evaluation. x <- withCallingHandlers( @@ -60,7 +73,7 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code errorCondition( message = sprintf( "%s \n when evaluating qenv code:\n%s", - .ansi_strip(conditionMessage(e)), + cli::ansi_strip(conditionMessage(e)), current_code ), class = c("qenv.error", "try-error", "simpleError"), @@ -69,11 +82,11 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code } ), warning = function(w) { - attr(current_code, "warning") <<- .ansi_strip(sprintf("> %s\n", conditionMessage(w))) + attr(current_code, "warning") <<- cli::ansi_strip(sprintf("> %s\n", conditionMessage(w))) invokeRestart("muffleWarning") }, message = function(m) { - attr(current_code, "message") <<- .ansi_strip(sprintf("> %s", conditionMessage(m))) + attr(current_code, "message") <<- cli::ansi_strip(sprintf("> %s", conditionMessage(m))) invokeRestart("muffleMessage") } ) @@ -87,42 +100,17 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code lockEnvironment(object@.xData, bindings = TRUE) object -}) - -setMethod("eval_code", signature = c("qenv", "language"), function(object, code) { - eval_code(object, code = paste(vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)), collapse = "\n")) -}) +} -setMethod("eval_code", signature = c("qenv", "expression"), function(object, code) { - srcref <- attr(code, "wholeSrcref") - if (length(srcref)) { - eval_code(object, code = paste(attr(code, "wholeSrcref"), collapse = "\n")) +setGeneric(".preprocess_code", function(code) standardGeneric(".preprocess_code")) +setMethod(".preprocess_code", signature = c("character"), function(code) paste(code, collapse = "\n")) +setMethod(".preprocess_code", signature = c("ANY"), function(code) { + if (is.expression(code) && length(attr(code, "wholeSrcref"))) { + paste(attr(code, "wholeSrcref"), collapse = "\n") } else { - Reduce(function(u, v) { - if (inherits(v, "=") && identical(typeof(v), "language")) { - # typeof(`=`) is language, but it doesn't dispatch on it, so we need to - # explicitly pass it as first class of the object - class(v) <- unique(c("language", class(v))) - } - eval_code(u, v) - }, init = object, x = code) + paste( + vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)), + collapse = "\n" + ) } }) - -setMethod("eval_code", signature = c("qenv.error", "ANY"), function(object, code) { - object -}) - -# if cli is installed rlang adds terminal printing characters -# which need to be removed -.ansi_strip <- function(chr) { - if (requireNamespace("cli", quietly = TRUE)) { - cli::ansi_strip(chr) - } else { - chr - } -} - -get_code_attr <- function(qenv, attr) { - unlist(lapply(qenv@code, function(x) attr(x, attr))) -} diff --git a/R/qenv-within.R b/R/qenv-within.R index ef68da14..951c2273 100644 --- a/R/qenv-within.R +++ b/R/qenv-within.R @@ -1,3 +1,4 @@ +#' Evaluate code in `qenv` #' @details #' `within()` is a convenience method that wraps `eval_code` to provide a simplified way of passing expression. #' `within` accepts only inline expressions (both simple and compound) and allows to substitute `expr` @@ -43,25 +44,18 @@ #' within(q, exprlist) # fails #' do.call(within, list(q, do.call(c, exprlist))) #' -#' @rdname eval_code -#' #' @export #' within.qenv <- function(data, expr, ...) { - expr <- substitute(expr) + expr <- as.expression(substitute(expr)) extras <- list(...) - # Add braces for consistency. - if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { - expr <- call("{", expr) - } - - calls <- as.list(expr)[-1] - # Inject extra values into expressions. - calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) - - eval_code(object = data, code = as.expression(calls)) + calls <- lapply(expr, function(x) do.call(substitute, list(x, env = extras))) + do.call( + eval_code, + utils::modifyList(extras, list(object = data, code = as.expression(calls))) + ) } diff --git a/man/eval_code.Rd b/man/eval_code.Rd index 203a96af..2cece165 100644 --- a/man/eval_code.Rd +++ b/man/eval_code.Rd @@ -1,17 +1,12 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/qenv-eval_code.R, R/qenv-within.R +% Please edit documentation in R/qenv-eval_code.R \name{eval_code} \alias{eval_code} -\alias{eval_code,qenv,character-method} -\alias{eval_code,qenv,language-method} -\alias{eval_code,qenv,expression-method} -\alias{eval_code,qenv.error,ANY-method} -\alias{within.qenv} +\alias{eval_code,qenv-method} +\alias{eval_code,qenv.error-method} \title{Evaluate code in \code{qenv}} \usage{ -eval_code(object, code) - -\method{within}{qenv}(data, expr, ...) +eval_code(object, code, ...) } \arguments{ \item{object}{(\code{qenv})} @@ -20,12 +15,7 @@ eval_code(object, code) It is possible to preserve original formatting of the \code{code} by providing a \code{character} or an \code{expression} being a result of \code{parse(keep.source = TRUE)}.} -\item{data}{(\code{qenv})} - -\item{expr}{(\code{expression}) to evaluate. Must be inline code, see \verb{Using language objects...}} - -\item{...}{named argument value will substitute a symbol in the \code{expr} matched by the name. -For practical usage see Examples section below.} +\item{...}{(\code{\link{dots}}) additional arguments passed to future methods.} } \value{ \code{qenv} environment with \code{code/expr} evaluated or \code{qenv.error} if evaluation fails. @@ -36,17 +26,7 @@ Evaluate code in \code{qenv} \details{ \code{eval_code()} evaluates given code in the \code{qenv} environment and appends it to the \code{code} slot. Thus, if the \code{qenv} had been instantiated empty, contents of the environment are always a result of the stored code. - -\code{within()} is a convenience method that wraps \code{eval_code} to provide a simplified way of passing expression. -\code{within} accepts only inline expressions (both simple and compound) and allows to substitute \code{expr} -with \code{...} named argument values. } -\section{Using language objects with \code{within}}{ - -Passing language objects to \code{expr} is generally not intended but can be achieved with \code{do.call}. -Only single \code{expression}s will work and substitution is not available. See examples. -} - \examples{ # evaluate code in qenv q <- qenv() @@ -55,34 +35,7 @@ q <- eval_code(q, "b <- 2L # with comment") q <- eval_code(q, quote(library(checkmate))) q <- eval_code(q, expression(assert_number(a))) -# evaluate code using within -q <- qenv() -q <- within(q, { - i <- iris -}) -q <- within(q, { - m <- mtcars - f <- faithful -}) -q -get_code(q) - -# inject values into code -q <- qenv() -q <- within(q, i <- iris) -within(q, print(dim(subset(i, Species == "virginica")))) -within(q, print(dim(subset(i, Species == species)))) # fails -within(q, print(dim(subset(i, Species == species))), species = "versicolor") -species_external <- "versicolor" -within(q, print(dim(subset(i, Species == species))), species = species_external) - -# pass language objects -expr <- expression(i <- iris, m <- mtcars) -within(q, expr) # fails -do.call(within, list(q, expr)) - -exprlist <- list(expression(i <- iris), expression(m <- mtcars)) -within(q, exprlist) # fails -do.call(within, list(q, do.call(c, exprlist))) - +} +\seealso{ +\link{within.qenv} } diff --git a/man/within.qenv.Rd b/man/within.qenv.Rd new file mode 100644 index 00000000..a4f2237c --- /dev/null +++ b/man/within.qenv.Rd @@ -0,0 +1,62 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/qenv-within.R +\name{within.qenv} +\alias{within.qenv} +\title{Evaluate code in \code{qenv}} +\usage{ +\method{within}{qenv}(data, expr, ...) +} +\arguments{ +\item{data}{(\code{qenv})} + +\item{expr}{(\code{expression}) to evaluate. Must be inline code, see \verb{Using language objects...}} + +\item{...}{named argument value will substitute a symbol in the \code{expr} matched by the name. +For practical usage see Examples section below.} +} +\description{ +Evaluate code in \code{qenv} +} +\details{ +\code{within()} is a convenience method that wraps \code{eval_code} to provide a simplified way of passing expression. +\code{within} accepts only inline expressions (both simple and compound) and allows to substitute \code{expr} +with \code{...} named argument values. +} +\section{Using language objects with \code{within}}{ + +Passing language objects to \code{expr} is generally not intended but can be achieved with \code{do.call}. +Only single \code{expression}s will work and substitution is not available. See examples. +} + +\examples{ +# evaluate code using within +q <- qenv() +q <- within(q, { + i <- iris +}) +q <- within(q, { + m <- mtcars + f <- faithful +}) +q +get_code(q) + +# inject values into code +q <- qenv() +q <- within(q, i <- iris) +within(q, print(dim(subset(i, Species == "virginica")))) +within(q, print(dim(subset(i, Species == species)))) # fails +within(q, print(dim(subset(i, Species == species))), species = "versicolor") +species_external <- "versicolor" +within(q, print(dim(subset(i, Species == species))), species = species_external) + +# pass language objects +expr <- expression(i <- iris, m <- mtcars) +within(q, expr) # fails +do.call(within, list(q, expr)) + +exprlist <- list(expression(i <- iris), expression(m <- mtcars)) +within(q, exprlist) # fails +do.call(within, list(q, do.call(c, exprlist))) + +} diff --git a/tests/testthat/test-qenv_eval_code.R b/tests/testthat/test-qenv_eval_code.R index 689ee170..b16ddbdf 100644 --- a/tests/testthat/test-qenv_eval_code.R +++ b/tests/testthat/test-qenv_eval_code.R @@ -45,6 +45,11 @@ testthat::test_that("eval_code works with expression", { testthat::expect_equal(q1, list2env(list(a = 1, b = 2))) }) +testthat::test_that("eval_code ignores empty code", { + q <- qenv() + testthat::expect_identical(q, eval_code(q, "")) +}) + testthat::test_that("eval_code preserves original formatting when `srcref` is present in the expression", { code <- "# comment a <- 1L" @@ -77,12 +82,11 @@ testthat::test_that("eval_code works with quoted code block", { testthat::expect_equal(q1, list2env(list(a = 1, b = 2))) }) -testthat::test_that("eval_code fails with unquoted expression", { - b <- 3 - testthat::expect_error( - eval_code(qenv(), a <- b), - "unable to find an inherited method for function .eval_code. for signature" - ) +testthat::test_that("eval_code fails with code not being language nor character", { + msg <- "eval_code accepts code being language or character" + testthat::expect_error(eval_code(qenv(), NULL), msg) + testthat::expect_error(eval_code(qenv(), 1), msg) + testthat::expect_error(eval_code(qenv(), list()), msg) }) testthat::test_that("an error when calling eval_code returns a qenv.error object which has message and trace", { @@ -182,8 +186,3 @@ testthat::test_that("comments passed alone to eval_code that contain @linksto ta "x" ) }) - -testthat::test_that("Code executed with integer shorthand (1L) is the same as original", { - q <- within(qenv(), a <- 1L) - testthat::expect_identical(get_code(q), "a <- 1L") -}) diff --git a/tests/testthat/test-qenv_join.R b/tests/testthat/test-qenv_join.R index 9a5d356b..a234fced 100644 --- a/tests/testthat/test-qenv_join.R +++ b/tests/testthat/test-qenv_join.R @@ -131,7 +131,7 @@ testthat::test_that("Joining two independent qenvs with warnings results in obje q <- c(q1, q2) testthat::expect_equal( - unname(get_code_attr(q, "warning")), + vapply(q@code, attr, which = "warning", character(1L), USE.NAMES = FALSE), c( "> This is warning 1\n", "> This is warning 2\n" @@ -146,7 +146,7 @@ testthat::test_that("Joining two independent qenvs with messages results in obje q <- c(q1, q2) testthat::expect_equal( - unname(get_code_attr(q, "message")), + vapply(q@code, attr, which = "message", character(1L), USE.NAMES = FALSE), c( "> This is message 1\n", "> This is message 2\n" diff --git a/tests/testthat/test-qenv_within.R b/tests/testthat/test-qenv_within.R index 9853b460..14a311a3 100644 --- a/tests/testthat/test-qenv_within.R +++ b/tests/testthat/test-qenv_within.R @@ -149,3 +149,8 @@ testthat::describe("within run with `=`", { testthat::expect_equal(q$i, 1) }) }) + +testthat::test_that("Code executed with integer shorthand (1L) is the same as original", { + q <- within(qenv(), a <- 1L) + testthat::expect_identical(get_code(q), "a <- 1L") +})