diff --git a/DESCRIPTION b/DESCRIPTION index 52025189d..01b176742 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -78,6 +78,5 @@ Collate: 'teal_data.R' 'testhat-helpers.R' 'topological_sort.R' - 'utils-get_code_dependency.R' 'verify.R' 'zzz.R' diff --git a/NEWS.md b/NEWS.md index 76287f165..d38e183b8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # teal.data 0.6.0.9013 +### Breaking changes + +- soft deprecate `datanames` argument of `get_code()`. Use `names` instead. + ### Enhancements - `datanames()` diff --git a/R/teal_data-get_code.R b/R/teal_data-get_code.R index 5cc5c6494..1fd668193 100644 --- a/R/teal_data-get_code.R +++ b/R/teal_data-get_code.R @@ -3,10 +3,10 @@ #' Retrieve code from `teal_data` object. #' #' Retrieve code stored in `@code`, which (in principle) can be used to recreate all objects found in `@env`. -#' Use `datanames` to limit the code to one or more of the datasets enumerated in `@datanames`. +#' Use `names` to limit the code to one or more of the datasets enumerated in `@datanames`. #' #' @section Extracting dataset-specific code: -#' When `datanames` is specified, the code returned will be limited to the lines needed to _create_ +#' When `names` is specified, the code returned will be limited to the lines needed to _create_ #' the requested datasets. The code stored in the `@code` slot is analyzed statically to determine #' which lines the datasets 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. @@ -23,10 +23,10 @@ #' x <- 0 #' y <- foo(x) #' }) -#' get_code(data, datanames = "y") +#' get_code(data, names = "y") #' ``` -#' `x` has no dependencies, so `get_code(data, datanames = "x")` will return only the second call.\cr -#' `y` depends on `x` and `foo`, so `get_code(data, datanames = "y")` will contain all three calls. +#' `x` has no dependencies, so `get_code(data, names = "x")` will return only the second call.\cr +#' `y` depends on `x` and `foo`, so `get_code(data, names = "y")` will contain all three calls. #' #' _Case 2: Some objects are created by a function's side effects._ #' ```r @@ -39,10 +39,10 @@ #' foo() #' y <- x #' }) -#' get_code(data, datanames = "y") +#' get_code(data, names = "y") #' ``` #' Here, `y` depends on `x` but `x` is modified by `foo` as a side effect (not by reassignment) -#' and so `get_code(data, datanames = "y")` will not return the `foo()` call.\cr +#' and so `get_code(data, names = "y")` will not return the `foo()` call.\cr #' To overcome this limitation, code dependencies can be specified manually. #' Lines where side effects occur can be flagged by adding "`# @linksto `" at the end.\cr #' Note that `within` evaluates code passed to `expr` as is and comments are ignored. @@ -58,7 +58,7 @@ #' foo() # @linksto x #' y <- x #' ") -#' get_code(data, datanames = "y") +#' get_code(data, names = "y") #' ``` #' Now the `foo()` call will be properly included in the code required to recreate `y`. #' @@ -72,7 +72,9 @@ #' #' #' @param object (`teal_data`) -#' @param datanames `r lifecycle::badge("experimental")` (`character`) vector of dataset names to return the code for. +#' @param datanames `r lifecycle::badge("deprecated")` (`character`) vector of dataset names to return the code for. +#' For more details see the "Extracting dataset-specific code" section. Use `names` instead. +#' @param names (`character`) Successor of `datanames`. Vector of dataset names to return the code for. #' For more details see the "Extracting dataset-specific code" section. #' @param deparse (`logical`) flag specifying whether to return code as `character` (`deparse = TRUE`) or as #' `expression` (`deparse = FALSE`). @@ -81,7 +83,7 @@ #' `code` but are passed in `datanames`. To remove the warning, set `check_names = FALSE`. #' #' @return -#' Either a character string or an expression. If `datanames` is used to request a specific dataset, +#' Either a character string or an expression. If `names` is used to request a specific dataset, #' only code that _creates_ that dataset (not code that uses it) is returned. Otherwise, all contents of `@code`. #' #' @examples @@ -92,8 +94,8 @@ #' c <- list(x = 2) #' }) #' get_code(tdata1) -#' get_code(tdata1, datanames = "a") -#' get_code(tdata1, datanames = "b") +#' get_code(tdata1, names = "a") +#' get_code(tdata1, names = "b") #' #' tdata2 <- teal_data(x1 = iris, code = "x1 <- iris") #' get_code(tdata2) @@ -103,28 +105,26 @@ #' @aliases get_code,teal_data-method #' #' @export -setMethod("get_code", signature = "teal_data", definition = function(object, deparse = TRUE, datanames = NULL, ...) { - checkmate::assert_character(datanames, min.len = 1L, null.ok = TRUE) - checkmate::assert_flag(deparse) +setMethod("get_code", + signature = "teal_data", + definition = function(object, deparse = TRUE, names = NULL, datanames = lifecycle::deprecated(), ...) { + if (lifecycle::is_present(datanames)) { + lifecycle::deprecate_warn( + when = "0.6.1", + what = "teal.data::get_code(datanames)", + with = "teal.code::get_code(names)", + always = TRUE + ) + names <- datanames + } - # Normalize in case special it is backticked - if (!is.null(datanames)) { - datanames <- gsub("^`(.*)`$", "\\1", datanames) - } + if (!is.null(names) && lifecycle::is_present(datanames)) { + stop("Please use either 'names' (recommended) or 'datanames' parameter.") + } - code <- if (!is.null(datanames)) { - get_code_dependency(object@code, datanames, ...) - } else { - object@code - } + checkmate::assert_character(names, min.len = 1L, null.ok = TRUE) + checkmate::assert_flag(deparse) - if (deparse) { - if (length(code) == 0) { - code - } else { - paste(code, collapse = "\n") - } - } else { - parse(text = paste(c("{", code, "}"), collapse = "\n"), keep.source = TRUE) + methods::callNextMethod(object = object, deparse = deparse, names = names, ...) } -}) +) diff --git a/R/utils-get_code_dependency.R b/R/utils-get_code_dependency.R deleted file mode 100644 index 1f549c360..000000000 --- a/R/utils-get_code_dependency.R +++ /dev/null @@ -1,455 +0,0 @@ -# get_code_dependency ---- - -#' Get code dependency of an object -#' -#' Extract subset of code required to reproduce specific object(s), including code producing side-effects. -#' -#' Given a character vector with code, this function will extract the part of the code responsible for creating -#' the variables specified by `names`. -#' This includes the final call that creates the variable(s) in question as well as all _parent calls_, -#' _i.e._ calls that create variables used in the final call and their parents, etc. -#' Also included are calls that create side-effects like establishing connections. -#' -#' It is assumed that object dependency is established by using three assignment operators: `<-`, `=`, and `->` . -#' Other assignment methods (`assign`, `<<-`) or non-standard-evaluation methods are not supported. -#' -#' Side-effects are not detected automatically and must be marked in the code. -#' Add `# @linksto object` at the end of a line where a side-effect occurs to specify that this line is required -#' to reproduce a variable called `object`. -#' -#' @param code `character` with the code. -#' @param names `character` vector of object names. -#' @param check_names `logical(1)` flag specifying if a warning for non-existing names should be displayed. -#' -#' @return Character vector, a subset of `code`. -#' Note that subsetting is actually done on the calls `code`, not necessarily on the elements of the vector. -#' -#' @keywords internal -get_code_dependency <- function(code, names, check_names = TRUE) { - checkmate::assert_character(code) - checkmate::assert_character(names, any.missing = FALSE) - - if (identical(code, character(0)) || identical(trimws(code), "")) { - return(code) - } - - # If code is bound in curly brackets, remove them. - tcode <- trimws(code) - if (any(grepl("^\\{.*\\}$", tcode))) { - code <- sub("^\\{(.*)\\}$", "\\1", tcode) - } - - - code <- parse(text = code, keep.source = TRUE) - pd <- utils::getParseData(code) - pd <- normalize_pd(pd) - calls_pd <- extract_calls(pd) - - - if (check_names) { - # Detect if names are actually in code. - symbols <- unlist(lapply(calls_pd, function(call) call[call$token == "SYMBOL", "text"])) - if (any(pd$text == "assign")) { - assign_calls <- Filter(function(call) find_call(call, "assign"), calls_pd) - ass_str <- unlist(lapply(assign_calls, function(call) call[call$token == "STR_CONST", "text"])) - ass_str <- gsub("^['\"]|['\"]$", "", ass_str) - symbols <- c(ass_str, symbols) - } - if (!all(names %in% unique(symbols))) { - warning("Object(s) not found in code: ", toString(setdiff(names, symbols))) - } - } - - graph <- code_graph(calls_pd) - ind <- unlist(lapply(names, function(x) graph_parser(x, graph))) - - lib_ind <- detect_libraries(calls_pd) - - as.character(code[sort(unique(c(lib_ind, ind)))]) -} - -#' Locate function call token -#' -#' Determine which row of parsed data is specific `SYMBOL_FUNCTION_CALL` token. -#' -#' Useful for determining occurrence of `assign` or `data` functions in an input call. -#' -#' @param call_pd `data.frame` as returned by `extract_calls()` -#' @param text `character(1)` to look for in `text` column of `call_pd` -#' -#' @return -#' Single integer specifying row in `call_pd` where `token` is `SYMBOL_FUNCTION_CALL` and `text` is `text`. -#' 0 if not found. -#' -#' @keywords internal -#' @noRd -find_call <- function(call_pd, text) { - checkmate::check_data_frame(call_pd) - checkmate::check_names(call_pd, must.include = c("token", "text")) - checkmate::check_string(text) - - ans <- which(call_pd$token == "SYMBOL_FUNCTION_CALL" & call_pd$text == text) - if (length(ans)) { - ans - } else { - 0L - } -} - -#' Split the result of `utils::getParseData()` into separate calls -#' -#' @param pd (`data.frame`) A result of `utils::getParseData()`. -#' -#' @return -#' A `list` of `data.frame`s. -#' Each element is a subset of `pd` corresponding to one call in the original code from which `pd` was obtained. -#' Only four columns (`"token"`, `"text"`, `"id"`, `"parent"`) are kept, the rest is discarded. -#' -#' @keywords internal -#' @noRd -extract_calls <- function(pd) { - calls <- lapply( - pd[pd$parent == 0, "id"], - function(parent) { - rbind( - pd[pd$id == parent, c("token", "text", "id", "parent")], - get_children(pd = pd, parent = parent) - ) - } - ) - calls <- Filter(function(call) !(nrow(call) == 1 && call$token == "';'"), calls) - calls <- Filter(Negate(is.null), calls) - calls <- fix_shifted_comments(calls) - fix_arrows(calls) -} - -#' @keywords internal -#' @noRd -get_children <- function(pd, parent) { - idx_children <- abs(pd$parent) == parent - children <- pd[idx_children, c("token", "text", "id", "parent")] - if (nrow(children) == 0) { - return(NULL) - } - - if (parent > 0) { - do.call(rbind, c(list(children), lapply(children$id, get_children, pd = pd))) - } -} - -#' Fixes edge case of comments being shifted to the next call. -#' @keywords internal -#' @noRd -fix_shifted_comments <- function(calls) { - # If the first or the second token is a @linksto COMMENT, - # then it belongs to the previous call. - if (length(calls) >= 2) { - for (i in 2:length(calls)) { - comment_idx <- grep("@linksto", calls[[i]][, "text"]) - if (isTRUE(comment_idx[1] <= 2)) { - calls[[i - 1]] <- rbind( - calls[[i - 1]], - calls[[i]][seq_len(comment_idx[1]), ] - ) - calls[[i]] <- calls[[i]][-seq_len(comment_idx[1]), ] - } - } - } - Filter(nrow, calls) -} - -#' Fixes edge case of `<-` assignment operator being called as function, -#' which is \code{`<-`(y,x)} instead of traditional `y <- x`. -#' @keywords internal -#' @noRd -fix_arrows <- function(calls) { - checkmate::assert_list(calls) - lapply(calls, function(call) { - sym_fun <- call$token == "SYMBOL_FUNCTION_CALL" - call[sym_fun, ] <- sub_arrows(call[sym_fun, ]) - call - }) -} - -#' Execution of assignment operator substitutions for a call. -#' @keywords internal -#' @noRd -sub_arrows <- function(call) { - checkmate::assert_data_frame(call) - map <- data.frame( - row.names = c("<-", "<<-", "="), - token = rep("LEFT_ASSIGN", 3), - text = rep("<-", 3) - ) - sub_ids <- call$text %in% rownames(map) - call[sub_ids, c("token", "text")] <- map[call$text[sub_ids], ] - call -} - -# code_graph ---- - -#' Create object dependencies graph within parsed code -#' -#' Builds dependency graph that identifies dependencies between objects in parsed code. -#' Helps understand which objects depend on which. -#' -#' @param calls_pd `list` of `data.frame`s; -#' result of `utils::getParseData()` split into subsets representing individual calls; -#' created by `extract_calls()` function -#' -#' @return -#' A list (of length of input `calls_pd`) where each element represents one call. -#' Each element is a character vector listing names of objects that depend on this call -#' and names of objects that this call depends on. -#' Dependencies are listed after the `"<-"` string, e.g. `c("a", "<-", "b", "c")` means that in this call object `a` -#' depends on objects `b` and `c`. -#' If a call is tagged with `@linksto a`, then object `a` is understood to depend on that call. -#' -#' @keywords internal -#' @noRd -code_graph <- function(calls_pd) { - cooccurrence <- extract_occurrence(calls_pd) - - side_effects <- extract_side_effects(calls_pd) - - mapply(c, side_effects, cooccurrence, SIMPLIFY = FALSE) -} - -#' Extract object occurrence -#' -#' Extracts objects occurrence within calls passed by `calls_pd`. -#' Also detects which objects depend on which within a call. -#' -#' @param calls_pd `list` of `data.frame`s; -#' result of `utils::getParseData()` split into subsets representing individual calls; -#' created by `extract_calls()` function -#' -#' @return -#' A list (of length of input `calls_pd`) where each element represents one call. -#' Each element is a character vector listing names of objects that depend on this call -#' and names of objects that this call depends on. -#' Dependencies are listed after the `"<-"` string, e.g. `c("a", "<-", "b", "c")` means that in this call object `a` -#' depends on objects `b` and `c`. -#' If a call is tagged with `@linksto a`, then object `a` is understood to depend on that call. -#' -#' @keywords internal -#' @noRd -extract_occurrence <- function(calls_pd) { - is_in_function <- function(x) { - # If an object is a function parameter, - # then in calls_pd there is a `SYMBOL_FORMALS` entry for that object. - function_id <- x[x$token == "FUNCTION", "parent"] - if (length(function_id)) { - x$id %in% get_children(x, function_id[1])$id - } else { - rep(FALSE, nrow(x)) - } - } - in_parenthesis <- function(x) { - if (any(x$token %in% c("LBB", "'['"))) { - id_start <- min(x$id[x$token %in% c("LBB", "'['")]) - id_end <- min(x$id[x$token == "']'"]) - x$text[x$token == "SYMBOL" & x$id > id_start & x$id < id_end] - } - } - lapply( - calls_pd, - function(call_pd) { - # Handle data(object)/data("object")/data(object, envir = ) independently. - data_call <- find_call(call_pd, "data") - if (data_call) { - sym <- call_pd[data_call + 1, "text"] - return(c(gsub("^['\"]|['\"]$", "", sym), "<-")) - } - # Handle assign(x = ). - assign_call <- find_call(call_pd, "assign") - if (assign_call) { - # Check if parameters were named. - # "','" is for unnamed parameters, where "SYMBOL_SUB" is for named. - # "EQ_SUB" is for `=` appearing after the name of the named parameter. - if (any(call_pd$token == "SYMBOL_SUB")) { - params <- call_pd[call_pd$token %in% c("SYMBOL_SUB", "','", "EQ_SUB"), "text"] - # Remove sequence of "=", ",". - if (length(params > 1)) { - remove <- integer(0) - for (i in 2:length(params)) { - if (params[i - 1] == "=" & params[i] == ",") { - remove <- c(remove, i - 1, i) - } - } - if (length(remove)) params <- params[-remove] - } - pos <- match("x", setdiff(params, ","), nomatch = match(",", params, nomatch = 0)) - if (!pos) { - return(character(0L)) - } - # pos is indicator of the place of 'x' - # 1. All parameters are named, but none is 'x' - return(character(0L)) - # 2. Some parameters are named, 'x' is in named parameters: match("x", setdiff(params, ",")) - # - check "x" in params being just a vector of named parameters. - # 3. Some parameters are named, 'x' is not in named parameters - # - check first appearance of "," (unnamed parameter) in vector parameters. - } else { - # Object is the first entry after 'assign'. - pos <- 1 - } - sym <- call_pd[assign_call + pos, "text"] - return(c(gsub("^['\"]|['\"]$", "", sym), "<-")) - } - - # What occurs in a function body is not tracked. - x <- call_pd[!is_in_function(call_pd), ] - sym_cond <- which(x$token %in% c("SPECIAL", "SYMBOL", "SYMBOL_FUNCTION_CALL")) - - if (length(sym_cond) == 0) { - return(character(0L)) - } - # Watch out for SYMBOLS after $ and @. For x$a x@a: x is object, a is not. - # For x$a, a's ID is $'s ID-2 so we need to remove all IDs that have ID = $ID - 2. - dollar_ids <- x[x$token %in% c("'$'", "'@'"), "id"] - if (length(dollar_ids)) { - object_ids <- x[sym_cond, "id"] - after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids] - sym_cond <- setdiff(sym_cond, which(x$id %in% after_dollar)) - } - - ass_cond <- grep("ASSIGN", x$token) - if (!length(ass_cond)) { - return(c("<-", unique(x[sym_cond, "text"]))) - } - - sym_cond <- sym_cond[sym_cond > ass_cond] # NOTE 1 - # If there was an assignment operation detect direction of it. - if (unique(x$text[ass_cond]) == "->") { # NOTE 2 - sym_cond <- rev(sym_cond) - } - - after <- match(min(x$id[ass_cond]), sort(x$id[c(min(ass_cond), sym_cond)])) - 1 - ans <- append(x[sym_cond, "text"], "<-", after = max(1, after)) - roll <- in_parenthesis(call_pd) - if (length(roll)) { - c(setdiff(ans, roll), roll) - } else { - ans - } - - ### NOTE 2: What if there are 2 assignments: e.g. a <- b -> c. - ### NOTE 1: For cases like 'eval(expression(b <- b + 2))' removes 'eval(expression('. - } - ) -} - -#' Extract side effects -#' -#' Extracts all object names from the code that are marked with `@linksto` tag. -#' -#' The code may contain functions calls that create side effects, e.g. modify the environment. -#' Static code analysis may be insufficient to determine which objects are created or modified by such a function call. -#' The `@linksto` comment tag is introduced to mark a call as having a (side) effect on one or more objects. -#' With this tag a complete object dependency structure can be established. -#' Read more about side effects and the usage of `@linksto` tag in [`get_code_dependencies()`] function. -#' -#' @param calls_pd `list` of `data.frame`s; -#' result of `utils::getParseData()` split into subsets representing individual calls; -#' created by `extract_calls()` function -#' -#' @return -#' A list of length equal to that of `calls_pd`, where each element is a character vector of names of objects -#' depending a call tagged with `@linksto` in a corresponding element of `calls_pd`. -#' -#' @keywords internal -#' @noRd -extract_side_effects <- function(calls_pd) { - lapply( - calls_pd, - function(x) { - linksto <- grep("@linksto", x[x$token == "COMMENT", "text"], value = TRUE) - unlist(strsplit(sub("\\s*#\\s*@linksto\\s+", "", linksto), "\\s+")) - } - ) -} - -# graph_parser ---- - -#' Return the indices of calls needed to reproduce an object -#' -#' @param x The name of the object to return code for. -#' @param graph A result of `code_graph()`. -#' -#' @return -#' Integer vector of indices that can be applied to `graph` to obtain all calls required to reproduce object `x`. -#' -#' @keywords internal -#' @noRd -graph_parser <- function(x, graph) { - occurrence <- vapply( - graph, - function(call) { - ind <- match("<-", call, nomatch = length(call) + 1L) - x %in% call[seq_len(ind - 1L)] - }, - logical(1) - ) - - dependencies <- lapply(graph[occurrence], function(call) { - ind <- match("<-", call, nomatch = 0L) - call[(ind + 1L):length(call)] - }) - dependencies <- setdiff(unlist(dependencies), x) - - if (length(dependencies) && any(occurrence)) { - dependency_ids <- lapply(dependencies, function(dependency) { - graph_parser(dependency, graph[1:max(which(occurrence))]) - }) - sort(unique(c(which(occurrence), unlist(dependency_ids)))) - } else { - which(occurrence) - } -} - - -# default_side_effects -------------------------------------------------------------------------------------------- - -#' Detect library calls -#' -#' Detects `library()` and `require()` function calls. -#' -#' @param calls_pd `list` of `data.frame`s; -#' result of `utils::getParseData()` split into subsets representing individual calls; -#' created by `extract_calls()` function -#' -#' @return -#' Integer vector of indices that can be applied to `graph` (result of `code_graph()`) to obtain all calls containing -#' `library()` or `require()` calls that are always returned for reproducibility. -#' -#' @keywords internal -#' @noRd -detect_libraries <- function(calls_pd) { - defaults <- c("library", "require") - - which( - vapply( - calls_pd, - function(call) { - any(call$token == "SYMBOL_FUNCTION_CALL" & call$text %in% defaults) - }, - logical(1) - ) - ) -} - -#' Normalize parsed data removing backticks from symbols -#' -#' @param pd `data.frame` resulting from `utils::getParseData()` call. -#' -#' @return `data.frame` with backticks removed from `text` column for `SYMBOL` tokens. -#' -#' @keywords internal -#' @noRd -normalize_pd <- function(pd) { - # Remove backticks from SYMBOL tokens - symbol_index <- grepl("^SYMBOL.*$", pd$token) - pd[symbol_index, "text"] <- gsub("^`(.*)`$", "\\1", pd[symbol_index, "text"]) - - pd -} diff --git a/man/get_code.Rd b/man/get_code.Rd index 29b3a4134..8a21424e7 100644 --- a/man/get_code.Rd +++ b/man/get_code.Rd @@ -4,7 +4,13 @@ \alias{get_code,teal_data-method} \title{Get code from \code{teal_data} object} \usage{ -\S4method{get_code}{teal_data}(object, deparse = TRUE, datanames = NULL, ...) +\S4method{get_code}{teal_data}( + object, + deparse = TRUE, + names = NULL, + datanames = lifecycle::deprecated(), + ... +) } \arguments{ \item{object}{(\code{teal_data})} @@ -12,15 +18,18 @@ \item{deparse}{(\code{logical}) flag specifying whether to return code as \code{character} (\code{deparse = TRUE}) or as \code{expression} (\code{deparse = FALSE}).} -\item{datanames}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} (\code{character}) vector of dataset names to return the code for. +\item{names}{(\code{character}) Successor of \code{datanames}. Vector of dataset names to return the code for. For more details see the "Extracting dataset-specific code" section.} +\item{datanames}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} (\code{character}) vector of dataset names to return the code for. +For more details see the "Extracting dataset-specific code" section. Use \code{names} instead.} + \item{...}{Parameters passed to internal methods. Currently, the only supported parameter is \code{check_names} (\code{logical(1)}) flag, which is \code{TRUE} by default. Function warns about missing objects, if they do not exist in \code{code} but are passed in \code{datanames}. To remove the warning, set \code{check_names = FALSE}.} } \value{ -Either a character string or an expression. If \code{datanames} is used to request a specific dataset, +Either a character string or an expression. If \code{names} is used to request a specific dataset, only code that \emph{creates} that dataset (not code that uses it) is returned. Otherwise, all contents of \verb{@code}. } \description{ @@ -28,11 +37,11 @@ Retrieve code from \code{teal_data} object. } \details{ Retrieve code stored in \verb{@code}, which (in principle) can be used to recreate all objects found in \verb{@env}. -Use \code{datanames} to limit the code to one or more of the datasets enumerated in \verb{@datanames}. +Use \code{names} to limit the code to one or more of the datasets enumerated in \verb{@datanames}. } \section{Extracting dataset-specific code}{ -When \code{datanames} is specified, the code returned will be limited to the lines needed to \emph{create} +When \code{names} is specified, the code returned will be limited to the lines needed to \emph{create} the requested datasets. The code stored in the \verb{@code} slot is analyzed statically to determine which lines the datasets 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. @@ -49,11 +58,11 @@ Consider the following examples: x <- 0 y <- foo(x) \}) -get_code(data, datanames = "y") +get_code(data, names = "y") }\if{html}{\out{}} -\code{x} has no dependencies, so \code{get_code(data, datanames = "x")} will return only the second call.\cr -\code{y} depends on \code{x} and \code{foo}, so \code{get_code(data, datanames = "y")} will contain all three calls. +\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.} @@ -66,11 +75,11 @@ get_code(data, datanames = "y") foo() y <- x \}) -get_code(data, datanames = "y") +get_code(data, 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, datanames = "y")} will not return the \code{foo()} call.\cr +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. @@ -85,7 +94,7 @@ In order to include comments in code one must use the \code{eval_code} function foo() # @linksto x y <- x ") -get_code(data, datanames = "y") +get_code(data, names = "y") }\if{html}{\out{}} Now the \code{foo()} call will be properly included in the code required to recreate \code{y}. @@ -109,8 +118,8 @@ tdata1 <- within(tdata1, { c <- list(x = 2) }) get_code(tdata1) -get_code(tdata1, datanames = "a") -get_code(tdata1, datanames = "b") +get_code(tdata1, names = "a") +get_code(tdata1, names = "b") tdata2 <- teal_data(x1 = iris, code = "x1 <- iris") get_code(tdata2) diff --git a/man/get_code_dependency.Rd b/man/get_code_dependency.Rd deleted file mode 100644 index 8db40903d..000000000 --- a/man/get_code_dependency.Rd +++ /dev/null @@ -1,37 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils-get_code_dependency.R -\name{get_code_dependency} -\alias{get_code_dependency} -\title{Get code dependency of an object} -\usage{ -get_code_dependency(code, names, check_names = TRUE) -} -\arguments{ -\item{code}{\code{character} with the code.} - -\item{names}{\code{character} vector of object names.} - -\item{check_names}{\code{logical(1)} flag specifying if a warning for non-existing names should be displayed.} -} -\value{ -Character vector, a subset of \code{code}. -Note that subsetting is actually done on the calls \code{code}, not necessarily on the elements of the vector. -} -\description{ -Extract subset of code required to reproduce specific object(s), including code producing side-effects. -} -\details{ -Given a character vector with code, this function will extract the part of the code responsible for creating -the variables specified by \code{names}. -This includes the final call that creates the variable(s) in question as well as all \emph{parent calls}, -\emph{i.e.} calls that create variables used in the final call and their parents, etc. -Also included are calls that create side-effects like establishing connections. - -It is assumed that object dependency is established by using three assignment operators: \verb{<-}, \code{=}, and \verb{->} . -Other assignment methods (\code{assign}, \verb{<<-}) or non-standard-evaluation methods are not supported. - -Side-effects are not detected automatically and must be marked in the code. -Add \verb{# @linksto object} at the end of a line where a side-effect occurs to specify that this line is required -to reproduce a variable called \code{object}. -} -\keyword{internal} diff --git a/tests/testthat/test-get_code.R b/tests/testthat/test-get_code.R deleted file mode 100644 index 114dee932..000000000 --- a/tests/testthat/test-get_code.R +++ /dev/null @@ -1,828 +0,0 @@ -testthat::test_that("handles empty @code slot", { - testthat::expect_identical( - get_code(teal_data(a = 1, code = character(0)), datanames = "a"), - character(0) - ) - testthat::expect_identical( - get_code(teal_data(a = 1, code = ""), datanames = "a"), - "" - ) -}) - -testthat::test_that("handles the code without symbols on rhs", { - code <- c( - "1 + 1", - "a <- 5", - "501" - ) - - testthat::expect_identical( - get_code(teal_data(a = 5, code = code), datanames = "a"), - "a <- 5" - ) -}) - -testthat::test_that("handles the code included in curly brackets", { - code <- "{1 + 1;a <- 5}" - - testthat::expect_identical( - get_code(teal_data(a = 5, code = code), datanames = "a"), - "a <- 5" - ) -}) - -testthat::test_that("handles the code of length > 1 when at least one is enclosed in curly brackets", { - code <- c("{a<-5}", "1+1") - tdata <- eval_code(eval_code(teal_data(), code[1]), code[2]) - - testthat::expect_identical( - get_code(tdata, datanames = "a"), - "a <- 5" - ) -}) - - -testthat::test_that("extracts the code of a binding from character vector containing simple code", { - code <- c( - "a <- 1", - "b <- 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - "a <- 1" - ) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - "b <- 2" - ) -}) - -testthat::test_that("extracts the code without downstream usage", { - code <- c( - "a <- 1", - "head(a)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - "a <- 1" - ) -}) - -testthat::test_that("works for datanames of length > 1", { - code <- c( - "a <- 1", - "b <- 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = c("a", "b")), - paste(code, collapse = "\n") - ) -}) - -testthat::test_that("warns if binding doesn't exist in code", { - code <- c("a <- 1") - tdata <- eval_code(teal_data(), code) - testthat::expect_warning( - get_code(tdata, datanames = "c"), - "Object\\(s\\) not found in code: c" - ) -}) - -testthat::test_that("does not fall into a loop", { - code <- c( - "a <- 1", - "b <- a", - "c <- b", - "a <- c" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - paste(code, collapse = "\n") - ) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste(code[1:2], collapse = "\n") - ) - testthat::expect_identical( - get_code(tdata, datanames = "c"), - paste(code[1:3], collapse = "\n") - ) -}) - - -testthat::test_that("extracts code of a parent binding but only those evaluated before coocurence", { - code <- c( - "a <- 1", - "b <- a", - "a <- 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("a <- 1", "b <- a", sep = "\n") - ) -}) - -testthat::test_that("extracts the code of a parent binding if used as an arg in a function call", { - code <- c( - "a <- 1", - "b <- identity(x = a)", - "a <- 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("a <- 1", "b <- identity(x = a)", sep = "\n") - ) -}) - -testthat::test_that("extracts the code when using <<-", { - code <- c( - "a <- 1", - "b <- a", - "b <<- b + 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("a <- 1", "b <- a", "b <<- b + 2", sep = "\n") - ) -}) - -testthat::test_that("detects every assign calls even if not evaluated, if there is only one assignment in this line", { - code <- c( - "a <- 1", - "b <- 2", - "eval(expression({b <- b + 2}))" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("b <- 2", "eval(expression({\n b <- b + 2\n}))", sep = "\n") - ) -}) - -testthat::test_that("returns result of length 1 for non-empty input", { - tdata1 <- teal_data() - tdata1 <- within(tdata1, { - a <- 1 - b <- a^5 - c <- list(x = 2) - }) - - testthat::expect_length(get_code(tdata1, deparse = FALSE), 1) - testthat::expect_length(get_code(tdata1, deparse = TRUE), 1) -}) - -testthat::test_that("does not break if code is separated by ;", { - code <- c( - "a <- 1;a <- a + 1" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - gsub(";", "\n", code, fixed = TRUE) - ) -}) - -testthat::test_that("does not break if code uses quote()", { - code <- c( - "expr <- quote(x <- x + 1)", - "x <- 0", - "eval(expr)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - code[2] - ) -}) - -testthat::test_that("does not break if object is used in a function on lhs", { - code <- c( - "data(iris)", - "iris2 <- iris", - "names(iris) <- letters[1:5]" - ) - tdata <- eval_code(teal_data(), code = code) - testthat::expect_identical( - get_code(tdata, datanames = "iris"), - paste(code[c(1, 3)], collapse = "\n") - ) -}) - -testthat::test_that( - "does not break if object is used in a function on lhs and influencers are both on lhs and rhs", - { - code <- c( - "x <- 5", - "y <- length(x)", - "names(x)[y] <- y" - ) - tdata <- eval_code(teal_data(), code = code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - paste(code, collapse = "\n") - ) - } -) - -# assign ---------------------------------------------------------------------------------------------------------- - -testthat::test_that("extracts the code for assign() where \"x\" is a literal string", { - code <- c( - "a <- 1", - "assign('b', 5)", - "assign(value = 7, x = 'c')", - "assign(value = 15, x = \"d\")", - "b <- b + 2", - "c <- b", - "d <- d * 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("assign(\"b\", 5)", "b <- b + 2", sep = "\n") - ) - testthat::expect_identical( - get_code(tdata, datanames = "c"), - paste( - "assign(\"b\", 5)", - "assign(value = 7, x = \"c\")", - "b <- b + 2", - "c <- b", - sep = "\n" - ) - ) - testthat::expect_identical( - get_code(tdata, datanames = "d"), - paste("assign(value = 15, x = \"d\")", "d <- d * 2", sep = "\n") - ) -}) - -testthat::test_that("extracts the code for assign() where \"x\" is variable", { - testthat::skip("We will not resolve this, as this requires code evaluation.") - code <- c( - "x <- \"a\"", - "assign(x, 5)", - "b <- a" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste(code, collapse = "\n") - ) -}) - -testthat::test_that("works for assign() detection no matter how many parametrers were provided in assignq()", { - code <- c( - "x <- 1", - "assign(\"x\", 0, envir = environment())", - "assign(inherits = FALSE, immediate = TRUE, \"z\", 5, envir = environment())", - "y <- x + z", - "y <- x" - ) - - tdata <- eval_code(teal_data(), code) - - testthat::expect_identical( - get_code(tdata, datanames = "y"), - paste(code, collapse = "\n") - ) -}) - -testthat::test_that("detects function usage of the assignment operator", { - code <- c( - "x <- 1", - "`<-`(y,x)" - ) - code2 <- "`<-`(y, `<-`(x, 2))" - - tdata <- eval_code(teal_data(), code) - tdata2 <- eval_code(teal_data(), code2) - - testthat::expect_identical( - get_code(tdata, datanames = "y"), - paste(c(code[1], "y <- x"), collapse = "\n") - ) - testthat::expect_identical( - get_code(tdata2, datanames = "y"), - "y <- x <- 2" - ) -}) - - -# @linksto --------------------------------------------------------------------------------------------------------- - -testthat::test_that("get_code does not break if @linksto is put in the last line", { - # In some cases R parses comment as a separate expression so the comment is not - # directly associated with this line of code. This situation occurs when `eval` is in the last - # line of the code. Other cases are not known but are highly probable. - code <- c( - "expr <- quote(x <- x + 1)", - "x <- 0", - "eval(expr) #@linksto x" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - paste(gsub(" #@linksto x", "", code, fixed = TRUE), collapse = "\n") - ) -}) - -testthat::test_that("@linksto makes a line being returned for an affected binding", { - code <- " - a <- 1 # @linksto b - b <- 2 - " - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("a <- 1", "b <- 2", sep = "\n") - ) -}) - -testthat::test_that( - "@linksto returns the line for an affected binding - even if the object did not exist in the same iteration of eval_code", - { - code <- c( - "a <- 1 # @linksto b", - "b <- 2" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("a <- 1", "b <- 2", sep = "\n") - ) - } -) - -testthat::test_that( - "lines affecting parent evaluated after co-occurrence are not included in output when using @linksto", - { - code <- c( - "a <- 1 ", - "b <- 2 # @linksto a", - "a <- a + 1", - "b <- b + 1" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - paste("a <- 1", "b <- 2", "a <- a + 1", sep = "\n") - ) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("b <- 2", "b <- b + 1", sep = "\n") - ) - } -) - -testthat::test_that( - "@linksto gets extracted if it's a side-effect on a dependent object (even of a dependent object)", - { - code <- " - iris[1:5, ] -> iris2 - iris_head <- head(iris) # @linksto iris3 - iris3 <- iris_head[1, ] # @linksto iris2 - classes <- lapply(iris2, class) - " - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "classes"), - paste("iris2 <- iris[1:5, ]", - "iris_head <- head(iris)", - "iris3 <- iris_head[1, ]", - "classes <- lapply(iris2, class)", - sep = "\n" - ) - ) - } -) - -# functions ------------------------------------------------------------------------------------------------------- - -testthat::test_that("ignores occurrence in a function definition", { - code <- c( - "b <- 2", - "foo <- function(b) { b <- b + 2 }" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - "b <- 2" - ) - testthat::expect_identical( - get_code(tdata, datanames = "foo"), - "foo <- function(b) {\n b <- b + 2\n}" - ) -}) - -testthat::test_that("ignores occurrence in a function definition that has function in it", { - code <- c( - "b <- 2", - "foo <- function(b) { function(c) {b <- c + 2 }}" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - "b <- 2" - ) - testthat::expect_identical( - get_code(tdata, datanames = "foo"), - "foo <- function(b) {\n function(c) {\n b <- c + 2\n }\n}" - ) -}) - -testthat::test_that("ignores occurrence in a function definition if there is multiple function definitions", { - code <- c( - "b <- 2", - "foo <- function(b) { function(c) {b <- c + 2 }}", - "b <- b + 1", - "bar <- function(b) print(b)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - "b <- 2\nb <- b + 1" - ) - testthat::expect_identical( - get_code(tdata, datanames = "foo"), - "foo <- function(b) {\n function(c) {\n b <- c + 2\n }\n}" - ) -}) - -testthat::test_that("ignores occurrence in a function definition in lapply", { - code <- c( - "a <- list(a = 1, b = 2, c = 3)", - "b <- lapply(a, FUN = function(x) { x <- x + 1 })", - "b <- Filter(function(x) x > 2, b)", - "x <- 1", - "identity(x)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - "x <- 1" - ) -}) - -testthat::test_that("does not ignore occurrence in function body if object exsits in env", { - skip("This is not urgent and can be ommitted with @linksto tag.") - code <- c( - "a <- list(a = 1, b = 2, c = 3)", - "p <- 5", # This is not extracted, even though is used in the next line. - "b <- lapply(a, FUN = function(x) { x <- x + p })", - "b <- Filter(function(x) x > 2, b)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste(code, sep = "\n") - ) -}) - -testthat::test_that("ignores occurrence in function definition without { curly brackets", { - code <- c( - "b <- 2", - "foo <- function(b) b <- b + 2 " - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "foo"), - "foo <- function(b) b <- b + 2" - ) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - "b <- 2" - ) -}) - -testthat::test_that("detects occurrence of the function object", { - code <- c( - "a <- 1", - "b <- 2", - "foo <- function(b) { b <- b + 2 }", - "b <- foo(a)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste("a <- 1", "b <- 2", "foo <- function(b) {\n b <- b + 2\n}", "b <- foo(a)", sep = "\n") - ) -}) - -testthat::test_that("detects occurrence of a function definition when a formal is named the same as a function", { - code <- c( - "x <- 1", - "foo <- function(foo = 1) 'text'", - "a <- foo(x)" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - paste("x <- 1", "foo <- function(foo = 1) \"text\"", "a <- foo(x)", sep = "\n") - ) -}) - -testthat::test_that("detects occurrence of a function definition with a @linksto usage", { - code <- c( - " - foo <- function() { - env <- parent.frame() - env$x <- 0 - }", - "foo() # @linksto x", - "y <- x" - ) - tdata <- teal_data(code = code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - "foo <- function() {\n env <- parent.frame()\n env$x <- 0\n}\nfoo()" - ) -}) -# $ --------------------------------------------------------------------------------------------------------------- - -testthat::test_that("understands $ usage and do not treat rhs of $ as objects (only lhs)", { - code <- c( - "x <- data.frame(a = 1:3)", - "a <- data.frame(y = 1:3)", - "a$x <- a$y", - "a$x <- a$x + 2", - "a$x <- x$a" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - "x <- data.frame(a = 1:3)" - ) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - paste("x <- data.frame(a = 1:3)", - "a <- data.frame(y = 1:3)", - "a$x <- a$y", - "a$x <- a$x + 2", - "a$x <- x$a", - sep = "\n" - ) - ) -}) - -testthat::test_that("detects cooccurrence properly even if all objects are on lhs", { - code <- c( - "a <- 1", - "b <- list(c = 2)", - "b[[a]] <- 3" - ) - tdata <- eval_code(teal_data(), code) - testthat::expect_identical( - get_code(tdata, datanames = "b"), - paste(code, collapse = "\n") - ) -}) - - -# @ --------------------------------------------------------------------------------------------------------------- - -testthat::test_that("understands @ usage and do not treat rhs of @ as objects (only lhs)", { - code <- c( - "setClass('aclass', slots = c(a = 'numeric', x = 'numeric', y = 'numeric')) # @linksto a x", - "x <- new('aclass', a = 1:3, x = 1:3, y = 1:3)", - "a <- new('aclass', a = 1:3, x = 1:3, y = 1:3)", - "a@x <- a@y", - "a@x <- a@x + 2", - "a@x <- x@a" - ) - tdata <- teal_data(x = 1, a = 1, code = code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - paste( - 'setClass("aclass", slots = c(a = "numeric", x = "numeric", y = "numeric"))', - 'x <- new("aclass", a = 1:3, x = 1:3, y = 1:3)', - sep = "\n" - ) - ) - testthat::expect_identical( - get_code(tdata, datanames = "a"), - paste( - 'setClass("aclass", slots = c(a = "numeric", x = "numeric", y = "numeric"))', - 'x <- new("aclass", a = 1:3, x = 1:3, y = 1:3)', - 'a <- new("aclass", a = 1:3, x = 1:3, y = 1:3)', - "a@x <- a@y", - "a@x <- a@x + 2", - "a@x <- x@a", - sep = "\n" - ) - ) -}) - - - -# libraries ------------------------------------------------------------------------------------------------------- - -testthat::test_that("library() and require() are always returned", { - code <- c( - "set.seed(1)", - "library(random.cdisc.data)", - "require(dplyr)", - "library(MultiAssayExperiment)", - "x <- 5", - "y <- 6" - ) - tdata <- teal_data(x = 5, y = 6, code = code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - paste( - "library(random.cdisc.data)", - "require(dplyr)", - "library(MultiAssayExperiment)", - "x <- 5", - sep = "\n" - ) - ) -}) - - -# data() ---------------------------------------------------------------------------------------------------------- - -testthat::test_that("data() call is returned when data name is provided as is", { - code <- c( - "set.seed(1)", - "library(random.cdisc.data)", - "require(dplyr)", - "library(MultiAssayExperiment)", - "data(miniACC, envir = environment())", - "x <- miniACC" - ) - tdata <- teal_data(x = 1, code = code) - testthat::expect_identical( - get_code(tdata, datanames = "x"), - paste( - "library(random.cdisc.data)", - "require(dplyr)", - "library(MultiAssayExperiment)", - "data(miniACC, envir = environment())", - "x <- miniACC", - sep = "\n" - ) - ) -}) - -testthat::test_that("data() call is returned when data name is provided as a character", { - code <- c( - "set.seed(1)", - "library(random.cdisc.data)", - "require(dplyr)", - "library(MultiAssayExperiment)", - "data('mtcars')", - "z <- mtcars" - ) - tdata <- teal_data(z = 1, code = code) - testthat::expect_identical( - get_code(tdata, datanames = "z"), - paste( - "library(random.cdisc.data)", - "require(dplyr)", - "library(MultiAssayExperiment)", - "data(\"mtcars\")", - "z <- mtcars", - sep = "\n" - ) - ) -}) - -testthat::describe("Backticked symbol", { - testthat::it("code can be retrieved with get_code", { - td <- within( - teal_data(), - { - `%cbind%` <- function(lhs, rhs) cbind(lhs, rhs) # nolint: object_name. - iris_ds <- iris %cbind% data.frame(new_col = "new column") - } - ) - - testthat::expect_identical( - get_code(td, datanames = "%cbind%"), - "`%cbind%` <- function(lhs, rhs) cbind(lhs, rhs)" - ) - }) - - testthat::it("code can be retrieved with get_code", { - td <- within( - teal_data(), - { - `%cbind%` <- function(lhs, rhs) cbind(lhs, rhs) # nolint: object_name. - iris_ds <- iris %cbind% data.frame(new_col = "new column") - } - ) - - testthat::expect_identical( - get_code(td, datanames = "`%cbind%`"), - "`%cbind%` <- function(lhs, rhs) cbind(lhs, rhs)" - ) - }) - - testthat::it("starting with underscore is detected in code dependency", { - td <- within( - teal_data(), - { - `_add_column_` <- function(lhs, rhs) cbind(lhs, rhs) # nolint: object_name. - iris_ds <- `_add_column_`(iris, data.frame(new_col = "new column")) - } - ) - - testthat::expect_identical( - get_code(td, datanames = "iris_ds"), - paste( - sep = "\n", - "`_add_column_` <- function(lhs, rhs) cbind(lhs, rhs)", - "iris_ds <- `_add_column_`(iris, data.frame(new_col = \"new column\"))" - ) - ) - }) - - testthat::it("with space character is detected in code dependency", { - td <- within( - teal_data(), - { - `add column` <- function(lhs, rhs) cbind(lhs, rhs) # nolint: object_name. - iris_ds <- `add column`(iris, data.frame(new_col = "new column")) - } - ) - - testthat::expect_identical( - get_code(td, datanames = "iris_ds"), - paste( - sep = "\n", - "`add column` <- function(lhs, rhs) cbind(lhs, rhs)", - "iris_ds <- `add column`(iris, data.frame(new_col = \"new column\"))" - ) - ) - }) - - testthat::it("without special characters is cleaned and detected in code dependency", { - td <- within( - teal_data(), - { - `add_column` <- function(lhs, rhs) cbind(lhs, rhs) - iris_ds <- `add_column`(iris, data.frame(new_col = "new column")) - } - ) - - testthat::expect_identical( - get_code(td, datanames = "iris_ds"), - paste( - sep = "\n", - "add_column <- function(lhs, rhs) cbind(lhs, rhs)", - "iris_ds <- add_column(iris, data.frame(new_col = \"new column\"))" - ) - ) - }) - - testthat::it("with non-native pipe used as function is detected code dependency", { - td <- within( - teal_data(), - { - `%add_column%` <- function(lhs, rhs) cbind(lhs, rhs) - iris_ds <- `%add_column%`(iris, data.frame(new_col = "new column")) - } - ) - - # Note that the original code is changed to use the non-native pipe operator - # correctly. - testthat::expect_identical( - get_code(td, datanames = "iris_ds"), - paste( - sep = "\n", - "`%add_column%` <- function(lhs, rhs) cbind(lhs, rhs)", - "iris_ds <- iris %add_column% data.frame(new_col = \"new column\")" - ) - ) - }) - - testthat::it("with non-native pipe is detected code dependency", { - td <- within( - teal_data(), - { - `%add_column%` <- function(lhs, rhs) cbind(lhs, rhs) - iris_ds <- iris %add_column% data.frame(new_col = "new column") - } - ) - - # Note that the original code is changed to use the non-native pipe operator - # correctly. - testthat::expect_identical( - get_code(td, datanames = "iris_ds"), - paste( - sep = "\n", - "`%add_column%` <- function(lhs, rhs) cbind(lhs, rhs)", - "iris_ds <- iris %add_column% data.frame(new_col = \"new column\")" - ) - ) - }) -}) diff --git a/vignettes/teal-data-reproducibility.Rmd b/vignettes/teal-data-reproducibility.Rmd index 10f08d583..f39d0a81b 100644 --- a/vignettes/teal-data-reproducibility.Rmd +++ b/vignettes/teal-data-reproducibility.Rmd @@ -87,7 +87,7 @@ verify(data_wrong) # fails verification, raises error ## Retrieving code The `get_code` function is used to retrieve the code stored in a `teal_data` object. -A simple `get_code()` will return the entirety of the code but using the `datanames` argument allows for obtaining a subset of the code that only deals with some of the objects stored in `teal_data`. +A simple `get_code()` will return the entirety of the code but using the `names` argument allows for obtaining a subset of the code that only deals with some of the objects stored in `teal_data`. ```{r} library(teal.data) @@ -98,7 +98,7 @@ data <- within(teal_data(), { head(i) }) cat(get_code(data)) # retrieve all code -cat(get_code(data, datanames = "i")) # retrieve code for `i` +cat(get_code(data, names = "i")) # retrieve code for `i` ``` Note that in when retrieving code for a specific dataset, the result is only the code used to _create_ that dataset, not code that _uses_ is.