Skip to content

Commit

Permalink
error handling quosures (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
gogonzo authored Sep 15, 2022
1 parent a5c454a commit fe2af66
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 41 deletions.
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand

S3method("[[",quosure.error)
export(chunk)
export(chunk_call)
export(chunk_comment)
Expand Down Expand Up @@ -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)
1 change: 1 addition & 0 deletions R/quosure-class.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
2 changes: 2 additions & 0 deletions R/quosure-errors.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# needed to handle try-error
setOldClass("quosure.error")
81 changes: 45 additions & 36 deletions R/quosure-eval_code.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
19 changes: 19 additions & 0 deletions R/quosure-get_code.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})

Expand All @@ -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")
)
)
})
21 changes: 20 additions & 1 deletion R/quosure-get_var.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
))
}
12 changes: 12 additions & 0 deletions R/quosure-join.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions R/quosure-show.R
Original file line number Diff line number Diff line change
@@ -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")
}
})
3 changes: 3 additions & 0 deletions man/eval_code.Rd

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

3 changes: 3 additions & 0 deletions man/get_code.Rd

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

6 changes: 6 additions & 0 deletions man/get_var.Rd

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

6 changes: 6 additions & 0 deletions man/join.Rd

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

21 changes: 21 additions & 0 deletions man/show-Quosure-method.Rd

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

17 changes: 13 additions & 4 deletions tests/testthat/test-eval_code.R
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand All @@ -30,15 +30,15 @@ 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(
quote(library(checkmate)),
quote(assert_number(1))
))
),
"could not find function \"assert_number\""
"quosure.error"
)
})

Expand Down Expand Up @@ -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")
Expand Down
21 changes: 21 additions & 0 deletions tests/testthat/test-quosure-get_code.R
Original file line number Diff line number Diff line change
@@ -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"
)
})
Loading

0 comments on commit fe2af66

Please sign in to comment.