diff --git a/NAMESPACE b/NAMESPACE index 559c6c9a..1633eb8b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method("[[",quosure.error) export(chunk) export(chunk_call) export(chunk_comment) @@ -42,7 +43,9 @@ exportMethods(get_code) exportMethods(get_var) exportMethods(join) exportMethods(new_quosure) +exportMethods(show) import(shiny) importFrom(R6,R6Class) importFrom(lifecycle,badge) +importFrom(methods,show) importFrom(styler,style_text) diff --git a/R/quosure-class.R b/R/quosure-class.R index 1e6a5b25..316b3117 100644 --- a/R/quosure-class.R +++ b/R/quosure-class.R @@ -16,6 +16,7 @@ setClass( ) #' It takes a `Quosure` class and returns TRUE if the input is valid +#' @name Quosure-class #' @keywords internal setValidity("Quosure", function(object) { if (length(object@code) != length(object@id)) { diff --git a/R/quosure-errors.R b/R/quosure-errors.R new file mode 100644 index 00000000..cfb37f8b --- /dev/null +++ b/R/quosure-errors.R @@ -0,0 +1,2 @@ +# needed to handle try-error +setOldClass("quosure.error") diff --git a/R/quosure-eval_code.R b/R/quosure-eval_code.R index 0e4b62e9..af4016e5 100644 --- a/R/quosure-eval_code.R +++ b/R/quosure-eval_code.R @@ -22,47 +22,56 @@ setGeneric("eval_code", function(object, code, name = "code") { #' @rdname eval_code #' @export -setMethod( - "eval_code", - signature = c("Quosure", "character"), - function(object, code, name) { - checkmate::assert_string(name) - if (is.null(names(code))) { - code <- paste(code, collapse = "\n") - names(code) <- name - } - id <- sample.int(.Machine$integer.max, size = length(code)) - object@id <- c(object@id, id) - object@code <- .keep_code_name_unique(object@code, code) - - # need to copy the objects from old env to new env - # to avoid updating environments in the separate objects - object@env <- .copy_env(object@env) - eval(parse(text = code), envir = object@env) - lockEnvironment(object@env) - object +setMethod("eval_code", signature = c("Quosure", "character"), function(object, code, name) { + checkmate::assert_string(name) + if (is.null(names(code))) { + code <- paste(code, collapse = "\n") + names(code) <- name } -) + id <- sample.int(.Machine$integer.max, size = length(code)) + + object@id <- c(object@id, id) + object@code <- .keep_code_name_unique(object@code, code) + + # need to copy the objects from old env to new env + # to avoid updating environments in the separate objects + object@env <- .copy_env(object@env) + tryCatch( + { + eval(parse(text = code), envir = object@env) + lockEnvironment(object@env) + object + }, + error = function(e) { + errorCondition( + message = sprintf( + "%s \n when evaluating Quosure code:\n %s", + conditionMessage(e), + paste(code, collapse = "\n ") + ), + class = c("quosure.error", "try-error", "simpleError"), + trace = object@code + ) + } + ) +}) #' @rdname eval_code #' @export -setMethod( - "eval_code", - signature = c("Quosure", "expression"), - function(object, code, name) { - code_char <- as.character(code) - eval_code(object, code_char, name = name) - } -) +setMethod("eval_code", signature = c("Quosure", "expression"), function(object, code, name) { + code_char <- as.character(code) + eval_code(object, code_char, name = name) +}) #' @rdname eval_code #' @export -setMethod( - "eval_code", - signature = c("Quosure", "language"), - function(object, code, name) { - code_char <- as.expression(code) - eval_code(object, code_char, name = name) - } -) +setMethod("eval_code", signature = c("Quosure", "language"), function(object, code, name) { + code_char <- as.expression(code) + eval_code(object, code_char, name = name) +}) +#' @rdname eval_code +#' @export +setMethod("eval_code", signature = "quosure.error", function(object, code, name) { + object +}) diff --git a/R/quosure-get_code.R b/R/quosure-get_code.R index 2cafe511..7981fab5 100644 --- a/R/quosure-get_code.R +++ b/R/quosure-get_code.R @@ -12,6 +12,10 @@ #' #' @export setGeneric("get_code", function(object) { + # this line forces evaluation of object before passing to the generic + # needed for error handling to work properly + object + standardGeneric("get_code") }) @@ -20,3 +24,18 @@ setGeneric("get_code", function(object) { setMethod("get_code", signature = "Quosure", function(object) { object@code }) + +#' @rdname get_code +#' @export +setMethod("get_code", signature = "quosure.error", function(object) { + stop( + errorCondition( + sprintf( + "%s\n\ntrace: \n %s\n", + conditionMessage(object), + paste(object$trace, collapse = "\n ") + ), + class = c("validation", "try-error", "simpleError") + ) + ) +}) diff --git a/R/quosure-get_var.R b/R/quosure-get_var.R index 3afbdf0b..2ef17539 100644 --- a/R/quosure-get_var.R +++ b/R/quosure-get_var.R @@ -22,12 +22,31 @@ setMethod("get_var", signature = c("Quosure", "character"), function(object, var get(var, envir = object@env) }) +#' @rdname get_var +#' @export +setMethod("get_var", signature = "quosure.error", function(object, var) { + stop(errorCondition( + list(message = conditionMessage(object)), + class = c("validation", "try-error", "simpleError") + )) +}) + + #' @param x (`Quosure`) #' @param i (`character`) name of the binding in environment (name of the variable) #' @param j not used #' @param ... not used #' @rdname get_var #' @export -setMethod("[[", c("Quosure", "ANY", "missing"), function(x, i, j, ...) { +setMethod("[[", signature = c("Quosure", "ANY", "missing"), function(x, i, j, ...) { get_var(x, i) }) + +#' @rdname get_var +#' @export +`[[.quosure.error` <- function(x, i, j, ...) { + stop(errorCondition( + list(message = conditionMessage(x)), + class = c("validation", "try-error", "simpleError") + )) +} diff --git a/R/quosure-join.R b/R/quosure-join.R index 5e0d6131..11d85b14 100644 --- a/R/quosure-join.R +++ b/R/quosure-join.R @@ -48,6 +48,18 @@ setMethod("join", signature = c("Quosure", "Quosure"), function(x, y) { x }) +#' @rdname join +#' @export +setMethod("join", signature = "quosure.error", function(x, y) { + x +}) + +#' @rdname join +#' @export +setMethod("join", signature = c("Quosure", "quosure.error"), function(x, y) { + y +}) + #' If two `Quosure` can be joined #' #' Checks if two `Quosure` objects can be combined. diff --git a/R/quosure-show.R b/R/quosure-show.R new file mode 100644 index 00000000..56e36199 --- /dev/null +++ b/R/quosure-show.R @@ -0,0 +1,18 @@ +#' Show the `Quosure` object +#' +#' Prints the `Quosure` object +#' @param object (`Quosure`) +#' @return nothing +#' @importFrom methods show +#' @examples +#' q1 <- new_quosure(code = "print('a')", env = new.env()) +#' q1 +#' @export +setMethod("show", "Quosure", function(object) { + obs <- names(as.list(object@env)) + if (length(obs) > 0) { + cat(paste("A quosure object containing:", paste(obs, collapse = ", "))) + } else { + cat("A quosure object containing no objects") + } +}) diff --git a/man/eval_code.Rd b/man/eval_code.Rd index 4e9be79c..c782b97f 100644 --- a/man/eval_code.Rd +++ b/man/eval_code.Rd @@ -5,6 +5,7 @@ \alias{eval_code,Quosure,character-method} \alias{eval_code,Quosure,expression-method} \alias{eval_code,Quosure,language-method} +\alias{eval_code,quosure.error,ANY-method} \title{Evaluate the code in the \code{Quosure} environment} \usage{ eval_code(object, code, name = "code") @@ -14,6 +15,8 @@ eval_code(object, code, name = "code") \S4method{eval_code}{Quosure,expression}(object, code, name = "code") \S4method{eval_code}{Quosure,language}(object, code, name = "code") + +\S4method{eval_code}{quosure.error,ANY}(object, code, name = "code") } \arguments{ \item{object}{(\code{Quosure})} diff --git a/man/get_code.Rd b/man/get_code.Rd index 405fe9c7..722d425e 100644 --- a/man/get_code.Rd +++ b/man/get_code.Rd @@ -3,11 +3,14 @@ \name{get_code} \alias{get_code} \alias{get_code,Quosure-method} +\alias{get_code,quosure.error-method} \title{Get code from \code{Quosure}} \usage{ get_code(object) \S4method{get_code}{Quosure}(object) + +\S4method{get_code}{quosure.error}(object) } \arguments{ \item{object}{(\code{Quosure})} diff --git a/man/get_var.Rd b/man/get_var.Rd index 307a63e6..a81cd231 100644 --- a/man/get_var.Rd +++ b/man/get_var.Rd @@ -3,14 +3,20 @@ \name{get_var} \alias{get_var} \alias{get_var,Quosure,character-method} +\alias{get_var,quosure.error,ANY-method} \alias{[[,Quosure,ANY,missing-method} +\alias{[[.quosure.error} \title{Get object from the \code{Quosure} environment} \usage{ get_var(object, var) \S4method{get_var}{Quosure,character}(object, var) +\S4method{get_var}{quosure.error,ANY}(object, var) + \S4method{[[}{Quosure,ANY,missing}(x, i, j, ...) + +\method{[[}{quosure.error}(x, i, j, ...) } \arguments{ \item{object}{(\code{Quosure})} diff --git a/man/join.Rd b/man/join.Rd index 3794ddb6..f2dfde90 100644 --- a/man/join.Rd +++ b/man/join.Rd @@ -3,11 +3,17 @@ \name{join} \alias{join} \alias{join,Quosure,Quosure-method} +\alias{join,quosure.error,ANY-method} +\alias{join,Quosure,quosure.error-method} \title{Join two \code{Quosure} objects} \usage{ join(x, y) \S4method{join}{Quosure,Quosure}(x, y) + +\S4method{join}{quosure.error,ANY}(x, y) + +\S4method{join}{Quosure,quosure.error}(x, y) } \arguments{ \item{x}{(\code{Quosure})} diff --git a/man/show-Quosure-method.Rd b/man/show-Quosure-method.Rd new file mode 100644 index 00000000..ef79d050 --- /dev/null +++ b/man/show-Quosure-method.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/quosure-show.R +\name{show,Quosure-method} +\alias{show,Quosure-method} +\title{Show the \code{Quosure} object} +\usage{ +\S4method{show}{Quosure}(object) +} +\arguments{ +\item{object}{(\code{Quosure})} +} +\value{ +nothing +} +\description{ +Prints the \code{Quosure} object +} +\examples{ +q1 <- new_quosure(code = "print('a')", env = new.env()) +q1 +} diff --git a/tests/testthat/test-eval_code.R b/tests/testthat/test-eval_code.R index a23d1dee..f7195c53 100644 --- a/tests/testthat/test-eval_code.R +++ b/tests/testthat/test-eval_code.R @@ -8,7 +8,7 @@ testthat::test_that("eval_code doesn't have access to environment where it's cal a <- 1L q1 <- new_quosure("a <- 1", env = environment()) b <- 2L - testthat::expect_error(eval_code(q1, "d <- b"), "object 'b' not found") + testthat::expect_s3_class(eval_code(q1, "d <- b"), c("quosure.error", "try-error", "error", "condition")) }) testthat::test_that("@env in quosure is always a sibling of .GlobalEnv", { @@ -30,7 +30,7 @@ testthat::test_that("library have to be called separately before using function testthat::expect_identical(parent.env(q2@env), parent.env(.GlobalEnv)) detach("package:checkmate", unload = TRUE) - testthat::expect_error( + testthat::expect_s3_class( eval_code( new_quosure(), as.expression(c( @@ -38,7 +38,7 @@ testthat::test_that("library have to be called separately before using function quote(assert_number(1)) )) ), - "could not find function \"assert_number\"" + "quosure.error" ) }) @@ -89,7 +89,16 @@ testthat::test_that("each eval_code adds name to passed code", { testthat::expect_identical(q3@code, c(test = "a <- 1", test2 = "b <- 2")) }) -testthat::test_that("get_code make name of the code block unique if duplicated", { +testthat::test_that("an error when calling eval_code returns a quosure.error object which has message and trace", { + q <- eval_code(new_quosure(), "x <- 1") + q <- eval_code(q, "y <- 2") + q <- eval_code(q, "z <- w * x") + testthat::expect_s3_class(q, "quosure.error") + testthat::expect_equal(unname(q$trace), c("x <- 1", "y <- 2", "z <- w * x")) + testthat::expect_equal(q$message, "object 'w' not found \n when evaluating Quosure code:\n z <- w * x") +}) + +testthat::test_that("eval_code make name of the code block unique if duplicated", { q1 <- new_quosure() q2 <- eval_code(q1, code = "a <- 1", name = "test") q3 <- eval_code(q2, code = "b <- 2", name = "test") diff --git a/tests/testthat/test-quosure-get_code.R b/tests/testthat/test-quosure-get_code.R new file mode 100644 index 00000000..b7afaefe --- /dev/null +++ b/tests/testthat/test-quosure-get_code.R @@ -0,0 +1,21 @@ +testthat::test_that("get_code returns code of Quosure object", { + q <- new_quosure(list2env(list(x = 1)), code = "x <- 1") + q <- eval_code(q, "y <- x", name = "next_code") + testthat::expect_equal(get_code(q), c("initial code" = "x <- 1", "next_code" = "y <- x")) +}) + +testthat::test_that("get_code called with quosure.error returns error with trace in error message", { + q <- new_quosure(list2env(list(x = 1)), code = "x <- 1") + q <- eval_code(q, "y <- x") + q <- eval_code(q, "w <- v") + + code <- tryCatch( + get_code(q), + error = function(e) e + ) + testthat::expect_equal(class(code), c("validation", "try-error", "simpleError", "error", "condition")) + testthat::expect_equal( + code$message, + "object 'v' not found \n when evaluating Quosure code:\n w <- v\n\ntrace: \n x <- 1\n y <- x\n w <- v\n" + ) +}) diff --git a/tests/testthat/test-quosure-get_var.R b/tests/testthat/test-quosure-get_var.R new file mode 100644 index 00000000..94bdbdab --- /dev/null +++ b/tests/testthat/test-quosure-get_var.R @@ -0,0 +1,24 @@ +testthat::test_that("get_var and `[[`return error if object is quosure.error", { + q <- eval_code(new_quosure(), "x <- 1") + q <- eval_code(q, "y <- w * x") + + testthat::expect_error(get_var(q, "x"), "when evaluating Quosure code") + testthat::expect_error(q[["x"]], "when evaluating Quosure code") +}) + + +testthat::test_that("get_var and `[[` return object from quosure environment", { + q <- eval_code(new_quosure(), "x <- 1") + q <- eval_code(q, "y <- 5 * x") + + testthat::expect_equal(get_var(q, "y"), 5) + testthat::expect_equal(q[["x"]], 1) +}) + +testthat::test_that("get_var and `[[` throw error if object not in quosure environment", { + q <- eval_code(new_quosure(), "x <- 1") + q <- eval_code(q, "y <- 5 * x") + + testthat::expect_error(get_var(q, "z"), "object 'z' not found") + testthat::expect_error(q[["w"]], "object 'w' not found") +}) diff --git a/tests/testthat/test-quosure-join.R b/tests/testthat/test-quosure-join.R index 82f1315f..b7a9e5df 100644 --- a/tests/testthat/test-quosure-join.R +++ b/tests/testthat/test-quosure-join.R @@ -153,3 +153,17 @@ testthat::test_that("Quosure objects are not mergable if they have multiple comm testthat::expect_match(check_joinable(q1, q2), "doesn't have the same indices") testthat::expect_error(join(q1, q2), "doesn't have the same indices") }) + + +testthat::test_that("joining with a quosure.error object returns the quosure.error object", { + q1 <- eval_code(new_quosure(), "x <- 1") + error_q <- eval_code(new_quosure(), "y <- w") + error_q2 <- eval_code(new_quosure(), "z <- w") + + testthat::expect_s3_class(join(q1, error_q), "quosure.error") + testthat::expect_s3_class(join(error_q, error_q2), "quosure.error") + testthat::expect_s3_class(join(error_q, q1), "quosure.error") + + # if joining two quosure.error objects keep the first + testthat::expect_equal(join(error_q, error_q2), error_q) +}) diff --git a/tests/testthat/test-quosure_show.R b/tests/testthat/test-quosure_show.R new file mode 100644 index 00000000..d3ecd4d6 --- /dev/null +++ b/tests/testthat/test-quosure_show.R @@ -0,0 +1,11 @@ +testthat::test_that("showing an empty quosure states quosure is empty", { + q <- new_quosure() + testthat::expect_output(show(q), "A quosure object containing no objects") +}) + +testthat::test_that("showing a non-empty quosure lists its contents", { + q <- new_quosure() + q <- eval_code(q, "x <- 1") + q <- eval_code(q, "y <- 2") + testthat::expect_output(show(q), "A quosure object containing: x, y") +})