Skip to content

Commit

Permalink
Change qenv as environment "type" -- adds `names(qenv/qenv.error)…
Browse files Browse the repository at this point in the history
…`, `get()` and `$` S3 methods (#218)

# Pull Request

- Part of insightsengineering/teal.data#333
- Fixes #221 
- closes #164
- Companion of insightsengineering/teal.data#347

#### Changes description

- `qenv` S4 class inherits from `environment` data class
- Removes `@env` slot in favor of `qenv`
- Replace all instances of `@env` with `@.xData` (slot created by parent
class)
- All functions/methods that work for `environment` class are supported
natively in `qenv`

---------

Signed-off-by: André Veríssimo <[email protected]>
Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Dawid Kałędkowski <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Nov 8, 2024
1 parent 622cb09 commit e14dc64
Show file tree
Hide file tree
Showing 33 changed files with 502 additions and 202 deletions.
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Language: en-US
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Collate:
'qenv-c.R'
'qenv-class.R'
'qenv-errors.R'
'qenv-concat.R'
Expand All @@ -63,6 +64,7 @@ Collate:
'qenv-get_var.R'
'qenv-get_warnings.R'
'qenv-join.R'
'qenv-length.R'
'qenv-show.R'
'qenv-within.R'
'teal.code-package.R'
Expand Down
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Generated by roxygen2: do not edit by hand

S3method("$",qenv.error)
S3method("[[",qenv.error)
S3method(as.list,qenv.error)
S3method(c,qenv)
S3method(c,qenv.error)
S3method(length,qenv)
S3method(length,qenv.error)
S3method(names,qenv.error)
S3method(within,qenv)
S3method(within,qenv.error)
export(concat)
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

* `get_code()` was extended with `names` parameter and allows the code extraction to be limited to objects stored in
`qenv` but limited to `names`.
* `qenv` inherits from the `environment` class, allowing to use `ls()`, `names()`, `as.environment()` and other functions on `qenv` objects.
* `join()` method is deprecated, please use `c()` instead
* `get_var()` method is deprecated, please use `get`, `[[` or `$` instead.

# teal.code 0.5.0

Expand Down
107 changes: 107 additions & 0 deletions R/qenv-c.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#' If two `qenv` can be joined
#'
#' Checks if two `qenv` objects can be combined.
#' For more information, please see [`join`]
#' @param x (`qenv`)
#' @param y (`qenv`)
#' @return `TRUE` if able to join or `character` used to print error message.
#' @keywords internal
.check_joinable <- function(x, y) {
checkmate::assert_class(x, "qenv")
checkmate::assert_class(y, "qenv")

common_names <- intersect(rlang::env_names(x@.xData), rlang::env_names(y@.xData))
is_overwritten <- vapply(common_names, function(el) {
!identical(get(el, x@.xData), get(el, y@.xData))
}, logical(1))
if (any(is_overwritten)) {
return(
paste(
"Not possible to join qenv objects if anything in their environment has been modified.\n",
"Following object(s) have been modified:\n - ",
paste(common_names[is_overwritten], collapse = "\n - ")
)
)
}

shared_ids <- intersect(x@id, y@id)
if (length(shared_ids) == 0) {
return(TRUE)
}

shared_in_x <- match(shared_ids, x@id)
shared_in_y <- match(shared_ids, y@id)

# indices of shared ids should be 1:n in both slots
if (identical(shared_in_x, shared_in_y) && identical(shared_in_x, seq_along(shared_ids))) {
TRUE
} else if (!identical(shared_in_x, shared_in_y)) {
paste(
"The common shared code of the qenvs does not occur in the same position in both qenv objects",
"so they cannot be joined together as it's impossible to determine the evaluation's order.",
collapse = ""
)
} else {
paste(
"There is code in the qenv objects before their common shared code",
"which means these objects cannot be joined.",
collapse = ""
)
}
}

#' @rdname join
#' @param ... (`qenv` or `qenv.error`).
#' @examples
#' q <- qenv()
#' q1 <- within(q, {
#' iris1 <- iris
#' mtcars1 <- mtcars
#' })
#' q1 <- within(q1, iris2 <- iris)
#' q2 <- within(q1, mtcars2 <- mtcars)
#' qq <- c(q1, q2)
#' cat(get_code(qq))
#'
#' @export
c.qenv <- function(...) {
dots <- rlang::list2(...)
if (!checkmate::test_list(dots[-1], types = c("qenv", "qenv.error"))) {
return(NextMethod(c, dots[[1]]))
}

first_non_qenv_ix <- which.min(vapply(dots, inherits, what = "qenv", logical(1)))
if (first_non_qenv_ix > 1) {
return(dots[[first_non_qenv_ix]])
}

Reduce(
x = dots[-1],
init = dots[[1]],
f = function(x, y) {
join_validation <- .check_joinable(x, y)

# join expressions
if (!isTRUE(join_validation)) {
stop(join_validation)
}

id_unique <- !y@id %in% x@id
x@id <- c(x@id, y@id[id_unique])
x@code <- c(x@code, y@code[id_unique])
x@warnings <- c(x@warnings, y@warnings[id_unique])
x@messages <- c(x@messages, y@messages[id_unique])

# insert (and overwrite) objects from y to x
x@.xData <- rlang::env_clone(x@.xData, parent = parent.env(.GlobalEnv))
rlang::env_coalesce(env = x@.xData, from = y@.xData)
x
}
)
}

#' @rdname join
#' @export
c.qenv.error <- function(...) {
rlang::list2(...)[[1]]
}
63 changes: 57 additions & 6 deletions R/qenv-class.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#' @name qenv-class
#' @rdname qenv-class
#' @slot code (`character`) representing code necessary to reproduce the environment
#' @slot env (`environment`) environment which content was generated by the evaluation
#' @slot .xData (`environment`) environment with content was generated by the evaluation
#' of the `code` slot.
#' @slot id (`integer`) random identifier of the code element to make sure uniqueness
#' when joining.
Expand All @@ -14,11 +14,60 @@
#' @exportClass qenv
setClass(
"qenv",
slots = c(env = "environment", code = "character", id = "integer", warnings = "character", messages = "character"),
prototype = list(
env = new.env(parent = parent.env(.GlobalEnv)), code = character(0), id = integer(0),
warnings = character(0), messages = character(0)
)
slots = c(
code = "character",
id = "integer",
warnings = "character",
messages = "character"
),
contains = "environment"
)

#' It initializes the `qenv` class
#' @noRd
setMethod(
"initialize",
"qenv",
function(.Object, # nolint: object_name.
.xData, # nolint: object_name.
code = character(0L),
warnings = rep("", length(code)),
messages = rep("", length(code)),
id = integer(0L),
...) {
# # Pre-process parameters to ensure they are ready to be used by parent constructors
stopifnot("`code` must be a character or language object." = any(is.language(code), is.character(code)))

if (is.language(code)) {
code <- paste(lang2calls(code), collapse = "\n")
}
if (length(code)) {
code <- paste(code, collapse = "\n")
}

if (length(id) == 0L) {
id <- sample.int(.Machine$integer.max, size = length(code))
}

new_xdata <- if (rlang::is_missing(.xData)) {
new.env(parent = parent.env(.GlobalEnv))
} else {
checkmate::assert_environment(.xData)
rlang::env_clone(.xData, parent = parent.env(.GlobalEnv))
}
lockEnvironment(new_xdata, bindings = TRUE)

# .xData needs to be unnamed as the `.environment` constructor allows at
# most 1 unnamed formal argument of class `environment`.
# See methods::findMethods("initialize")$.environment
.Object <- methods::callNextMethod( # nolint: object_name.
# Mandatory use of `xData` to build a correct [email protected]
.Object, new_xdata,
code = code, messages = messages, warnings = warnings, id = id, ...
)

.Object
}
)

#' It takes a `qenv` class and returns `TRUE` if the input is valid
Expand All @@ -33,6 +82,8 @@ setValidity("qenv", function(object) {
"@code and @messages slots must have the same length"
} else if (any(duplicated(object@id))) {
"@id contains duplicated values."
} else if (!environmentIsLocked(object@.xData)) {
"@.xData must be locked."
} else {
TRUE
}
Expand Down
4 changes: 2 additions & 2 deletions R/qenv-concat.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ setMethod("concat", signature = c("qenv", "qenv"), function(x, y) {
y@messages <- c(x@messages, y@messages)

# insert (and overwrite) objects from y to x
y@env <- rlang::env_clone(y@env, parent = parent.env(.GlobalEnv))
rlang::env_coalesce(env = y@env, from = x@env)
y@.xData <- rlang::env_clone(y@.xData, parent = parent.env(.GlobalEnv))
rlang::env_coalesce(env = y@.xData, from = x@.xData)
y
})

Expand Down
6 changes: 2 additions & 4 deletions R/qenv-constructor.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#'
#' @name qenv
#'
#' @return Returns a `qenv` object.
#' @return `qenv` returns a `qenv` object.
#'
#' @seealso [`base::within()`], [`get_var()`], [`get_env()`], [`get_warnings()`], [`join()`], [`concat()`]
#' @examples
Expand All @@ -21,7 +21,5 @@
#'
#' @export
qenv <- function() {
q_env <- new.env(parent = parent.env(.GlobalEnv))
lockEnvironment(q_env, bindings = TRUE)
methods::new("qenv", env = q_env)
methods::new("qenv")
}
8 changes: 8 additions & 0 deletions R/qenv-errors.R
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
# needed to handle try-error
setOldClass("qenv.error")

#' @export
as.list.qenv.error <- function(x, ...) {
stop(errorCondition(
list(message = conditionMessage(x)),
class = c("validation", "try-error", "simpleError")
))
}
19 changes: 9 additions & 10 deletions R/qenv-eval_code.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#' Evaluate code in `qenv`
#'
#' @details
#' `eval_code` evaluates given code in the `qenv` environment and appends it to the `code` slot.
#' `eval_code()` evaluates given code in the `qenv` environment and appends it to the `code` slot.
#' Thus, if the `qenv` had been instantiated empty, contents of the environment are always a result of the stored code.
#'
#' @param object (`qenv`)
Expand Down Expand Up @@ -31,7 +31,7 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code
id <- sample.int(.Machine$integer.max, size = 1)

object@id <- c(object@id, id)
object@env <- rlang::env_clone(object@env, parent = parent.env(.GlobalEnv))
object@.xData <- rlang::env_clone(object@.xData, parent = parent.env(.GlobalEnv))
code <- paste(code, collapse = "\n")
object@code <- c(object@code, code)

Expand All @@ -45,11 +45,11 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code
x <- withCallingHandlers(
tryCatch(
{
eval(single_call, envir = object@env)
if (!identical(parent.env(object@env), parent.env(.GlobalEnv))) {
# needed to make sure that @env is always a sibling of .GlobalEnv
eval(single_call, envir = object@.xData)
if (!identical(parent.env(object@.xData), parent.env(.GlobalEnv))) {
# needed to make sure that @.xData is always a sibling of .GlobalEnv
# could be changed when any new package is added to search path (through library or require call)
parent.env(object@env) <- parent.env(.GlobalEnv)
parent.env(object@.xData) <- parent.env(.GlobalEnv)
}
NULL
},
Expand Down Expand Up @@ -80,20 +80,19 @@ setMethod("eval_code", signature = c("qenv", "character"), function(object, code
}
}


object@warnings <- c(object@warnings, current_warnings)
object@messages <- c(object@messages, current_messages)

lockEnvironment(object@env, bindings = TRUE)
lockEnvironment(object@.xData, bindings = TRUE)
object
})

setMethod("eval_code", signature = c("qenv", "language"), function(object, code) {
eval_code(object, code = paste(lang2calls(code), collapse = "\n"))
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) {
eval_code(object, code = paste(lang2calls(code), collapse = "\n"))
eval_code(object, code = paste(vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)), collapse = "\n"))
})

setMethod("eval_code", signature = c("qenv.error", "ANY"), function(object, code) {
Expand Down
26 changes: 25 additions & 1 deletion R/qenv-get_code.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
#' @name qenv-inheritted
#' @rdname qenv
#'
#' @details
#'
#' `x[[name]]`, `x$name` and `get(name, x)` are generic \R operators to access the objects in the environment.
#' See [`[[`] for more details.
#' `names(x)` calls on the `qenv` object and will list all objects in the environment.
#'
#' @return `[[`, `$` and `get` return the value of the object named `name` in the `qenv` object.
#' @return `names` return a character vector of all the names of the objects in the `qenv` object.
#' @return `ls` return a character vector of the names of the objects in the `qenv` object.
#' It will only show the objects that are not named with a dot prefix, unless
#' the `all.names = TRUE`, which will show all objects.
#'
#' @examples
#' # Extract objects from qenv
#' q[["a"]]
#' q$a
#'
#' # list objects in qenv
#' names(q)
NULL

#' Get code from `qenv`
#'
#' @details
#' `get_code` retrieves the code stored in the `qenv`. `...` passes arguments to methods.
#' `get_code()` retrieves the code stored in the `qenv`. `...` passes arguments to methods.
#'
#' @param object (`qenv`)
#' @param deparse (`logical(1)`) flag specifying whether to return code as `character` or `expression`.
Expand Down
15 changes: 5 additions & 10 deletions R/qenv-get_env.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#' Access environment included in `qenv`
#'
#' The access of environment included in `qenv@env` allows to e.g. list object names included in `qenv@env` slot.
#' The access of environment included in the `qenv` that contains all data objects.
#'
#' @param object (`qenv`)
#' @param object (`qenv`).
#'
#' @return An `environment` stored in `qenv@env` slot.
#' @return An `environment` stored in `qenv` slot with all data objects.
#'
#' @examples
#' q <- qenv()
Expand All @@ -13,7 +13,6 @@
#' b <- data.frame(x = 1:10)
#' })
#' get_env(q1)
#' ls(get_env(q1))
#'
#' @aliases get_env,qenv-method
#' @aliases get_env,qenv.error-method
Expand All @@ -23,10 +22,6 @@ setGeneric("get_env", function(object) {
standardGeneric("get_env")
})

setMethod("get_env", "qenv", function(object) {
object@env
})
setMethod("get_env", "qenv", function(object) [email protected])

setMethod("get_env", "qenv.error", function(object) {
object
})
setMethod("get_env", "qenv.error", function(object) object)
Loading

0 comments on commit e14dc64

Please sign in to comment.