diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c4fad42b8..bd9da0e09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,7 @@ repos: name: Regenerate package documentation additional_dependencies: - checkmate + - cli - grDevices - lifecycle - methods diff --git a/DESCRIPTION b/DESCRIPTION index 615c079e4..a0187e7e3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,13 +28,13 @@ Depends: R (>= 4.0) Imports: checkmate (>= 2.1.0), + cli (>= 3.4.0), grDevices, lifecycle (>= 0.2.0), rlang (>= 1.1.0), stats, utils Suggests: - cli (>= 3.4.0), knitr (>= 1.42), rmarkdown (>= 2.23), shiny (>= 1.6.0), @@ -45,7 +45,7 @@ VignetteBuilder: rmarkdown RdMacros: lifecycle -Config/Needs/verdepcheck: mllg/checkmate, r-lib/lifecycle, r-lib/rlang, +Config/Needs/verdepcheck: mllg/checkmate, r-lib/cli, r-lib/lifecycle, r-lib/rlang, r-lib/cli, yihui/knitr, rstudio/rmarkdown, rstudio/shiny, r-lib/testthat, r-lib/withr Config/Needs/website: insightsengineering/nesttemplate diff --git a/R/qenv-constructor.R b/R/qenv-constructor.R index b022db728..75d2757e8 100644 --- a/R/qenv-constructor.R +++ b/R/qenv-constructor.R @@ -1,28 +1,38 @@ -#' Code tracking with `qenv` object +#' Instantiates a `qenv` environment #' #' @description #' `r badge("stable")` #' -#' Create a `qenv` object and evaluate code in it to track code history. -#' -#' @param names (`character`) for `x[names]`, names of objects included in `qenv` to subset. Names not present in `qenv` -#' are skipped. For `get_code` `r lifecycle::badge("experimental")` vector of object names to return the code for. -#' For more details see the "Extracting dataset-specific code" section. +#' Instantiates a `qenv` environment. #' #' @details -#' -#' `qenv()` instantiates a `qenv` with an empty environment. -#' Any changes must be made by evaluating code in it with `eval_code` or `within`, thereby ensuring reproducibility. +#' `qenv` class has following characteristics: +#' +#' - It inherits from the environment and methods such as [`$`], [get()], [ls()], [as.list()], +#' [parent.env()] work out of the box. +#' - `qenv` is a locked environment, and data modification is only possible through the [eval_code()] +#' and [within.qenv()] functions. +#' - It stores metadata about the code used to create the data (see [get_code()]). +#' - It supports slicing (see [`subset-qenv`]) +#' - It is immutable which means that each code evaluation does not modify the original `qenv` +#' environment directly. See the following code: +#' +#' ``` +#' q1 <- qenv() +#' q2 <- eval_code(q1, "a <- 1") +#' identical(q1, q2) # FALSE +#' ``` #' #' @name qenv #' -#' @return `qenv` returns a `qenv` object. +#' @return `qenv` environment. #' -#' @seealso [`base::within()`], [`get_var()`], [`get_env()`], [`get_warnings()`], [`join()`], [`concat()`] +#' @seealso [eval_code()], [get_var()], [`subset-qenv`], [get_env()],[get_warnings()], [join()], [concat()] #' @examples -#' # create empty qenv -#' qenv() -#' +#' q <- qenv() +#' q2 <- within(q, a <- 1) +#' ls(q2) +#' q2$a #' @export qenv <- function() { methods::new("qenv") diff --git a/R/qenv-eval_code.R b/R/qenv-eval_code.R index e8166b473..b8593a33f 100644 --- a/R/qenv-eval_code.R +++ b/R/qenv-eval_code.R @@ -1,6 +1,7 @@ #' Evaluate code in `qenv` #' #' @details +#' #' `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. #' @@ -10,7 +11,7 @@ #' `expression` being a result of `parse(keep.source = TRUE)`. #' #' @return -#' `eval_code` returns a `qenv` object with `expr` evaluated or `qenv.error` if evaluation fails. +#' `qenv` environment with `code/expr` evaluated or `qenv.error` if evaluation fails. #' #' @examples #' # evaluate code in qenv @@ -20,8 +21,6 @@ #' q <- eval_code(q, quote(library(checkmate))) #' q <- eval_code(q, expression(assert_number(a))) #' -#' @name eval_code -#' @rdname qenv #' @aliases eval_code,qenv,character-method #' @aliases eval_code,qenv,language-method #' @aliases eval_code,qenv,expression-method diff --git a/R/qenv-extract.R b/R/qenv-extract.R index 0af752320..48c63d2a5 100644 --- a/R/qenv-extract.R +++ b/R/qenv-extract.R @@ -1,20 +1,21 @@ +#' Subsets `qenv` #' -#' @section Subsetting: -#' `x[names]` subsets objects in `qenv` environment and limit the code to the necessary needed to build limited objects. -#' `...` passes parameters to further methods. +#' @description +#' Subsets [`qenv`] environment and limits the code to the necessary needed to build limited objects. #' #' @param x (`qenv`) +#' @param names (`character`) names of objects included in [`qenv`] to subset. Names not present in [`qenv`] +#' are skipped. +#' @param ... internal usage, please ignore. #' -#' @examples +#' @name subset-qenv #' -#' # Subsetting +#' @examples #' q <- qenv() #' q <- eval_code(q, "a <- 1;b<-2") #' q["a"] #' q[c("a", "b")] #' -#' @rdname qenv -#' #' @export `[.qenv` <- function(x, names, ...) { checkmate::assert_character(names, any.missing = FALSE) diff --git a/R/qenv-get_code.R b/R/qenv-get_code.R index 1c076c335..136096100 100644 --- a/R/qenv-get_code.R +++ b/R/qenv-get_code.R @@ -1,39 +1,17 @@ -#' @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. +#' @description +#' Retrieves the code stored in the `qenv`. #' #' @param object (`qenv`) #' @param deparse (`logical(1)`) flag specifying whether to return code as `character` or `expression`. -#' @param ... see `Details` -#' +#' @param ... internal usage, please ignore. +#' @param names (`character`) `r lifecycle::badge("experimental")` vector of object names to return the code for. +#' For more details see the "Extracting dataset-specific code" section. #' #' @section Extracting dataset-specific code: -#' When `names` for `get_code` is specified, the code returned will be limited to the lines needed to _create_ +#' +#' `get_code(object, names)` limits the returned code to contain only those lines needed to _create_ #' the requested objects. The code stored in the `qenv` is analyzed statically to determine #' which lines the objects of interest depend upon. The analysis works well when objects are created #' with standard infix assignment operators (see `?assignOps`) but it can fail in some situations. @@ -98,7 +76,7 @@ NULL #' - creating and evaluating language objects, _e.g._ `eval()` #' #' @return -#' `get_code` returns the traced code in the form specified by `deparse`. +#' The code used in the `qenv` in the form specified by `deparse`. #' #' @examples #' # retrieve code @@ -114,8 +92,6 @@ NULL #' q <- eval_code(q, code = c("a <- 1", "b <- 2")) #' get_code(q, names = "a") #' -#' @name get_code -#' @rdname qenv #' @aliases get_code,qenv-method #' @aliases get_code,qenv.error-method #' diff --git a/R/qenv-get_var.R b/R/qenv-get_var.R index b370de4ba..e80b4cc82 100644 --- a/R/qenv-get_var.R +++ b/R/qenv-get_var.R @@ -1,8 +1,9 @@ #' Get object from `qenv` #' #' @description -#' `r lifecycle::badge("deprecated")` by native \R operators/functions: -#' `x[[name]]`, `x$name` or [get()]. +#' `r lifecycle::badge("deprecated")` +#' Instead of [get_var()] use native \R operators/functions: +#' `x[[name]]`, `x$name` or [get()]: #' #' Retrieve variables from the `qenv` environment. #' @@ -17,8 +18,6 @@ #' q2 <- eval_code(q1, code = "b <- a") #' get_var(q2, "b") #' -#' @name get_var -#' @rdname get_var #' @aliases get_var,qenv,character-method #' @aliases get_var,qenv.error,ANY-method #' diff --git a/R/qenv-show.R b/R/qenv-show.R index b2fa7447f..e389fd76e 100644 --- a/R/qenv-show.R +++ b/R/qenv-show.R @@ -16,5 +16,36 @@ #' @importFrom methods show #' @export setMethod("show", "qenv", function(object) { - rlang::env_print(object@.xData) + env <- get_env(object) + header <- cli::col_blue(sprintf("", rlang::env_label(env))) + parent <- sprintf("Parent: ", rlang::env_label(rlang::env_parent(env))) + cat(cli::style_bold(header), "\U1F512", "\n") + cat(parent, "\n") + + shown <- ls(object) + if (length(shown > 0L)) cat(cli::style_bold("Bindings:\n")) + lapply(shown, function(x) { + cat( + sprintf( + "- %s: [%s]\n", + deparse(rlang::sym(x), backtick = TRUE), + class(object[[x]])[1] + ) + ) + }) + + hidden <- setdiff(ls(object, all.names = TRUE), shown) + lapply(hidden, function(x) { + cat( + cli::style_blurred( + sprintf( + "- %s: [%s]\n", + deparse(rlang::sym(x), backtick = TRUE), + class(object[[x]])[1] + ) + ) + ) + }) + + invisible(object) }) diff --git a/R/qenv-within.R b/R/qenv-within.R index 7d5fd32db..ef68da14b 100644 --- a/R/qenv-within.R +++ b/R/qenv-within.R @@ -1,11 +1,7 @@ -#' Evaluate Expression in `qenv` -#' #' @details -#' `within()` is a convenience function for evaluating inline code inside the environment of a `qenv`. -#' It is a method for the `base` generic that wraps `eval_code` to provide a simplified way of passing code. -#' `within` accepts only inline expressions (both simple and compound) and allows for injecting values into `expr` -#' through the `...` argument: -#' as `name:value` pairs are passed to `...`, `name` in `expr` will be replaced with `value`. +#' `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` +#' with `...` named argument values. #' #' @section Using language objects with `within`: #' Passing language objects to `expr` is generally not intended but can be achieved with `do.call`. @@ -13,10 +9,8 @@ #' #' @param data (`qenv`) #' @param expr (`expression`) to evaluate. Must be inline code, see `Using language objects...` -#' @param ... see `Details` -#' -#' @return -#' `within` returns a `qenv` object with `expr` evaluated or `qenv.error` if evaluation fails. +#' @param ... named argument value will substitute a symbol in the `expr` matched by the name. +#' For practical usage see Examples section below. #' #' @examples #' # evaluate code using within @@ -49,7 +43,7 @@ #' within(q, exprlist) # fails #' do.call(within, list(q, do.call(c, exprlist))) #' -#' @rdname qenv +#' @rdname eval_code #' #' @export #' diff --git a/_pkgdown.yml b/_pkgdown.yml index 7837af587..e4a8c973c 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -37,4 +37,5 @@ reference: - join - qenv - show,qenv-method + - subset-qenv - within.qenv diff --git a/man/eval_code.Rd b/man/eval_code.Rd new file mode 100644 index 000000000..203a96af0 --- /dev/null +++ b/man/eval_code.Rd @@ -0,0 +1,88 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/qenv-eval_code.R, R/qenv-within.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} +\title{Evaluate code in \code{qenv}} +\usage{ +eval_code(object, code) + +\method{within}{qenv}(data, expr, ...) +} +\arguments{ +\item{object}{(\code{qenv})} + +\item{code}{(\code{character}, \code{language} or \code{expression}) code to evaluate. +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.} +} +\value{ +\code{qenv} environment with \code{code/expr} evaluated or \code{qenv.error} if evaluation fails. +} +\description{ +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() +q <- eval_code(q, "a <- 1") +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))) + +} diff --git a/man/get_code.Rd b/man/get_code.Rd new file mode 100644 index 000000000..af8ca3c0e --- /dev/null +++ b/man/get_code.Rd @@ -0,0 +1,113 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/qenv-get_code.R +\name{get_code} +\alias{get_code} +\alias{get_code,qenv-method} +\alias{get_code,qenv.error-method} +\title{Get code from \code{qenv}} +\usage{ +get_code(object, deparse = TRUE, names = NULL, ...) +} +\arguments{ +\item{object}{(\code{qenv})} + +\item{deparse}{(\code{logical(1)}) flag specifying whether to return code as \code{character} or \code{expression}.} + +\item{names}{(\code{character}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} vector of object names to return the code for. +For more details see the "Extracting dataset-specific code" section.} + +\item{...}{internal usage, please ignore.} +} +\value{ +The code used in the \code{qenv} in the form specified by \code{deparse}. +} +\description{ +Retrieves the code stored in the \code{qenv}. +} +\section{Extracting dataset-specific code}{ + + +\code{get_code(object, names)} limits the returned code to contain only those lines needed to \emph{create} +the requested objects. The code stored in the \code{qenv} is analyzed statically to determine +which lines the objects of interest depend upon. The analysis works well when objects are created +with standard infix assignment operators (see \code{?assignOps}) but it can fail in some situations. + +Consider the following examples: + +\emph{Case 1: Usual assignments.} + +\if{html}{\out{
}}\preformatted{q1 <- + within(qenv(), \{ + foo <- function(x) \{ + x + 1 + \} + x <- 0 + y <- foo(x) + \}) +get_code(q1, names = "y") +}\if{html}{\out{
}} + +\code{x} has no dependencies, so \code{get_code(data, names = "x")} will return only the second call.\cr +\code{y} depends on \code{x} and \code{foo}, so \code{get_code(data, names = "y")} will contain all three calls. + +\emph{Case 2: Some objects are created by a function's side effects.} + +\if{html}{\out{
}}\preformatted{q2 <- + within(qenv()\{ + foo <- function() \{ + x <<- x + 1 + \} + x <- 0 + foo() + y <- x + \}) +get_code(q2, names = "y") +}\if{html}{\out{
}} + +Here, \code{y} depends on \code{x} but \code{x} is modified by \code{foo} as a side effect (not by reassignment) +and so \code{get_code(data, names = "y")} will not return the \code{foo()} call.\cr +To overcome this limitation, code dependencies can be specified manually. +Lines where side effects occur can be flagged by adding "\verb{# @linksto }" at the end.\cr +Note that \code{within} evaluates code passed to \code{expr} as is and comments are ignored. +In order to include comments in code one must use the \code{eval_code} function instead. + +\if{html}{\out{
}}\preformatted{q3 <- + eval_code(qenv(), " + foo <- function() \{ + x <<- x + 1 + \} + x <- 0 + foo() # @linksto x + y <- x + ") +get_code(q3, names = "y") +}\if{html}{\out{
}} + +Now the \code{foo()} call will be properly included in the code required to recreate \code{y}. + +Note that two functions that create objects as side effects, \code{assign} and \code{data}, are handled automatically. + +Here are known cases where manual tagging is necessary: +\itemize{ +\item non-standard assignment operators, \emph{e.g.} \verb{\%<>\%} +\item objects used as conditions in \code{if} statements: \verb{if ()} +\item objects used to iterate over in \code{for} loops: \verb{for(i in )} +\item creating and evaluating language objects, \emph{e.g.} \verb{eval()} +} +} + +\examples{ +# retrieve code +q <- within(qenv(), { + a <- 1 + b <- 2 +}) +get_code(q) +get_code(q, deparse = FALSE) +get_code(q, names = "a") + +q <- qenv() +q <- eval_code(q, code = c("a <- 1", "b <- 2")) +get_code(q, names = "a") + +} diff --git a/man/get_var.Rd b/man/get_var.Rd index 7b40bbaa1..ff8b71207 100644 --- a/man/get_var.Rd +++ b/man/get_var.Rd @@ -20,8 +20,9 @@ get_var(object, var) The value of required variable (\code{var}) within \code{qenv} object. } \description{ -\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} by native \R operators/functions: -\code{x[[name]]}, \code{x$name} or \code{\link[=get]{get()}}. +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} +Instead of \code{\link[=get_var]{get_var()}} use native \R operators/functions: +\code{x[[name]]}, \code{x$name} or \code{\link[=get]{get()}}: Retrieve variables from the \code{qenv} environment. } diff --git a/man/qenv.Rd b/man/qenv.Rd index 0d45c8f36..ac9c1a90a 100644 --- a/man/qenv.Rd +++ b/man/qenv.Rd @@ -1,244 +1,43 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/qenv-constructor.R, R/qenv-eval_code.R, -% R/qenv-extract.R, R/qenv-get_code.R, R/qenv-within.R +% Please edit documentation in R/qenv-constructor.R \name{qenv} \alias{qenv} -\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{[.qenv} -\alias{qenv-inheritted} -\alias{get_code} -\alias{get_code,qenv-method} -\alias{get_code,qenv.error-method} -\alias{within.qenv} -\title{Code tracking with \code{qenv} object} +\title{Instantiates a \code{qenv} environment} \usage{ qenv() - -eval_code(object, code) - -\method{[}{qenv}(x, names, ...) - -get_code(object, deparse = TRUE, names = NULL, ...) - -\method{within}{qenv}(data, expr, ...) -} -\arguments{ -\item{object}{(\code{qenv})} - -\item{code}{(\code{character}, \code{language} or \code{expression}) code to evaluate. -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{x}{(\code{qenv})} - -\item{names}{(\code{character}) for \code{x[names]}, names of objects included in \code{qenv} to subset. Names not present in \code{qenv} -are skipped. For \code{get_code} \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} vector of object names to return the code for. -For more details see the "Extracting dataset-specific code" section.} - -\item{...}{see \code{Details}} - -\item{deparse}{(\code{logical(1)}) flag specifying whether to return code as \code{character} or \code{expression}.} - -\item{data}{(\code{qenv})} - -\item{expr}{(\code{expression}) to evaluate. Must be inline code, see \verb{Using language objects...}} } \value{ -\code{qenv} returns a \code{qenv} object. - -\code{eval_code} returns a \code{qenv} object with \code{expr} evaluated or \code{qenv.error} if evaluation fails. - -\code{[[}, \code{$} and \code{get} return the value of the object named \code{name} in the \code{qenv} object. - -\code{names} return a character vector of all the names of the objects in the \code{qenv} object. - -\code{ls} return a character vector of the names of the objects in the \code{qenv} object. -It will only show the objects that are not named with a dot prefix, unless -the \code{all.names = TRUE}, which will show all objects. - -\code{get_code} returns the traced code in the form specified by \code{deparse}. - -\code{within} returns a \code{qenv} object with \code{expr} evaluated or \code{qenv.error} if evaluation fails. +\code{qenv} environment. } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#stable}{\figure{lifecycle-stable.svg}{options: alt='[Stable]'}}}{\strong{[Stable]}} -Create a \code{qenv} object and evaluate code in it to track code history. +Instantiates a \code{qenv} environment. } \details{ -\code{qenv()} instantiates a \code{qenv} with an empty environment. -Any changes must be made by evaluating code in it with \code{eval_code} or \code{within}, thereby ensuring reproducibility. - -\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{x[[name]]}, \code{x$name} and \code{get(name, x)} are generic \R operators to access the objects in the environment. -See [\code{[[}] for more details. -\code{names(x)} calls on the \code{qenv} object and will list all objects in the environment. - -\code{get_code()} retrieves the code stored in the \code{qenv}. \code{...} passes arguments to methods. - -\code{within()} is a convenience function for evaluating inline code inside the environment of a \code{qenv}. -It is a method for the \code{base} generic that wraps \code{eval_code} to provide a simplified way of passing code. -\code{within} accepts only inline expressions (both simple and compound) and allows for injecting values into \code{expr} -through the \code{...} argument: -as \code{name:value} pairs are passed to \code{...}, \code{name} in \code{expr} will be replaced with \code{value}. -} -\section{Subsetting}{ - -\code{x[names]} subsets objects in \code{qenv} environment and limit the code to the necessary needed to build limited objects. -\code{...} passes parameters to further methods. -} - -\section{Extracting dataset-specific code}{ - -When \code{names} for \code{get_code} is specified, the code returned will be limited to the lines needed to \emph{create} -the requested objects. The code stored in the \code{qenv} is analyzed statically to determine -which lines the objects of interest depend upon. The analysis works well when objects are created -with standard infix assignment operators (see \code{?assignOps}) but it can fail in some situations. - -Consider the following examples: - -\emph{Case 1: Usual assignments.} - -\if{html}{\out{
}}\preformatted{q1 <- - within(qenv(), \{ - foo <- function(x) \{ - x + 1 - \} - x <- 0 - y <- foo(x) - \}) -get_code(q1, names = "y") -}\if{html}{\out{
}} - -\code{x} has no dependencies, so \code{get_code(data, names = "x")} will return only the second call.\cr -\code{y} depends on \code{x} and \code{foo}, so \code{get_code(data, names = "y")} will contain all three calls. - -\emph{Case 2: Some objects are created by a function's side effects.} - -\if{html}{\out{
}}\preformatted{q2 <- - within(qenv()\{ - foo <- function() \{ - x <<- x + 1 - \} - x <- 0 - foo() - y <- x - \}) -get_code(q2, names = "y") -}\if{html}{\out{
}} - -Here, \code{y} depends on \code{x} but \code{x} is modified by \code{foo} as a side effect (not by reassignment) -and so \code{get_code(data, names = "y")} will not return the \code{foo()} call.\cr -To overcome this limitation, code dependencies can be specified manually. -Lines where side effects occur can be flagged by adding "\verb{# @linksto }" at the end.\cr -Note that \code{within} evaluates code passed to \code{expr} as is and comments are ignored. -In order to include comments in code one must use the \code{eval_code} function instead. - -\if{html}{\out{
}}\preformatted{q3 <- - eval_code(qenv(), " - foo <- function() \{ - x <<- x + 1 - \} - x <- 0 - foo() # @linksto x - y <- x - ") -get_code(q3, names = "y") -}\if{html}{\out{
}} - -Now the \code{foo()} call will be properly included in the code required to recreate \code{y}. - -Note that two functions that create objects as side effects, \code{assign} and \code{data}, are handled automatically. - -Here are known cases where manual tagging is necessary: +\code{qenv} class has following characteristics: \itemize{ -\item non-standard assignment operators, \emph{e.g.} \verb{\%<>\%} -\item objects used as conditions in \code{if} statements: \verb{if ()} -\item objects used to iterate over in \code{for} loops: \verb{for(i in )} -\item creating and evaluating language objects, \emph{e.g.} \verb{eval()} -} +\item It inherits from the environment and methods such as \code{\link{$}}, \code{\link[=get]{get()}}, \code{\link[=ls]{ls()}}, \code{\link[=as.list]{as.list()}}, +\code{\link[=parent.env]{parent.env()}} work out of the box. +\item \code{qenv} is a locked environment, and data modification is only possible through the \code{\link[=eval_code]{eval_code()}} +and \code{\link[=within.qenv]{within.qenv()}} functions. +\item It stores metadata about the code used to create the data (see \code{\link[=get_code]{get_code()}}). +\item It supports slicing (see \code{\link{subset-qenv}}) +\item It is immutable which means that each code evaluation does not modify the original \code{qenv} +environment directly. See the following code: + +\if{html}{\out{
}}\preformatted{q1 <- qenv() +q2 <- eval_code(q1, "a <- 1") +identical(q1, q2) # FALSE +}\if{html}{\out{
}} } - -\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{ -# create empty qenv -qenv() - -# evaluate code in qenv -q <- qenv() -q <- eval_code(q, "a <- 1") -q <- eval_code(q, "b <- 2L # with comment") -q <- eval_code(q, quote(library(checkmate))) -q <- eval_code(q, expression(assert_number(a))) - - -# Subsetting -q <- qenv() -q <- eval_code(q, "a <- 1;b<-2") -q["a"] -q[c("a", "b")] - -# Extract objects from qenv -q[["a"]] -q$a - -# list objects in qenv -names(q) -# retrieve code -q <- within(qenv(), { - a <- 1 - b <- 2 -}) -get_code(q) -get_code(q, deparse = FALSE) -get_code(q, names = "a") - q <- qenv() -q <- eval_code(q, code = c("a <- 1", "b <- 2")) -get_code(q, names = "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))) - +q2 <- within(q, a <- 1) +ls(q2) +q2$a } \seealso{ -\code{\link[base:with]{base::within()}}, \code{\link[=get_var]{get_var()}}, \code{\link[=get_env]{get_env()}}, \code{\link[=get_warnings]{get_warnings()}}, \code{\link[=join]{join()}}, \code{\link[=concat]{concat()}} +\code{\link[=eval_code]{eval_code()}}, \code{\link[=get_var]{get_var()}}, \code{\link{subset-qenv}}, \code{\link[=get_env]{get_env()}},\code{\link[=get_warnings]{get_warnings()}}, \code{\link[=join]{join()}}, \code{\link[=concat]{concat()}} } diff --git a/man/subset-qenv.Rd b/man/subset-qenv.Rd new file mode 100644 index 000000000..8ae6b2ff4 --- /dev/null +++ b/man/subset-qenv.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/qenv-extract.R +\name{subset-qenv} +\alias{subset-qenv} +\alias{[.qenv} +\title{Subsets \code{qenv}} +\usage{ +\method{[}{qenv}(x, names, ...) +} +\arguments{ +\item{x}{(\code{qenv})} + +\item{names}{(\code{character}) names of objects included in \code{\link{qenv}} to subset. Names not present in \code{\link{qenv}} +are skipped.} + +\item{...}{internal usage, please ignore.} +} +\description{ +Subsets \code{\link{qenv}} environment and limits the code to the necessary needed to build limited objects. +} +\examples{ +q <- qenv() +q <- eval_code(q, "a <- 1;b<-2") +q["a"] +q[c("a", "b")] + +} diff --git a/vignettes/qenv.Rmd b/vignettes/qenv.Rmd index 2db54b4bf..4859e32c5 100644 --- a/vignettes/qenv.Rmd +++ b/vignettes/qenv.Rmd @@ -10,7 +10,13 @@ vignette: > ## Introduction to `qenv` -A `qenv` is an R object which contains code and an environment and can be used to create reproducible outputs. +A `qenv` inherits from the `environment` class, behaves like an environment, and has the following characteristics: + +- It inherits from the environment and methods such as `$`, `get`, `ls`, `as.list()` work out of the box. +- `qenv` is a locked environment, and data modification is only possible through the `eval_code` and `within` functions. +- It stores metadata about the code used to create the data (see `get_code`). +- It supports slicing by `[`. +- It is immutable which means that each code evaluation does not modify the original `qenv` environment directly. ### Initialization @@ -26,14 +32,12 @@ print(empty_qenv) ### `qenv` basic usage -The `eval_code()` function executes code within a `qenv` environment, yielding a new `qenv` object as the output. +To modify the data use `eval_code` to execute R code within the environment, yielding a new `qenv` object as the output. ```{r} # evaluate code in qenv my_qenv <- eval_code(empty_qenv, "x <- 2") print(my_qenv) -as.environment(my_qenv) - q1 <- eval_code(my_qenv, "y <- x * 2") q1 <- eval_code(q1, "z <- y * 2") @@ -47,16 +51,14 @@ print(q1) names(q1) ``` -The same result can be achieved with the `within` method for the `qenv` class. +The same result can be achieved with the `within` method. + ```{r} q2 <- within(my_qenv, y <- x * 2) q2 <- within(q2, z <- y * 2) print(q2) ``` - - - To extract objects from a `qenv`, use `[[`; this is particularly useful for displaying them in a `shiny` app. You can retrieve the code used to generate the `qenv` using the `get_code()` function. ```{r} @@ -66,8 +68,10 @@ cat(get_code(q2)) ``` ### Substitutions + In some cases, one may want to substitute some elements of the code before evaluation. Consider a case when a subset of `iris` is defined by an input value. + ```{r} q <- qenv() q <- eval_code(q, quote(i <- subset(iris, Species == "setosa"))) @@ -87,6 +91,7 @@ summary(q[["iii"]]$Species) ``` A more convenient way to pass code with substitution is to use the `within` method. + ```{r} qq <- qenv() qq <- within(qq, i <- subset(iris, Species == "setosa")) @@ -101,7 +106,6 @@ summary(qq[["iii"]]$Species) See `?qenv` for more details. - ### Combining `qenv` objects Given a pair of `qenv` objects, you may be able to "join" them, creating a new `qenv` object encompassing the union of both environments, along with the requisite code for reproduction: diff --git a/vignettes/teal-code.Rmd b/vignettes/teal-code.Rmd index aee2c842a..1d205267b 100644 --- a/vignettes/teal-code.Rmd +++ b/vignettes/teal-code.Rmd @@ -18,5 +18,3 @@ In this context, the `qenv` object within the `teal.code` package emerges as a p It's worth noting that there exists a public [`shinymeta`](https://github.com/rstudio/shinymeta) R package by `RStudio`, offering similar functionality. However, integrating `shinymeta` into `teal` modules poses challenges. Consequently, we recommend the use of `qenv` for teal-based applications. For comprehensive insights, please refer to the [`qenv` vignette](https://insightsengineering.github.io/teal.code/latest-tag/articles/qenv.html). - -Important Note: The legacy approach to handling reproducibility, known as `chunks`, has been deprecated. We strongly advocate for the adoption of `qenv` as its successor.