| 1 |
# get_code_dependency ---- |
|
| 2 | ||
| 3 |
#' Get code dependency of an object |
|
| 4 |
#' |
|
| 5 |
#' Extract subset of code required to reproduce specific object(s), including code producing side-effects. |
|
| 6 |
#' |
|
| 7 |
#' Given a character vector with code, this function will extract the part of the code responsible for creating |
|
| 8 |
#' the variables specified by `names`. |
|
| 9 |
#' This includes the final call that creates the variable(s) in question as well as all _parent calls_, |
|
| 10 |
#' _i.e._ calls that create variables used in the final call and their parents, etc. |
|
| 11 |
#' Also included are calls that create side-effects like establishing connections. |
|
| 12 |
#' |
|
| 13 |
#' It is assumed that object dependency is established by using three assignment operators: `<-`, `=`, and `->` . |
|
| 14 |
#' Other assignment methods (`assign`, `<<-`) or non-standard-evaluation methods are not supported. |
|
| 15 |
#' |
|
| 16 |
#' Side-effects are not detected automatically and must be marked in the code. |
|
| 17 |
#' Add `# @linksto object` at the end of a line where a side-effect occurs to specify that this line is required |
|
| 18 |
#' to reproduce a variable called `object`. |
|
| 19 |
#' |
|
| 20 |
#' @param code `character` with the code. |
|
| 21 |
#' @param names `character` vector of object names. |
|
| 22 |
#' @param check_code_names `logical(1)` flag specifying if a warning for non-existing names should be displayed. |
|
| 23 |
#' |
|
| 24 |
#' @return Character vector, a subset of `code`. |
|
| 25 |
#' Note that subsetting is actually done on the calls `code`, not necessarily on the elements of the vector. |
|
| 26 |
#' |
|
| 27 |
#' @keywords internal |
|
| 28 |
get_code_dependency <- function(code, names, check_code_names = TRUE) {
|
|
| 29 | 85x |
checkmate::assert_list(code, "character") |
| 30 | 85x |
checkmate::assert_character(names, any.missing = FALSE) |
| 31 | ||
| 32 | 85x |
graph <- lapply(code, attr, "dependency") |
| 33 | ||
| 34 | 85x |
if (check_code_names) {
|
| 35 | 84x |
symbols <- unlist(lapply(graph, function(call) {
|
| 36 | 240x |
ind <- match("<-", call, nomatch = length(call) + 1L)
|
| 37 | 240x |
call[seq_len(ind - 1L)] |
| 38 |
})) |
|
| 39 | ||
| 40 | 84x |
if (!all(names %in% unique(symbols))) {
|
| 41 | 8x |
warning("Object(s) not found in code: ", toString(setdiff(names, symbols)), ".", call. = FALSE)
|
| 42 |
} |
|
| 43 |
} |
|
| 44 | ||
| 45 | 85x |
if (length(code) == 0) {
|
| 46 | 1x |
return(code) |
| 47 |
} |
|
| 48 | ||
| 49 | 84x |
ind <- unlist(lapply(names, function(x) graph_parser(x, graph))) |
| 50 | ||
| 51 | 84x |
lib_ind <- detect_libraries(graph) |
| 52 | ||
| 53 | 84x |
code_ids <- sort(unique(c(lib_ind, ind))) |
| 54 | 84x |
code[code_ids] |
| 55 |
} |
|
| 56 | ||
| 57 |
#' Locate function call token |
|
| 58 |
#' |
|
| 59 |
#' Determine which row of parsed data is specific `SYMBOL_FUNCTION_CALL` token. |
|
| 60 |
#' |
|
| 61 |
#' Useful for determining occurrence of `assign` or `data` functions in an input call. |
|
| 62 |
#' |
|
| 63 |
#' @param call_pd `data.frame` as returned by `extract_calls()` |
|
| 64 |
#' @param text `character(1)` to look for in `text` column of `call_pd` |
|
| 65 |
#' |
|
| 66 |
#' @return |
|
| 67 |
#' Single integer specifying row in `call_pd` where `token` is `SYMBOL_FUNCTION_CALL` and `text` is `text`. |
|
| 68 |
#' 0 if not found. |
|
| 69 |
#' |
|
| 70 |
#' @keywords internal |
|
| 71 |
#' @noRd |
|
| 72 |
find_call <- function(call_pd, text) {
|
|
| 73 | 775x |
checkmate::check_data_frame(call_pd) |
| 74 | 775x |
checkmate::check_names(call_pd, must.include = c("token", "text"))
|
| 75 | 775x |
checkmate::check_string(text) |
| 76 | ||
| 77 | 775x |
ans <- which(call_pd$token == "SYMBOL_FUNCTION_CALL" & call_pd$text == text) |
| 78 | 775x |
if (length(ans)) {
|
| 79 | 11x |
ans |
| 80 |
} else {
|
|
| 81 | 764x |
0L |
| 82 |
} |
|
| 83 |
} |
|
| 84 | ||
| 85 |
#' Split the result of `utils::getParseData()` into separate calls |
|
| 86 |
#' |
|
| 87 |
#' @param pd (`data.frame`) A result of `utils::getParseData()`. |
|
| 88 |
#' |
|
| 89 |
#' @return |
|
| 90 |
#' A `list` of `data.frame`s. |
|
| 91 |
#' Each element is a subset of `pd` corresponding to one call in the original code from which `pd` was obtained. |
|
| 92 |
#' Only four columns (`"token"`, `"text"`, `"id"`, `"parent"`) are kept, the rest is discarded. |
|
| 93 |
#' |
|
| 94 |
#' @keywords internal |
|
| 95 |
#' @noRd |
|
| 96 |
extract_calls <- function(pd) {
|
|
| 97 | 1012x |
calls <- lapply( |
| 98 | 1012x |
pd[pd$parent == 0 & (pd$token != "COMMENT" | grepl("@linksto", pd$text, fixed = TRUE)), "id"],
|
| 99 | 1012x |
function(parent) {
|
| 100 | 1148x |
rbind( |
| 101 | 1148x |
pd[pd$id == parent, ], |
| 102 | 1148x |
get_children(pd = pd, parent = parent) |
| 103 |
) |
|
| 104 |
} |
|
| 105 |
) |
|
| 106 | 1012x |
calls <- Filter(function(call) !(nrow(call) == 1 && call$token == "';'"), calls) |
| 107 | 1012x |
calls <- Filter(Negate(is.null), calls) |
| 108 | 1012x |
calls <- fix_shifted_comments(calls) |
| 109 | 1012x |
calls <- remove_custom_assign(calls, c(":="))
|
| 110 | 1012x |
fix_arrows(calls) |
| 111 |
} |
|
| 112 | ||
| 113 |
#' @keywords internal |
|
| 114 |
#' @noRd |
|
| 115 |
get_children <- function(pd, parent) {
|
|
| 116 | 11338x |
idx_children <- abs(pd$parent) == parent |
| 117 | 11338x |
children <- pd[idx_children, ] |
| 118 | 11338x |
if (nrow(children) == 0) {
|
| 119 | 6450x |
return(NULL) |
| 120 |
} |
|
| 121 | ||
| 122 | 4888x |
if (parent > 0) {
|
| 123 | 4888x |
do.call(rbind, c(list(children), lapply(children$id, get_children, pd = pd))) |
| 124 |
} |
|
| 125 |
} |
|
| 126 | ||
| 127 |
#' Fixes edge case of comments being shifted to the next call. |
|
| 128 |
#' @keywords internal |
|
| 129 |
#' @noRd |
|
| 130 |
fix_shifted_comments <- function(calls) {
|
|
| 131 |
# If the first or the second token is a @linksto COMMENT, |
|
| 132 |
# then it belongs to the previous call. |
|
| 133 | 1012x |
if (length(calls) >= 2) {
|
| 134 | 95x |
for (i in 2:length(calls)) {
|
| 135 | 173x |
comment_idx <- grep("@linksto", calls[[i]][, "text"])
|
| 136 | 173x |
if (isTRUE(comment_idx[1] <= 2)) {
|
| 137 | 9x |
calls[[i - 1]] <- rbind( |
| 138 | 9x |
calls[[i - 1]], |
| 139 | 9x |
calls[[i]][comment_idx[1], ] |
| 140 |
) |
|
| 141 | 9x |
calls[[i]] <- calls[[i]][-comment_idx[1], ] |
| 142 |
} |
|
| 143 |
} |
|
| 144 |
} |
|
| 145 | 1012x |
Filter(nrow, calls) |
| 146 |
} |
|
| 147 | ||
| 148 |
#' Fixes edge case of custom assignments operator being treated as assignment. |
|
| 149 |
#' |
|
| 150 |
#' @param exclude (`character`) custom assignment operators to be excluded |
|
| 151 |
#' @keywords internal |
|
| 152 |
#' @noRd |
|
| 153 |
remove_custom_assign <- function(calls, exclude = NULL) {
|
|
| 154 | 1012x |
checkmate::assert_list(calls) |
| 155 | 1012x |
checkmate::assert_character(exclude, null.ok = TRUE) |
| 156 | 1012x |
lapply(calls, function(call) {
|
| 157 | 1141x |
if (!is.null(exclude)) {
|
| 158 | 1141x |
call[!(call$token == "LEFT_ASSIGN" & call$text %in% exclude), ] |
| 159 |
} else {
|
|
| 160 | ! |
call |
| 161 |
} |
|
| 162 |
}) |
|
| 163 |
} |
|
| 164 | ||
| 165 |
#' Fixes edge case of `<-` assignment operator being called as function, |
|
| 166 |
#' which is \code{`<-`(y,x)} instead of traditional `y <- x`.
|
|
| 167 |
#' @keywords internal |
|
| 168 |
#' @noRd |
|
| 169 |
fix_arrows <- function(calls) {
|
|
| 170 | 1012x |
checkmate::assert_list(calls) |
| 171 | 1012x |
lapply(calls, function(call) {
|
| 172 | 1141x |
sym_fun <- call$token == "SYMBOL_FUNCTION_CALL" |
| 173 | 1141x |
call[sym_fun, ] <- sub_arrows(call[sym_fun, ]) |
| 174 | 1141x |
call |
| 175 |
}) |
|
| 176 |
} |
|
| 177 | ||
| 178 |
#' Execution of assignment operator substitutions for a call. |
|
| 179 |
#' @keywords internal |
|
| 180 |
#' @noRd |
|
| 181 |
sub_arrows <- function(call) {
|
|
| 182 | 1141x |
checkmate::assert_data_frame(call) |
| 183 | 1141x |
map <- data.frame( |
| 184 | 1141x |
row.names = c("<-", "<<-", "="),
|
| 185 | 1141x |
token = rep("LEFT_ASSIGN", 3),
|
| 186 | 1141x |
text = rep("<-", 3)
|
| 187 |
) |
|
| 188 | 1141x |
sub_ids <- call$text %in% rownames(map) |
| 189 | 1141x |
call[sub_ids, c("token", "text")] <- map[call$text[sub_ids], ]
|
| 190 | 1141x |
call |
| 191 |
} |
|
| 192 | ||
| 193 |
# code_graph ---- |
|
| 194 | ||
| 195 |
#' Extract object occurrence |
|
| 196 |
#' |
|
| 197 |
#' Extracts objects occurrence within calls passed by `pd`. |
|
| 198 |
#' Also detects which objects depend on which within a call. |
|
| 199 |
#' |
|
| 200 |
#' @param pd `data.frame`; |
|
| 201 |
#' one of the results of `utils::getParseData()` split into subsets representing individual calls; |
|
| 202 |
#' created by `extract_calls()` function |
|
| 203 |
#' |
|
| 204 |
#' @return |
|
| 205 |
#' A character vector listing names of objects that depend on this call |
|
| 206 |
#' and names of objects that this call depends on. |
|
| 207 |
#' Dependencies are listed after the `"<-"` string, e.g. `c("a", "<-", "b", "c")` means that in this call object `a`
|
|
| 208 |
#' depends on objects `b` and `c`. |
|
| 209 |
#' If a call is tagged with `@linksto a`, then object `a` is understood to depend on that call. |
|
| 210 |
#' |
|
| 211 |
#' @keywords internal |
|
| 212 |
#' @noRd |
|
| 213 |
extract_occurrence <- function(pd) {
|
|
| 214 | 389x |
is_in_function <- function(x) {
|
| 215 |
# If an object is a function parameter, |
|
| 216 |
# then in calls_pd there is a `SYMBOL_FORMALS` entry for that object. |
|
| 217 | 378x |
function_id <- x[x$token == "FUNCTION", "parent"] |
| 218 | 378x |
if (length(function_id)) {
|
| 219 | 20x |
x$id %in% get_children(x, function_id[1])$id |
| 220 |
} else {
|
|
| 221 | 358x |
rep(FALSE, nrow(x)) |
| 222 |
} |
|
| 223 |
} |
|
| 224 | 389x |
in_parenthesis <- function(x) {
|
| 225 | 288x |
if (any(x$token %in% c("LBB", "'['"))) {
|
| 226 | 9x |
id_start <- min(x$id[x$token %in% c("LBB", "'['")])
|
| 227 | 9x |
id_end <- min(x$id[x$token == "']'"]) |
| 228 | 9x |
x$text[x$token == "SYMBOL" & x$id > id_start & x$id < id_end] |
| 229 |
} |
|
| 230 |
} |
|
| 231 | ||
| 232 |
# Handle data(object)/data("object")/data(object, envir = ) independently.
|
|
| 233 | 389x |
data_call <- find_call(pd, "data") |
| 234 | 389x |
if (data_call) {
|
| 235 | 3x |
sym <- pd[data_call + 1, "text"] |
| 236 | 3x |
return(c(gsub("^['\"]|['\"]$", "", sym), "<-"))
|
| 237 |
} |
|
| 238 |
# Handle assign(x = ). |
|
| 239 | 386x |
assign_call <- find_call(pd, "assign") |
| 240 | 386x |
if (assign_call) {
|
| 241 |
# Check if parameters were named. |
|
| 242 |
# "','" is for unnamed parameters, where "SYMBOL_SUB" is for named. |
|
| 243 |
# "EQ_SUB" is for `=` appearing after the name of the named parameter. |
|
| 244 | 8x |
if (any(pd$token == "SYMBOL_SUB")) {
|
| 245 | 4x |
params <- pd[pd$token %in% c("SYMBOL_SUB", "','", "EQ_SUB"), "text"]
|
| 246 |
# Remove sequence of "=", ",". |
|
| 247 | 4x |
if (length(params > 1)) {
|
| 248 | 4x |
remove <- integer(0) |
| 249 | 4x |
for (i in 2:length(params)) {
|
| 250 | 20x |
if (params[i - 1] == "=" && params[i] == ",") {
|
| 251 | 4x |
remove <- c(remove, i - 1, i) |
| 252 |
} |
|
| 253 |
} |
|
| 254 | 3x |
if (length(remove)) params <- params[-remove] |
| 255 |
} |
|
| 256 | 4x |
pos <- match("x", setdiff(params, ","), nomatch = match(",", params, nomatch = 0))
|
| 257 | 4x |
if (!pos) {
|
| 258 | ! |
return(character(0L)) |
| 259 |
} |
|
| 260 |
# pos is indicator of the place of 'x' |
|
| 261 |
# 1. All parameters are named, but none is 'x' - return(character(0L)) |
|
| 262 |
# 2. Some parameters are named, 'x' is in named parameters: match("x", setdiff(params, ","))
|
|
| 263 |
# - check "x" in params being just a vector of named parameters. |
|
| 264 |
# 3. Some parameters are named, 'x' is not in named parameters |
|
| 265 |
# - check first appearance of "," (unnamed parameter) in vector parameters. |
|
| 266 |
} else {
|
|
| 267 |
# Object is the first entry after 'assign'. |
|
| 268 | 4x |
pos <- 1 |
| 269 |
} |
|
| 270 | 8x |
sym <- pd[assign_call + pos, "text"] |
| 271 | 8x |
return(c(gsub("^['\"]|['\"]$", "", sym), "<-"))
|
| 272 |
} |
|
| 273 | ||
| 274 |
# What occurs in a function body is not tracked. |
|
| 275 | 378x |
x <- pd[!is_in_function(pd), ] |
| 276 | 378x |
sym_cond <- which(x$token %in% c("SPECIAL", "SYMBOL", "SYMBOL_FUNCTION_CALL"))
|
| 277 | 378x |
sym_fc_cond <- which(x$token == "SYMBOL_FUNCTION_CALL") |
| 278 | ||
| 279 | 378x |
if (length(sym_cond) == 0) {
|
| 280 | 17x |
return(character(0L)) |
| 281 |
} |
|
| 282 |
# Watch out for SYMBOLS after $ and @. For x$a x@a: x is object, a is not. |
|
| 283 |
# For x$a, a's ID is $'s ID-2 so we need to remove all IDs that have ID = $ID - 2. |
|
| 284 | 361x |
dollar_ids <- x[x$token %in% c("'$'", "'@'"), "id"]
|
| 285 | 361x |
if (length(dollar_ids)) {
|
| 286 | 6x |
object_ids <- x[sym_cond, "id"] |
| 287 | 6x |
after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids] |
| 288 | 6x |
sym_cond <- setdiff(sym_cond, which(x$id %in% after_dollar)) |
| 289 |
} |
|
| 290 | ||
| 291 | 361x |
assign_cond <- grep("ASSIGN", x$token)
|
| 292 | 361x |
if (!length(assign_cond)) {
|
| 293 | 73x |
return(c("<-", unique(x[sym_cond, "text"])))
|
| 294 |
} |
|
| 295 | ||
| 296 |
# For cases like 'eval(expression(c <- b + 2))' removes 'eval(expression('.
|
|
| 297 | 288x |
sym_cond <- sym_cond[!(sym_cond < min(assign_cond) & sym_cond %in% sym_fc_cond)] |
| 298 | ||
| 299 |
# If there was an assignment operation detect direction of it. |
|
| 300 | 288x |
if (unique(x$text[assign_cond]) == "->") { # What if there are 2 assignments: e.g. a <- b -> c.
|
| 301 | 1x |
sym_cond <- rev(sym_cond) |
| 302 |
} |
|
| 303 | ||
| 304 | 288x |
after <- match(min(x$id[assign_cond]), sort(x$id[c(min(assign_cond), sym_cond)])) - 1 |
| 305 | 288x |
ans <- append(x[sym_cond, "text"], "<-", after = max(1, after)) |
| 306 | 288x |
ans <- move_functions_after_arrow(ans, unique(x[sym_fc_cond, "text"])) |
| 307 | 288x |
roll <- in_parenthesis(pd) |
| 308 | 288x |
if (length(roll)) {
|
| 309 | 3x |
c(setdiff(ans, roll), roll) |
| 310 |
} else {
|
|
| 311 | 285x |
ans |
| 312 |
} |
|
| 313 |
} |
|
| 314 | ||
| 315 |
#' Moves function names to the right side of dependency graph |
|
| 316 |
#' |
|
| 317 |
#' Changes status of the function call from dependent to dependency if occurs in the lhs. |
|
| 318 |
#' Technically, it means to move function names after the dependency operator. |
|
| 319 |
#' For example, for `attributes(a) <- b` the dependency graph should look like `c("a", "<-", "b", "attributes")`.
|
|
| 320 |
#' |
|
| 321 |
#' @param ans `character` vector of object names in dependency graph. |
|
| 322 |
#' @param functions `character` vector of function names. |
|
| 323 |
#' |
|
| 324 |
#' @return |
|
| 325 |
#' A character vector. |
|
| 326 |
#' @keywords internal |
|
| 327 |
#' @noRd |
|
| 328 |
move_functions_after_arrow <- function(ans, functions) {
|
|
| 329 | 288x |
arrow_pos <- which(ans == "<-") |
| 330 | 288x |
if (length(arrow_pos) == 0) {
|
| 331 | ! |
return(ans) |
| 332 |
} |
|
| 333 | 288x |
before_arrow <- setdiff(ans[1:arrow_pos], functions) |
| 334 | 288x |
after_arrow <- ans[(arrow_pos + 1):length(ans)] |
| 335 | 288x |
c(before_arrow, unique(c(intersect(ans[1:arrow_pos], functions), after_arrow))) |
| 336 |
} |
|
| 337 | ||
| 338 |
#' Extract side effects |
|
| 339 |
#' |
|
| 340 |
#' Extracts all object names from the code that are marked with `@linksto` tag. |
|
| 341 |
#' |
|
| 342 |
#' The code may contain functions calls that create side effects, e.g. modify the environment. |
|
| 343 |
#' Static code analysis may be insufficient to determine which objects are created or modified by such a function call. |
|
| 344 |
#' The `@linksto` comment tag is introduced to mark a call as having a (side) effect on one or more objects. |
|
| 345 |
#' With this tag a complete object dependency structure can be established. |
|
| 346 |
#' Read more about side effects and the usage of `@linksto` tag in [`get_code_dependencies()`] function. |
|
| 347 |
#' |
|
| 348 |
#' @param pd `data.frame`; |
|
| 349 |
#' one of the results of `utils::getParseData()` split into subsets representing individual calls; |
|
| 350 |
#' created by `extract_calls()` function |
|
| 351 |
#' |
|
| 352 |
#' @return |
|
| 353 |
#' A character vector of names of objects |
|
| 354 |
#' depending a call tagged with `@linksto` in a corresponding element of `pd`. |
|
| 355 |
#' |
|
| 356 |
#' @keywords internal |
|
| 357 |
#' @noRd |
|
| 358 |
extract_side_effects <- function(pd) {
|
|
| 359 | 376x |
linksto <- grep("@linksto", pd[pd$token == "COMMENT", "text"], value = TRUE)
|
| 360 | 376x |
unlist(strsplit(sub("\\s*#.*@linksto\\s+", "", linksto), "\\s+"))
|
| 361 |
} |
|
| 362 | ||
| 363 |
#' @param parsed_code results of `parse(text = code, keep.source = TRUE` (parsed text) |
|
| 364 |
#' @keywords internal |
|
| 365 |
#' @noRd |
|
| 366 |
extract_dependency <- function(parsed_code) {
|
|
| 367 | 412x |
full_pd <- normalize_pd(utils::getParseData(parsed_code)) |
| 368 | 412x |
reordered_full_pd <- extract_calls(full_pd) |
| 369 | ||
| 370 |
# Early return on empty code |
|
| 371 | 412x |
if (length(reordered_full_pd) == 0L) {
|
| 372 | 36x |
return(NULL) |
| 373 |
} |
|
| 374 | ||
| 375 | 376x |
if (length(parsed_code) == 0L) {
|
| 376 | 1x |
return(extract_side_effects(reordered_full_pd[[1]])) |
| 377 |
} |
|
| 378 | 375x |
expr_ix <- lapply(parsed_code[[1]], class) == "{"
|
| 379 | ||
| 380 |
# Build queue of expressions to parse individually |
|
| 381 | 375x |
queue <- list() |
| 382 | 375x |
parsed_code_list <- if (all(!expr_ix)) {
|
| 383 | 367x |
list(parsed_code) |
| 384 |
} else {
|
|
| 385 | 8x |
queue <- as.list(parsed_code[[1]][expr_ix]) |
| 386 | 8x |
new_list <- parsed_code[[1]] |
| 387 | 8x |
new_list[expr_ix] <- NULL |
| 388 | 8x |
list(parse(text = as.expression(new_list), keep.source = TRUE)) |
| 389 |
} |
|
| 390 | ||
| 391 | 375x |
while (length(queue) > 0) {
|
| 392 | 22x |
current <- queue[[1]] |
| 393 | 22x |
queue <- queue[-1] |
| 394 | 22x |
if (identical(current[[1L]], as.name("{"))) {
|
| 395 | 8x |
queue <- append(queue, as.list(current)[-1L]) |
| 396 |
} else {
|
|
| 397 | 14x |
parsed_code_list[[length(parsed_code_list) + 1]] <- parse(text = as.expression(current), keep.source = TRUE) |
| 398 |
} |
|
| 399 |
} |
|
| 400 | ||
| 401 | 375x |
parsed_occurences <- lapply( |
| 402 | 375x |
parsed_code_list, |
| 403 | 375x |
function(parsed_code) {
|
| 404 | 389x |
pd <- normalize_pd(utils::getParseData(parsed_code)) |
| 405 | 389x |
reordered_pd <- extract_calls(pd) |
| 406 | 389x |
if (length(reordered_pd) > 0) {
|
| 407 |
# extract_calls is needed to reorder the pd so that assignment operator comes before symbol names |
|
| 408 |
# extract_calls is needed also to substitute assignment operators into specific format with fix_arrows |
|
| 409 |
# extract_calls is needed to omit empty calls that contain only one token `"';'"` |
|
| 410 |
# This cleaning is needed as extract_occurrence assumes arrows are fixed, and order is different |
|
| 411 |
# than in original pd |
|
| 412 | 389x |
extract_occurrence(reordered_pd[[1]]) |
| 413 |
} |
|
| 414 |
} |
|
| 415 |
) |
|
| 416 | ||
| 417 |
# Merge results together |
|
| 418 | 375x |
result <- Reduce( |
| 419 | 375x |
function(u, v) {
|
| 420 | 389x |
ix <- if ("<-" %in% v) min(which(v == "<-")) else 0
|
| 421 | 389x |
u$left_side <- c(u$left_side, v[seq_len(max(0, ix - 1))]) |
| 422 | 389x |
u$right_side <- c( |
| 423 | 389x |
u$right_side, |
| 424 | 389x |
if (ix == length(v)) character(0L) else v[seq(ix + 1, max(ix + 1, length(v)))] |
| 425 |
) |
|
| 426 | 389x |
u |
| 427 |
}, |
|
| 428 | 375x |
init = list(left_side = character(0L), right_side = character(0L)), |
| 429 | 375x |
x = parsed_occurences |
| 430 |
) |
|
| 431 | ||
| 432 | 375x |
c(extract_side_effects(reordered_full_pd[[1]]), result$left_side, "<-", result$right_side) |
| 433 |
} |
|
| 434 | ||
| 435 |
# graph_parser ---- |
|
| 436 | ||
| 437 |
#' Return the indices of calls needed to reproduce an object |
|
| 438 |
#' |
|
| 439 |
#' @param x The name of the object to return code for. |
|
| 440 |
#' @param graph A result of `code_graph()`. |
|
| 441 |
#' |
|
| 442 |
#' @return |
|
| 443 |
#' Integer vector of indices that can be applied to `graph` to obtain all calls required to reproduce object `x`. |
|
| 444 |
#' |
|
| 445 |
#' @keywords internal |
|
| 446 |
#' @noRd |
|
| 447 |
graph_parser <- function(x, graph) {
|
|
| 448 |
# x occurrences (lhs) |
|
| 449 | 356x |
occurrence <- vapply( |
| 450 | 356x |
graph, function(call) {
|
| 451 | 612x |
ind <- match("<-", call, nomatch = length(call) + 1L)
|
| 452 | 612x |
x %in% call[seq_len(ind - 1L)] |
| 453 |
}, |
|
| 454 | 356x |
logical(1) |
| 455 |
) |
|
| 456 | ||
| 457 |
# x-dependent objects (rhs) |
|
| 458 | 356x |
dependencies <- lapply(graph[occurrence], function(call) {
|
| 459 | 157x |
ind <- match("<-", call, nomatch = 0L)
|
| 460 | 157x |
call[(ind + 1L):length(call)] |
| 461 |
}) |
|
| 462 | 356x |
dependencies <- setdiff(unlist(dependencies), x) |
| 463 | ||
| 464 | 356x |
dependency_occurrences <- lapply(dependencies, function(dependency) {
|
| 465 |
# track down dependencies and where they occur on the lhs in previous calls |
|
| 466 | 257x |
last_x_occurrence <- max(which(occurrence)) |
| 467 | 257x |
reduced_graph <- utils::head(graph[seq_len(last_x_occurrence)], -1) |
| 468 | 257x |
c(graph_parser(dependency, reduced_graph), last_x_occurrence) |
| 469 |
}) |
|
| 470 | ||
| 471 | 356x |
sort(unique(c(which(occurrence), unlist(dependency_occurrences)))) |
| 472 |
} |
|
| 473 | ||
| 474 | ||
| 475 |
# default_side_effects -------------------------------------------------------------------------------------------- |
|
| 476 | ||
| 477 |
#' Detect library calls |
|
| 478 |
#' |
|
| 479 |
#' Detects `library()` and `require()` function calls. |
|
| 480 |
#' |
|
| 481 |
#' @param `graph` the dependency graph, result of `lapply(code, attr, "dependency")` |
|
| 482 |
#' |
|
| 483 |
#' @return |
|
| 484 |
#' Integer vector of indices that can be applied to `graph` to obtain all calls containing |
|
| 485 |
#' `library()` or `require()` calls that are always returned for reproducibility. |
|
| 486 |
#' |
|
| 487 |
#' @keywords internal |
|
| 488 |
#' @noRd |
|
| 489 |
detect_libraries <- function(graph) {
|
|
| 490 | 84x |
defaults <- c("library", "require")
|
| 491 | ||
| 492 | 84x |
which( |
| 493 | 84x |
unlist( |
| 494 | 84x |
lapply( |
| 495 | 84x |
graph, function(x) {
|
| 496 | 241x |
any(grepl(pattern = paste(defaults, collapse = "|"), x = x)) |
| 497 |
} |
|
| 498 |
) |
|
| 499 |
) |
|
| 500 |
) |
|
| 501 |
} |
|
| 502 | ||
| 503 | ||
| 504 |
# utils ----------------------------------------------------------------------------------------------------------- |
|
| 505 | ||
| 506 | ||
| 507 |
#' Normalize parsed data removing backticks from symbols |
|
| 508 |
#' |
|
| 509 |
#' @param pd `data.frame` resulting from `utils::getParseData()` call. |
|
| 510 |
#' |
|
| 511 |
#' @return `data.frame` with backticks removed from `text` column for `SYMBOL` tokens. |
|
| 512 |
#' |
|
| 513 |
#' @keywords internal |
|
| 514 |
#' @noRd |
|
| 515 |
normalize_pd <- function(pd) {
|
|
| 516 |
# Remove backticks from SYMBOL tokens |
|
| 517 | 1012x |
symbol_index <- grepl("^SYMBOL.*$", pd$token)
|
| 518 | 1012x |
pd[symbol_index, "text"] <- gsub("^`(.*)`$", "\\1", pd[symbol_index, "text"])
|
| 519 | ||
| 520 | 1012x |
pd |
| 521 |
} |
|
| 522 | ||
| 523 | ||
| 524 |
# split_code ------------------------------------------------------------------------------------------------------ |
|
| 525 | ||
| 526 | ||
| 527 |
#' Get line/column in the source where the calls end |
|
| 528 |
#' |
|
| 529 |
#' |
|
| 530 |
#' @param code `character(1)` |
|
| 531 |
#' |
|
| 532 |
#' @return `matrix` with `colnames = c("line", "col")`
|
|
| 533 |
#' |
|
| 534 |
#' @keywords internal |
|
| 535 |
#' @noRd |
|
| 536 |
get_call_breaks <- function(code) {
|
|
| 537 | 211x |
parsed_code <- parse(text = code, keep.source = TRUE) |
| 538 | 211x |
pd <- utils::getParseData(parsed_code) |
| 539 | 211x |
pd <- normalize_pd(pd) |
| 540 | 211x |
pd <- pd[pd$token != "';'", ] |
| 541 | 211x |
call_breaks <- t(sapply( |
| 542 | 211x |
extract_calls(pd), |
| 543 | 211x |
function(x) {
|
| 544 | 376x |
matrix(c(max(x$line2), max(x$col2[x$line2 == max(x$line2)]))) |
| 545 |
} |
|
| 546 |
)) |
|
| 547 | 211x |
call_breaks <- call_breaks[-nrow(call_breaks), , drop = FALSE] # breaks in between needed only |
| 548 | 211x |
if (nrow(call_breaks) == 0L) {
|
| 549 | 122x |
call_breaks <- matrix(numeric(0), ncol = 2) |
| 550 |
} |
|
| 551 | 211x |
colnames(call_breaks) <- c("line", "col")
|
| 552 | 211x |
call_breaks |
| 553 |
} |
|
| 554 | ||
| 555 |
#' Split code by calls |
|
| 556 |
#' |
|
| 557 |
#' @param code `character` with the code. |
|
| 558 |
#' |
|
| 559 |
#' @return list of `character`s of the length equal to the number of calls in `code`. |
|
| 560 |
#' |
|
| 561 |
#' @keywords internal |
|
| 562 |
#' @noRd |
|
| 563 |
split_code <- function(code) {
|
|
| 564 | 211x |
call_breaks <- get_call_breaks(code) |
| 565 | 211x |
if (nrow(call_breaks) == 0) {
|
| 566 | 122x |
return(code) |
| 567 |
} |
|
| 568 | 89x |
call_breaks <- call_breaks[order(call_breaks[, "line"], call_breaks[, "col"]), , drop = FALSE] |
| 569 | 89x |
code_split <- strsplit(code, split = "\n", fixed = TRUE)[[1]] |
| 570 | 89x |
char_count_lines <- c(0, cumsum(sapply(code_split, nchar, USE.NAMES = FALSE) + 1), -1)[seq_along(code_split)] |
| 571 | ||
| 572 | 89x |
idx_start <- c( |
| 573 | 89x |
0, # first call starts in the beginning of src |
| 574 | 89x |
char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"] + 1 |
| 575 |
) |
|
| 576 | 89x |
idx_end <- c( |
| 577 | 89x |
char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"], |
| 578 | 89x |
nchar(code) # last call end in the end of src |
| 579 |
) |
|
| 580 | 89x |
new_code <- substring(code, idx_start, idx_end) |
| 581 | ||
| 582 |
# line split happens before call terminator (it could be `;` or `\n`) and the terminator goes to the next line |
|
| 583 |
# we need to move remove leading and add \n instead when combining calls |
|
| 584 | 89x |
c(new_code[1], gsub("^[\t ]*(\n|;)", "", new_code[-1]))
|
| 585 |
} |
| 1 |
#' Get code from `qenv` |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Retrieves the code stored in the `qenv`. |
|
| 5 |
#' |
|
| 6 |
#' @param object (`qenv`) |
|
| 7 |
#' @param deparse (`logical(1)`) flag specifying whether to return code as `character` or `expression`. |
|
| 8 |
#' @param ... internal usage, please ignore. |
|
| 9 |
#' @param names (`character`) `r lifecycle::badge("experimental")` vector of object names to return the code for.
|
|
| 10 |
#' For more details see the "Extracting dataset-specific code" section. |
|
| 11 |
#' |
|
| 12 |
#' @section Extracting dataset-specific code: |
|
| 13 |
#' |
|
| 14 |
#' `get_code(object, names)` limits the returned code to contain only those lines needed to _create_ |
|
| 15 |
#' the requested objects. The code stored in the `qenv` is analyzed statically to determine |
|
| 16 |
#' which lines the objects of interest depend upon. The analysis works well when objects are created |
|
| 17 |
#' with standard infix assignment operators (see `?assignOps`) but it can fail in some situations. |
|
| 18 |
#' |
|
| 19 |
#' Consider the following examples: |
|
| 20 |
#' |
|
| 21 |
#' _Case 1: Usual assignments._ |
|
| 22 |
#' ```r |
|
| 23 |
#' q1 <- |
|
| 24 |
#' within(qenv(), {
|
|
| 25 |
#' foo <- function(x) {
|
|
| 26 |
#' x + 1 |
|
| 27 |
#' } |
|
| 28 |
#' x <- 0 |
|
| 29 |
#' y <- foo(x) |
|
| 30 |
#' }) |
|
| 31 |
#' get_code(q1, names = "y") |
|
| 32 |
#' ``` |
|
| 33 |
#' `x` has no dependencies, so `get_code(data, names = "x")` will return only the second call.\cr |
|
| 34 |
#' `y` depends on `x` and `foo`, so `get_code(data, names = "y")` will contain all three calls. |
|
| 35 |
#' |
|
| 36 |
#' _Case 2: Some objects are created by a function's side effects._ |
|
| 37 |
#' ```r |
|
| 38 |
#' q2 <- |
|
| 39 |
#' within(qenv(){
|
|
| 40 |
#' foo <- function() {
|
|
| 41 |
#' x <<- x + 1 |
|
| 42 |
#' } |
|
| 43 |
#' x <- 0 |
|
| 44 |
#' foo() |
|
| 45 |
#' y <- x |
|
| 46 |
#' }) |
|
| 47 |
#' get_code(q2, names = "y") |
|
| 48 |
#' ``` |
|
| 49 |
#' Here, `y` depends on `x` but `x` is modified by `foo` as a side effect (not by reassignment) |
|
| 50 |
#' and so `get_code(data, names = "y")` will not return the `foo()` call.\cr |
|
| 51 |
#' To overcome this limitation, code dependencies can be specified manually. |
|
| 52 |
#' Lines where side effects occur can be flagged by adding "`# @linksto <object name>`" at the end.\cr |
|
| 53 |
#' Note that `within` evaluates code passed to `expr` as is and comments are ignored. |
|
| 54 |
#' In order to include comments in code one must use the `eval_code` function instead. |
|
| 55 |
#' |
|
| 56 |
#' ```r |
|
| 57 |
#' q3 <- |
|
| 58 |
#' eval_code(qenv(), " |
|
| 59 |
#' foo <- function() {
|
|
| 60 |
#' x <<- x + 1 |
|
| 61 |
#' } |
|
| 62 |
#' x <- 0 |
|
| 63 |
#' foo() # @linksto x |
|
| 64 |
#' y <- x |
|
| 65 |
#' ") |
|
| 66 |
#' get_code(q3, names = "y") |
|
| 67 |
#' ``` |
|
| 68 |
#' Now the `foo()` call will be properly included in the code required to recreate `y`. |
|
| 69 |
#' |
|
| 70 |
#' Note that two functions that create objects as side effects, `assign` and `data`, are handled automatically. |
|
| 71 |
#' |
|
| 72 |
#' Here are known cases where manual tagging is necessary: |
|
| 73 |
#' - non-standard assignment operators, _e.g._ `%<>%` |
|
| 74 |
#' - objects used as conditions in `if` statements: `if (<condition>)` |
|
| 75 |
#' - objects used to iterate over in `for` loops: `for(i in <sequence>)` |
|
| 76 |
#' - creating and evaluating language objects, _e.g._ `eval(<call>)` |
|
| 77 |
#' |
|
| 78 |
#' @return |
|
| 79 |
#' The code used in the `qenv` in the form specified by `deparse`. |
|
| 80 |
#' |
|
| 81 |
#' @examples |
|
| 82 |
#' # retrieve code |
|
| 83 |
#' q <- within(qenv(), {
|
|
| 84 |
#' a <- 1 |
|
| 85 |
#' b <- 2 |
|
| 86 |
#' }) |
|
| 87 |
#' get_code(q) |
|
| 88 |
#' get_code(q, deparse = FALSE) |
|
| 89 |
#' get_code(q, names = "a") |
|
| 90 |
#' |
|
| 91 |
#' q <- qenv() |
|
| 92 |
#' q <- eval_code(q, code = c("a <- 1", "b <- 2"))
|
|
| 93 |
#' get_code(q, names = "a") |
|
| 94 |
#' |
|
| 95 |
#' @aliases get_code,qenv-method |
|
| 96 |
#' @aliases get_code,qenv.error-method |
|
| 97 |
#' |
|
| 98 |
#' @export |
|
| 99 |
setGeneric("get_code", function(object, deparse = TRUE, names = NULL, ...) {
|
|
| 100 | 117x |
dev_suppress(object) |
| 101 | 117x |
standardGeneric("get_code")
|
| 102 |
}) |
|
| 103 | ||
| 104 |
setMethod("get_code", signature = "qenv", function(object, deparse = TRUE, names = NULL, ...) {
|
|
| 105 | 115x |
checkmate::assert_flag(deparse) |
| 106 | 115x |
checkmate::assert_character(names, min.len = 1L, null.ok = TRUE) |
| 107 | ||
| 108 |
# Normalize in case special it is backticked |
|
| 109 | 115x |
if (!is.null(names)) {
|
| 110 | 73x |
names <- gsub("^`(.*)`$", "\\1", names)
|
| 111 |
} |
|
| 112 | ||
| 113 | 115x |
code <- if (!is.null(names)) {
|
| 114 | 73x |
get_code_dependency(object@code, names, ...) |
| 115 |
} else {
|
|
| 116 | 42x |
object@code |
| 117 |
} |
|
| 118 | ||
| 119 | 115x |
if (deparse) {
|
| 120 | 113x |
paste(unlist(code), collapse = "\n") |
| 121 |
} else {
|
|
| 122 | 2x |
parse(text = paste(c("{", unlist(code), "}"), collapse = "\n"), keep.source = TRUE)
|
| 123 |
} |
|
| 124 |
}) |
|
| 125 | ||
| 126 |
setMethod("get_code", signature = "qenv.error", function(object, ...) {
|
|
| 127 | 2x |
stop( |
| 128 | 2x |
errorCondition( |
| 129 | 2x |
sprintf( |
| 130 | 2x |
"%s\n\ntrace: \n %s\n", |
| 131 | 2x |
conditionMessage(object), |
| 132 | 2x |
paste(object$trace, collapse = "\n ") |
| 133 |
), |
|
| 134 | 2x |
class = c("validation", "try-error", "simpleError")
|
| 135 |
) |
|
| 136 |
) |
|
| 137 |
}) |
| 1 |
#' Display `qenv` object |
|
| 2 |
#' |
|
| 3 |
#' Prints the `qenv` object. |
|
| 4 |
#' |
|
| 5 |
#' @param object (`qenv`) |
|
| 6 |
#' |
|
| 7 |
#' @return `object`, invisibly. |
|
| 8 |
#' |
|
| 9 |
#' @examples |
|
| 10 |
#' q <- qenv() |
|
| 11 |
#' q1 <- eval_code(q, expression(a <- 5, b <- data.frame(x = 1:10))) |
|
| 12 |
#' q1 |
|
| 13 |
#' |
|
| 14 |
#' @aliases show-qenv |
|
| 15 |
#' |
|
| 16 |
#' @importFrom methods show |
|
| 17 |
#' @export |
|
| 18 |
setMethod("show", "qenv", function(object) {
|
|
| 19 | ! |
env <- get_env(object) |
| 20 | ! |
header <- cli::col_blue(sprintf("<environment: %s>", rlang::env_label(env)))
|
| 21 | ! |
parent <- sprintf("Parent: <environment: %s>", rlang::env_label(rlang::env_parent(env)))
|
| 22 | ! |
cat(cli::style_bold(header), "\U1F512", "\n") |
| 23 | ! |
cat(parent, "\n") |
| 24 | ||
| 25 | ! |
shown <- ls(object) |
| 26 | ! |
if (length(shown > 0L)) cat(cli::style_bold("Bindings:\n"))
|
| 27 | ! |
lapply(shown, function(x) {
|
| 28 | ! |
cat( |
| 29 | ! |
sprintf( |
| 30 | ! |
"- %s: [%s]\n", |
| 31 | ! |
deparse(rlang::sym(x), backtick = TRUE), |
| 32 | ! |
class(object[[x]])[1] |
| 33 |
) |
|
| 34 |
) |
|
| 35 |
}) |
|
| 36 | ||
| 37 | ! |
hidden <- setdiff(ls(object, all.names = TRUE), shown) |
| 38 | ! |
lapply(hidden, function(x) {
|
| 39 | ! |
cat( |
| 40 | ! |
cli::style_blurred( |
| 41 | ! |
sprintf( |
| 42 | ! |
"- %s: [%s]\n", |
| 43 | ! |
deparse(rlang::sym(x), backtick = TRUE), |
| 44 | ! |
class(object[[x]])[1] |
| 45 |
) |
|
| 46 |
) |
|
| 47 |
) |
|
| 48 |
}) |
|
| 49 | ||
| 50 | ! |
invisible(object) |
| 51 |
}) |
| 1 |
#' Suppresses plot display in the IDE by opening a PDF graphics device |
|
| 2 |
#' |
|
| 3 |
#' This function opens a PDF graphics device using [`grDevices::pdf`] to suppress |
|
| 4 |
#' the plot display in the IDE. The purpose of this function is to avoid opening graphic devices |
|
| 5 |
#' directly in the IDE. |
|
| 6 |
#' |
|
| 7 |
#' @param x lazy binding which generates the plot(s) |
|
| 8 |
#' |
|
| 9 |
#' @details The function uses [`base::on.exit`] to ensure that the PDF graphics |
|
| 10 |
#' device is closed (using [`grDevices::dev.off`]) when the function exits, |
|
| 11 |
#' regardless of whether it exits normally or due to an error. This is necessary to |
|
| 12 |
#' clean up the graphics device properly and avoid any potential issues. |
|
| 13 |
#' |
|
| 14 |
#' @return No return value, called for side effects. |
|
| 15 |
#' |
|
| 16 |
#' @examples |
|
| 17 |
#' dev_suppress(plot(1:10)) |
|
| 18 |
#' @export |
|
| 19 |
dev_suppress <- function(x) {
|
|
| 20 | 137x |
grDevices::pdf(nullfile()) |
| 21 | 137x |
on.exit(grDevices::dev.off()) |
| 22 | 137x |
force(x) |
| 23 |
} |
|
| 24 | ||
| 25 |
#' Separate calls |
|
| 26 |
#' |
|
| 27 |
#' Converts language object or lists of language objects to list of simple calls. |
|
| 28 |
#' |
|
| 29 |
#' @param x `language` object or a list of thereof |
|
| 30 |
#' @return |
|
| 31 |
#' Given a `call`, an `expression`, a list of `call`s or a list of `expression`s, returns a list of `calls`. |
|
| 32 |
#' Symbols and atomic vectors (which may get mixed up in a list) are returned wrapped in list. |
|
| 33 |
#' @examples |
|
| 34 |
#' # use non-exported function from teal.code |
|
| 35 |
#' lang2calls <- getFromNamespace("lang2calls", "teal.code")
|
|
| 36 |
#' expr <- expression( |
|
| 37 |
#' i <- iris, |
|
| 38 |
#' m <- mtcars |
|
| 39 |
#' ) |
|
| 40 |
#' lang2calls(expr) |
|
| 41 |
#' @keywords internal |
|
| 42 |
lang2calls <- function(x) {
|
|
| 43 | 270x |
if (is.atomic(x) || is.symbol(x)) {
|
| 44 | 9x |
return(list(x)) |
| 45 |
} |
|
| 46 | 261x |
if (is.call(x)) {
|
| 47 | 176x |
if (identical(as.list(x)[[1L]], as.symbol("{"))) {
|
| 48 | 45x |
as.list(x)[-1L] |
| 49 |
} else {
|
|
| 50 | 131x |
list(x) |
| 51 |
} |
|
| 52 |
} else {
|
|
| 53 | 85x |
unlist(lapply(x, lang2calls), recursive = FALSE) |
| 54 |
} |
|
| 55 |
} |
|
| 56 | ||
| 57 |
#' Obtain warnings or messages from code slot |
|
| 58 |
#' |
|
| 59 |
#' @param object (`qenv`) |
|
| 60 |
#' @param what (`warning` or `message`) |
|
| 61 |
#' @return `character(1)` containing combined message or `NULL` when no warnings/messages |
|
| 62 |
#' @keywords internal |
|
| 63 |
get_warn_message_util <- function(object, what) {
|
|
| 64 | 14x |
checkmate::matchArg(what, choices = c("warning", "message"))
|
| 65 | 14x |
messages <- lapply( |
| 66 | 14x |
object@code, |
| 67 | 14x |
function(x) {
|
| 68 | 24x |
unlist(lapply( |
| 69 | 24x |
attr(x, "outputs"), |
| 70 | 24x |
function(el) {
|
| 71 | 20x |
if (inherits(el, what)) {
|
| 72 | 20x |
sprintf("> %s", conditionMessage(el))
|
| 73 |
} |
|
| 74 |
} |
|
| 75 |
)) |
|
| 76 |
} |
|
| 77 |
) |
|
| 78 | ||
| 79 | 14x |
idx_warn <- which(sapply(messages, function(x) !is.null(x) && !identical(x, ""))) |
| 80 | 14x |
if (!any(idx_warn)) {
|
| 81 | 2x |
return(NULL) |
| 82 |
} |
|
| 83 | 12x |
messages <- messages[idx_warn] |
| 84 | 12x |
code <- object@code[idx_warn] |
| 85 | ||
| 86 | 12x |
lines <- mapply( |
| 87 | 12x |
warn = messages, |
| 88 | 12x |
expr = code, |
| 89 | 12x |
function(warn, expr) {
|
| 90 | 20x |
sprintf("%s\nwhen running code:\n%s", trimws(warn), trimws(expr))
|
| 91 |
} |
|
| 92 |
) |
|
| 93 | ||
| 94 | 12x |
sprintf( |
| 95 | 12x |
"~~~ %ss ~~~\n\n%s\n\n~~~ Trace ~~~\n\n%s", |
| 96 | 12x |
tools::toTitleCase(what), |
| 97 | 12x |
paste(lines, collapse = "\n\n"), |
| 98 | 12x |
paste(get_code(object), collapse = "\n") |
| 99 |
) |
|
| 100 |
} |
| 1 |
#' Evaluate code in `qenv` |
|
| 2 |
#' |
|
| 3 |
#' @details |
|
| 4 |
#' |
|
| 5 |
#' `eval_code()` evaluates given code in the `qenv` environment and appends it to the `code` slot. |
|
| 6 |
#' Thus, if the `qenv` had been instantiated empty, contents of the environment are always a result of the stored code. |
|
| 7 |
#' |
|
| 8 |
#' @param object (`qenv`) |
|
| 9 |
#' @param code (`character`, `language` or `expression`) code to evaluate. |
|
| 10 |
#' It is possible to preserve original formatting of the `code` by providing a `character` or an |
|
| 11 |
#' `expression` being a result of `parse(keep.source = TRUE)`. |
|
| 12 |
#' @param ... ([`dots`]) additional arguments passed to future methods. |
|
| 13 |
#' |
|
| 14 |
#' @return |
|
| 15 |
#' `qenv` environment with `code/expr` evaluated or `qenv.error` if evaluation fails. |
|
| 16 |
#' |
|
| 17 |
#' @examples |
|
| 18 |
#' # evaluate code in qenv |
|
| 19 |
#' q <- qenv() |
|
| 20 |
#' q <- eval_code(q, "a <- 1") |
|
| 21 |
#' q <- eval_code(q, "b <- 2L # with comment") |
|
| 22 |
#' q <- eval_code(q, quote(library(checkmate))) |
|
| 23 |
#' q <- eval_code(q, expression(assert_number(a))) |
|
| 24 |
#' |
|
| 25 |
#' @aliases eval_code,qenv-method |
|
| 26 |
#' @aliases eval_code,qenv.error-method |
|
| 27 |
#' @seealso [within.qenv] |
|
| 28 |
#' @export |
|
| 29 | 215x |
setGeneric("eval_code", function(object, code, ...) standardGeneric("eval_code"))
|
| 30 | ||
| 31 |
setMethod("eval_code", signature = c(object = "qenv"), function(object, code, ...) {
|
|
| 32 | 215x |
if (!is.language(code) && !is.character(code)) {
|
| 33 | 3x |
stop("eval_code accepts code being language or character")
|
| 34 |
} |
|
| 35 | 212x |
code <- .preprocess_code(code) |
| 36 |
# preprocess code to ensure it is a character vector |
|
| 37 | 212x |
.eval_code(object = object, code = code, ...) |
| 38 |
}) |
|
| 39 | ||
| 40 | ! |
setMethod("eval_code", signature = c(object = "qenv.error"), function(object, code, ...) object)
|
| 41 | ||
| 42 |
#' @keywords internal |
|
| 43 |
.eval_code <- function(object, code, ...) {
|
|
| 44 | 212x |
if (identical(trimws(code), "") || length(code) == 0) {
|
| 45 | 2x |
return(object) |
| 46 |
} |
|
| 47 | 210x |
code <- paste(split_code(code), collapse = "\n") |
| 48 | ||
| 49 | 210x |
object@.xData <- rlang::env_clone(object@.xData, parent = parent.env(object@.xData)) |
| 50 | 210x |
parsed_code <- parse(text = code, keep.source = TRUE) |
| 51 | ||
| 52 | 210x |
old <- evaluate::inject_funs( |
| 53 | 210x |
library = function(...) {
|
| 54 | 5x |
x <- library(...) |
| 55 | 5x |
if (!identical(parent.env(object@.xData), parent.env(.GlobalEnv))) {
|
| 56 | 2x |
parent.env(object@.xData) <- parent.env(.GlobalEnv) |
| 57 |
} |
|
| 58 | 5x |
invisible(x) |
| 59 |
} |
|
| 60 |
) |
|
| 61 | 210x |
out <- evaluate::evaluate( |
| 62 | 210x |
code, |
| 63 | 210x |
envir = object@.xData, |
| 64 | 210x |
stop_on_error = 1, |
| 65 | 210x |
output_handler = evaluate::new_output_handler(value = identity) |
| 66 |
) |
|
| 67 | 210x |
out <- evaluate::trim_intermediate_plots(out) |
| 68 | ||
| 69 | 210x |
evaluate::inject_funs(old) # remove library() override |
| 70 | ||
| 71 | 210x |
new_code <- list() |
| 72 | 210x |
for (this in out) {
|
| 73 | 483x |
if (inherits(this, "source")) {
|
| 74 | 406x |
this_code <- gsub("\n$", "", this$src)
|
| 75 | 406x |
attr(this_code, "dependency") <- extract_dependency(parse(text = this_code, keep.source = TRUE)) |
| 76 | 406x |
new_code <- c(new_code, stats::setNames(list(this_code), sample.int(.Machine$integer.max, size = 1))) |
| 77 |
} else {
|
|
| 78 | 77x |
last_code <- new_code[[length(new_code)]] |
| 79 | 77x |
if (inherits(this, "error")) {
|
| 80 | 14x |
return( |
| 81 | 14x |
errorCondition( |
| 82 | 14x |
message = sprintf( |
| 83 | 14x |
"%s \n when evaluating qenv code:\n%s", |
| 84 | 14x |
cli::ansi_strip(conditionMessage(this)), |
| 85 | 14x |
last_code |
| 86 |
), |
|
| 87 | 14x |
class = c("qenv.error", "try-error", "simpleError"),
|
| 88 | 14x |
trace = unlist(c(object@code, list(new_code))) |
| 89 |
) |
|
| 90 |
) |
|
| 91 |
} |
|
| 92 | 63x |
attr(last_code, "outputs") <- c(attr(last_code, "outputs"), list(this)) |
| 93 | 63x |
new_code[[length(new_code)]] <- last_code |
| 94 |
} |
|
| 95 |
} |
|
| 96 | ||
| 97 | 196x |
object@code <- c(object@code, new_code) |
| 98 | 196x |
lockEnvironment(object@.xData, bindings = TRUE) |
| 99 | 196x |
object |
| 100 |
} |
|
| 101 | ||
| 102 | 212x |
setGeneric(".preprocess_code", function(code) standardGeneric(".preprocess_code"))
|
| 103 | 71x |
setMethod(".preprocess_code", signature = c("character"), function(code) paste(code, collapse = "\n"))
|
| 104 |
setMethod(".preprocess_code", signature = c("ANY"), function(code) {
|
|
| 105 | 141x |
if (is.expression(code) && length(attr(code, "wholeSrcref"))) {
|
| 106 | 2x |
paste(attr(code, "wholeSrcref"), collapse = "\n") |
| 107 |
} else {
|
|
| 108 | 139x |
paste( |
| 109 | 139x |
vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)), |
| 110 | 139x |
collapse = "\n" |
| 111 |
) |
|
| 112 |
} |
|
| 113 |
}) |
| 1 |
#' Get messages from `qenv` object |
|
| 2 |
#' |
|
| 3 |
#' Retrieve all messages raised during code evaluation in a `qenv`. |
|
| 4 |
#' |
|
| 5 |
#' @param object (`qenv`) |
|
| 6 |
#' |
|
| 7 |
#' @return `character` containing warning information or `NULL` if no messages. |
|
| 8 |
#' |
|
| 9 |
#' @examples |
|
| 10 |
#' data_q <- qenv() |
|
| 11 |
#' data_q <- eval_code(data_q, "iris_data <- iris") |
|
| 12 |
#' warning_qenv <- eval_code( |
|
| 13 |
#' data_q, |
|
| 14 |
#' bquote(p <- hist(iris_data[, .("Sepal.Length")], ff = ""))
|
|
| 15 |
#' ) |
|
| 16 |
#' cat(get_messages(warning_qenv)) |
|
| 17 |
#' |
|
| 18 |
#' @name get_messages |
|
| 19 |
#' @rdname get_messages |
|
| 20 |
#' @aliases get_messages,qenv-method |
|
| 21 |
#' @aliases get_messages,qenv.error-method |
|
| 22 |
#' @aliases get_messages,NULL-method |
|
| 23 |
#' |
|
| 24 |
#' @export |
|
| 25 |
setGeneric("get_messages", function(object) {
|
|
| 26 | 9x |
dev_suppress(object) |
| 27 | 9x |
standardGeneric("get_messages")
|
| 28 |
}) |
|
| 29 | ||
| 30 |
setMethod("get_messages", signature = "qenv", function(object) {
|
|
| 31 | 7x |
get_warn_message_util(object, "message") |
| 32 |
}) |
|
| 33 | ||
| 34 |
setMethod("get_messages", signature = "qenv.error", function(object) {
|
|
| 35 | 1x |
NULL |
| 36 |
}) |
|
| 37 | ||
| 38 |
setMethod("get_messages", "NULL", function(object) {
|
|
| 39 | 1x |
NULL |
| 40 |
}) |
| 1 |
#' Join `qenv` objects |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' `r lifecycle::badge("deprecated")`
|
|
| 5 |
#' Instead of [join()] use [c()]. |
|
| 6 |
#' |
|
| 7 |
#' @param ... function is deprecated. |
|
| 8 |
#' |
|
| 9 |
#' @name join |
|
| 10 |
#' @rdname join |
|
| 11 |
#' |
|
| 12 |
#' @export |
|
| 13 | ! |
join <- function(...) lifecycle::deprecate_stop("0.7.0", "join()", "c()")
|
| 1 |
#' Concatenate two `qenv` objects |
|
| 2 |
#' |
|
| 3 |
#' Combine two `qenv` objects by simple concatenate their environments and the code. |
|
| 4 |
#' |
|
| 5 |
#' We recommend to use the `join` method to have a stricter control |
|
| 6 |
#' in case `x` and `y` contain duplicated bindings and code. |
|
| 7 |
#' RHS argument content has priority over the LHS one. |
|
| 8 |
#' |
|
| 9 |
#' @param x (`qenv`) |
|
| 10 |
#' @param y (`qenv`) |
|
| 11 |
#' |
|
| 12 |
#' @return `qenv` object. |
|
| 13 |
#' |
|
| 14 |
#' @examples |
|
| 15 |
#' q <- qenv() |
|
| 16 |
#' q1 <- eval_code(q, expression(iris1 <- iris, mtcars1 <- mtcars)) |
|
| 17 |
#' q2 <- q1 |
|
| 18 |
#' q1 <- eval_code(q1, "iris2 <- iris") |
|
| 19 |
#' q2 <- eval_code(q2, "mtcars2 <- mtcars") |
|
| 20 |
#' qq <- concat(q1, q2) |
|
| 21 |
#' get_code(qq) |
|
| 22 |
#' |
|
| 23 |
#' @include qenv-errors.R |
|
| 24 |
#' |
|
| 25 |
#' @name concat |
|
| 26 |
#' @rdname concat |
|
| 27 |
#' @aliases concat,qenv,qenv-method |
|
| 28 |
#' @aliases concat,qenv.error,ANY-method |
|
| 29 |
#' @aliases concat,qenv,qenv.error-method |
|
| 30 |
#' |
|
| 31 |
#' @export |
|
| 32 | 9x |
setGeneric("concat", function(x, y) standardGeneric("concat"))
|
| 33 | ||
| 34 |
setMethod("concat", signature = c("qenv", "qenv"), function(x, y) {
|
|
| 35 | 5x |
y@code <- c(x@code, y@code) |
| 36 | ||
| 37 |
# insert (and overwrite) objects from y to x |
|
| 38 | 5x |
y@.xData <- rlang::env_clone(y@.xData, parent = parent.env(.GlobalEnv)) |
| 39 | 5x |
rlang::env_coalesce(env = y@.xData, from = x@.xData) |
| 40 | 5x |
y |
| 41 |
}) |
|
| 42 | ||
| 43 |
setMethod("concat", signature = c("qenv.error", "ANY"), function(x, y) {
|
|
| 44 | 3x |
x |
| 45 |
}) |
|
| 46 | ||
| 47 |
setMethod("concat", signature = c("qenv", "qenv.error"), function(x, y) {
|
|
| 48 | 1x |
y |
| 49 |
}) |
| 1 |
#' Reproducible class with environment and code |
|
| 2 |
#' |
|
| 3 |
#' Reproducible class with environment and code. |
|
| 4 |
#' @name qenv-class |
|
| 5 |
#' @rdname qenv-class |
|
| 6 |
#' @slot .xData (`environment`) environment with content was generated by the evaluation |
|
| 7 |
#' @slot code (`named list` of `character`) representing code necessary to reproduce the environment. |
|
| 8 |
#' Read more in Code section. |
|
| 9 |
#' of the `code` slot. |
|
| 10 |
#' |
|
| 11 |
#' @section Code: |
|
| 12 |
#' |
|
| 13 |
#' Each code element is a character representing one call. Each element is named with the random |
|
| 14 |
#' identifier to make sure uniqueness when joining. Each element has possible attributes: |
|
| 15 |
#' - `warnings` (`character`) the warnings output when evaluating the code element. |
|
| 16 |
#' - `messages` (`character`) the messages output when evaluating the code element. |
|
| 17 |
#' - `dependency` (`character`) names of objects that appear in this call and gets affected by this call, |
|
| 18 |
#' separated by `<-` (objects on LHS of `<-` are affected by this line, and objects on RHS are affecting this line). |
|
| 19 |
#' |
|
| 20 |
#' @keywords internal |
|
| 21 |
#' @exportClass qenv |
|
| 22 |
setClass( |
|
| 23 |
"qenv", |
|
| 24 |
slots = c(code = "list"), |
|
| 25 |
contains = "environment" |
|
| 26 |
) |
|
| 27 | ||
| 28 |
#' It initializes the `qenv` class |
|
| 29 |
#' @noRd |
|
| 30 |
setMethod( |
|
| 31 |
"initialize", |
|
| 32 |
"qenv", |
|
| 33 |
function(.Object, .xData, code = list(), ...) { # nolint: object_name.
|
|
| 34 | 195x |
parent <- parent.env(.GlobalEnv) |
| 35 | 195x |
new_xdata <- if (rlang::is_missing(.xData)) {
|
| 36 | 193x |
new.env(parent = parent) |
| 37 |
} else {
|
|
| 38 | 2x |
checkmate::assert_environment(.xData) |
| 39 | 1x |
rlang::env_clone(.xData, parent = parent) |
| 40 |
} |
|
| 41 | 194x |
lockEnvironment(new_xdata, bindings = TRUE) |
| 42 | ||
| 43 |
# .xData needs to be unnamed as the `.environment` constructor allows at |
|
| 44 |
# most 1 unnamed formal argument of class `environment`. |
|
| 45 |
# See methods::findMethods("initialize")$.environment
|
|
| 46 | 194x |
methods::callNextMethod( |
| 47 | 194x |
.Object, |
| 48 | 194x |
new_xdata, # Mandatory use of unnamed environment arg |
| 49 | 194x |
code = code, ... |
| 50 |
) |
|
| 51 |
} |
|
| 52 |
) |
|
| 53 | ||
| 54 |
#' It takes a `qenv` class and returns `TRUE` if the input is valid |
|
| 55 |
#' @name qenv-class |
|
| 56 |
#' @keywords internal |
|
| 57 |
setValidity("qenv", function(object) {
|
|
| 58 |
if (any(duplicated(names(object@code)))) {
|
|
| 59 |
"@code must have unique names." |
|
| 60 |
} else if (!environmentIsLocked(object@.xData)) {
|
|
| 61 |
"@.xData must be locked." |
|
| 62 |
} else {
|
|
| 63 |
TRUE |
|
| 64 |
} |
|
| 65 |
}) |
| 1 |
# needed to handle try-error |
|
| 2 |
setOldClass("qenv.error")
|
|
| 3 | ||
| 4 |
#' @export |
|
| 5 |
as.list.qenv.error <- function(x, ...) {
|
|
| 6 | ! |
stop(errorCondition( |
| 7 | ! |
list(message = conditionMessage(x)), |
| 8 | ! |
class = c("validation", "try-error", "simpleError")
|
| 9 |
)) |
|
| 10 |
} |
| 1 |
#' If two `qenv` can be joined |
|
| 2 |
#' |
|
| 3 |
#' Checks if two `qenv` objects can be combined. |
|
| 4 |
#' For more information, please see [`join`] |
|
| 5 |
#' @param x (`qenv`) |
|
| 6 |
#' @param y (`qenv`) |
|
| 7 |
#' @return `TRUE` if able to join or `character` used to print error message. |
|
| 8 |
#' @keywords internal |
|
| 9 |
.check_joinable <- function(x, y) {
|
|
| 10 | 16x |
checkmate::assert_class(x, "qenv") |
| 11 | 16x |
checkmate::assert_class(y, "qenv") |
| 12 | ||
| 13 | 16x |
common_names <- intersect(rlang::env_names(x@.xData), rlang::env_names(y@.xData)) |
| 14 | 16x |
is_overwritten <- vapply(common_names, function(el) {
|
| 15 | 13x |
!identical(get(el, x@.xData), get(el, y@.xData)) |
| 16 | 16x |
}, logical(1)) |
| 17 | 16x |
if (any(is_overwritten)) {
|
| 18 | 2x |
return( |
| 19 | 2x |
paste( |
| 20 | 2x |
"Not possible to join qenv objects if anything in their environment has been modified.\n", |
| 21 | 2x |
"Following object(s) have been modified:\n - ", |
| 22 | 2x |
paste(common_names[is_overwritten], collapse = "\n - ") |
| 23 |
) |
|
| 24 |
) |
|
| 25 |
} |
|
| 26 | ||
| 27 | 14x |
x_id <- names(x@code) |
| 28 | 14x |
y_id <- names(y@code) |
| 29 | ||
| 30 | 14x |
shared_ids <- intersect(x_id, y_id) |
| 31 | 14x |
if (length(shared_ids) == 0) {
|
| 32 | 8x |
return(TRUE) |
| 33 |
} |
|
| 34 | ||
| 35 | 6x |
shared_in_x <- match(shared_ids, x_id) |
| 36 | 6x |
shared_in_y <- match(shared_ids, y_id) |
| 37 | ||
| 38 |
# indices of shared ids should be 1:n in both slots |
|
| 39 | 6x |
if (identical(shared_in_x, shared_in_y) && identical(shared_in_x, seq_along(shared_ids))) {
|
| 40 | 4x |
TRUE |
| 41 | 2x |
} else if (!identical(shared_in_x, shared_in_y)) {
|
| 42 | 1x |
paste( |
| 43 | 1x |
"The common shared code of the qenvs does not occur in the same position in both qenv objects", |
| 44 | 1x |
"so they cannot be joined together as it's impossible to determine the evaluation's order.", |
| 45 | 1x |
collapse = "" |
| 46 |
) |
|
| 47 |
} else {
|
|
| 48 | 1x |
paste( |
| 49 | 1x |
"There is code in the qenv objects before their common shared code", |
| 50 | 1x |
"which means these objects cannot be joined.", |
| 51 | 1x |
collapse = "" |
| 52 |
) |
|
| 53 |
} |
|
| 54 |
} |
|
| 55 | ||
| 56 |
#' @rdname join |
|
| 57 |
#' @param ... (`qenv` or `qenv.error`). |
|
| 58 |
#' @examples |
|
| 59 |
#' q <- qenv() |
|
| 60 |
#' q1 <- within(q, {
|
|
| 61 |
#' iris1 <- iris |
|
| 62 |
#' mtcars1 <- mtcars |
|
| 63 |
#' }) |
|
| 64 |
#' q1 <- within(q1, iris2 <- iris) |
|
| 65 |
#' q2 <- within(q1, mtcars2 <- mtcars) |
|
| 66 |
#' qq <- c(q1, q2) |
|
| 67 |
#' cat(get_code(qq)) |
|
| 68 |
#' |
|
| 69 |
#' @export |
|
| 70 |
c.qenv <- function(...) {
|
|
| 71 | 211x |
dots <- rlang::list2(...) |
| 72 | 211x |
if (!checkmate::test_list(dots[-1], types = c("qenv", "qenv.error"))) {
|
| 73 | 194x |
return(NextMethod(c, dots[[1]])) |
| 74 |
} |
|
| 75 | ||
| 76 | 17x |
first_non_qenv_ix <- which.min(vapply(dots, inherits, what = "qenv", logical(1))) |
| 77 | 17x |
if (first_non_qenv_ix > 1) {
|
| 78 | 1x |
return(dots[[first_non_qenv_ix]]) |
| 79 |
} |
|
| 80 | ||
| 81 | 16x |
Reduce( |
| 82 | 16x |
x = dots[-1], |
| 83 | 16x |
init = dots[[1]], |
| 84 | 16x |
f = function(x, y) {
|
| 85 | 16x |
join_validation <- .check_joinable(x, y) |
| 86 | ||
| 87 |
# join expressions |
|
| 88 | 16x |
if (!isTRUE(join_validation)) {
|
| 89 | 4x |
stop(join_validation) |
| 90 |
} |
|
| 91 | ||
| 92 | 12x |
x@code <- utils::modifyList(x@code, y@code) |
| 93 | ||
| 94 |
# insert (and overwrite) objects from y to x |
|
| 95 | 12x |
x@.xData <- rlang::env_clone(x@.xData, parent = parent.env(.GlobalEnv)) |
| 96 | 12x |
rlang::env_coalesce(env = x@.xData, from = y@.xData) |
| 97 | 12x |
x |
| 98 |
} |
|
| 99 |
) |
|
| 100 |
} |
|
| 101 | ||
| 102 |
#' @rdname join |
|
| 103 |
#' @export |
|
| 104 |
c.qenv.error <- function(...) {
|
|
| 105 | 3x |
rlang::list2(...)[[1]] |
| 106 |
} |
| 1 |
#' Access environment included in `qenv` |
|
| 2 |
#' |
|
| 3 |
#' The access of environment included in the `qenv` that contains all data objects. |
|
| 4 |
#' |
|
| 5 |
#' @param object (`qenv`). |
|
| 6 |
#' |
|
| 7 |
#' @return An `environment` stored in `qenv` with all data objects. |
|
| 8 |
#' |
|
| 9 |
#' @examples |
|
| 10 |
#' q <- qenv() |
|
| 11 |
#' q1 <- within(q, {
|
|
| 12 |
#' a <- 5 |
|
| 13 |
#' b <- data.frame(x = 1:10) |
|
| 14 |
#' }) |
|
| 15 |
#' get_env(q1) |
|
| 16 |
#' |
|
| 17 |
#' @aliases get_env,qenv-method |
|
| 18 |
#' @aliases get_env,qenv.error-method |
|
| 19 |
#' |
|
| 20 |
#' @export |
|
| 21 |
setGeneric("get_env", function(object) {
|
|
| 22 | 14x |
standardGeneric("get_env")
|
| 23 |
}) |
|
| 24 | ||
| 25 | 14x |
setMethod("get_env", "qenv", function(object) object@.xData)
|
| 26 | ||
| 27 | ! |
setMethod("get_env", "qenv.error", function(object) object)
|
| 1 |
#' Subsets `qenv` |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Subsets [`qenv`] environment and limits the code to the necessary needed to build limited objects. |
|
| 5 |
#' |
|
| 6 |
#' @param x (`qenv`) |
|
| 7 |
#' @param names (`character`) names of objects included in [`qenv`] to subset. Names not present in [`qenv`] |
|
| 8 |
#' are skipped. |
|
| 9 |
#' @param ... internal usage, please ignore. |
|
| 10 |
#' |
|
| 11 |
#' @name subset-qenv |
|
| 12 |
#' |
|
| 13 |
#' @examples |
|
| 14 |
#' q <- qenv() |
|
| 15 |
#' q <- eval_code(q, "a <- 1;b<-2") |
|
| 16 |
#' q["a"] |
|
| 17 |
#' q[c("a", "b")]
|
|
| 18 |
#' |
|
| 19 |
#' @export |
|
| 20 |
`[.qenv` <- function(x, names, ...) {
|
|
| 21 | 12x |
checkmate::assert_character(names, any.missing = FALSE) |
| 22 | 12x |
possible_names <- ls(get_env(x), all.names = TRUE) |
| 23 | 12x |
names_corrected <- intersect(names, possible_names) |
| 24 | 12x |
env <- if (length(names_corrected)) {
|
| 25 | 9x |
names_missing <- setdiff(names, possible_names) |
| 26 | 9x |
if (length(names_missing)) {
|
| 27 | 2x |
warning( |
| 28 | 2x |
sprintf( |
| 29 | 2x |
"Some 'names' do not exist in the environment of the '%s'. Skipping those: %s.", |
| 30 | 2x |
class(x)[1], |
| 31 | 2x |
paste(names_missing, collapse = ", ") |
| 32 |
) |
|
| 33 |
) |
|
| 34 |
} |
|
| 35 | 9x |
list2env(as.list(x, all.names = TRUE)[names_corrected], parent = parent.env(.GlobalEnv)) |
| 36 |
} else {
|
|
| 37 | 3x |
warning( |
| 38 | 3x |
sprintf( |
| 39 | 3x |
"None of 'names' exist in the environment of the '%1$s'. Returning empty '%1$s'.", |
| 40 | 3x |
class(x)[1] |
| 41 |
), |
|
| 42 | 3x |
call. = FALSE |
| 43 |
) |
|
| 44 | 3x |
new.env(parent = parent.env(.GlobalEnv)) |
| 45 |
} |
|
| 46 | 12x |
lockEnvironment(env) |
| 47 | 12x |
x@.xData <- env |
| 48 | ||
| 49 | 12x |
normalized_names <- gsub("^`(.*)`$", "\\1", names)
|
| 50 | 12x |
x@code <- get_code_dependency(x@code, names = normalized_names, ...) |
| 51 | ||
| 52 | 12x |
x |
| 53 |
} |
| 1 |
#' Get object from `qenv` |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' `r lifecycle::badge("deprecated")`
|
|
| 5 |
#' Instead of [get_var()] use native \R operators/functions: |
|
| 6 |
#' `x[[name]]`, `x$name` or [get()]: |
|
| 7 |
#' |
|
| 8 |
#' @param ... function is deprecated. |
|
| 9 |
#' @param x (`qenv`) |
|
| 10 |
#' @param i (`character(1)`) variable name. |
|
| 11 |
#' |
|
| 12 |
#' @export |
|
| 13 | ! |
get_var <- function(...) lifecycle::deprecate_stop("0.7.0", "get_var()", "base::get()")
|
| 14 | ||
| 15 |
#' @rdname get_var |
|
| 16 |
#' @export |
|
| 17 |
`[[.qenv.error` <- function(x, i) {
|
|
| 18 | 1x |
stop(errorCondition( |
| 19 | 1x |
list(message = conditionMessage(x)), |
| 20 | 1x |
class = c("validation", "try-error", "simpleError")
|
| 21 |
)) |
|
| 22 |
} |
|
| 23 | ||
| 24 |
#' @export |
|
| 25 | 4x |
names.qenv.error <- function(x) NULL |
| 26 | ||
| 27 |
#' @export |
|
| 28 |
`$.qenv.error` <- function(x, name) {
|
|
| 29 |
# Must allow access of elements in qenv.error object (message, call, trace, ...) |
|
| 30 |
# Otherwise, it will enter an infinite recursion with the `conditionMessage(x)` call. |
|
| 31 | 8x |
if (exists(name, x)) {
|
| 32 | 7x |
return(NextMethod("$", x))
|
| 33 |
} |
|
| 34 | ||
| 35 | 1x |
class(x) <- setdiff(class(x), "qenv.error") |
| 36 | 1x |
stop(errorCondition( |
| 37 | 1x |
list(message = conditionMessage(x)), |
| 38 | 1x |
class = c("validation", "try-error", "simpleError")
|
| 39 |
)) |
|
| 40 |
} |
| 1 |
#' Evaluate code in `qenv` |
|
| 2 |
#' @details |
|
| 3 |
#' `within()` is a convenience method that wraps `eval_code` to provide a simplified way of passing expression. |
|
| 4 |
#' `within` accepts only inline expressions (both simple and compound) and allows to substitute `expr` |
|
| 5 |
#' with `...` named argument values. |
|
| 6 |
#' |
|
| 7 |
#' @section Using language objects with `within`: |
|
| 8 |
#' Passing language objects to `expr` is generally not intended but can be achieved with `do.call`. |
|
| 9 |
#' Only single `expression`s will work and substitution is not available. See examples. |
|
| 10 |
#' |
|
| 11 |
#' @param data (`qenv`) |
|
| 12 |
#' @param expr (`expression`) to evaluate. Must be inline code, see `Using language objects...` |
|
| 13 |
#' @param ... named argument value will substitute a symbol in the `expr` matched by the name. |
|
| 14 |
#' For practical usage see Examples section below. |
|
| 15 |
#' |
|
| 16 |
#' @examples |
|
| 17 |
#' # evaluate code using within |
|
| 18 |
#' q <- qenv() |
|
| 19 |
#' q <- within(q, {
|
|
| 20 |
#' i <- iris |
|
| 21 |
#' }) |
|
| 22 |
#' q <- within(q, {
|
|
| 23 |
#' m <- mtcars |
|
| 24 |
#' f <- faithful |
|
| 25 |
#' }) |
|
| 26 |
#' q |
|
| 27 |
#' get_code(q) |
|
| 28 |
#' |
|
| 29 |
#' # inject values into code |
|
| 30 |
#' q <- qenv() |
|
| 31 |
#' q <- within(q, i <- iris) |
|
| 32 |
#' within(q, print(dim(subset(i, Species == "virginica")))) |
|
| 33 |
#' within(q, print(dim(subset(i, Species == species)))) # fails |
|
| 34 |
#' within(q, print(dim(subset(i, Species == species))), species = "versicolor") |
|
| 35 |
#' species_external <- "versicolor" |
|
| 36 |
#' within(q, print(dim(subset(i, Species == species))), species = species_external) |
|
| 37 |
#' |
|
| 38 |
#' # pass language objects |
|
| 39 |
#' expr <- expression(i <- iris, m <- mtcars) |
|
| 40 |
#' within(q, expr) # fails |
|
| 41 |
#' do.call(within, list(q, expr)) |
|
| 42 |
#' |
|
| 43 |
#' exprlist <- list(expression(i <- iris), expression(m <- mtcars)) |
|
| 44 |
#' within(q, exprlist) # fails |
|
| 45 |
#' do.call(within, list(q, do.call(c, exprlist))) |
|
| 46 |
#' |
|
| 47 |
#' @export |
|
| 48 |
#' |
|
| 49 |
within.qenv <- function(data, expr, ...) {
|
|
| 50 | 51x |
expr <- as.expression(substitute(expr)) |
| 51 | 51x |
extras <- list(...) |
| 52 | ||
| 53 |
# Inject extra values into expressions. |
|
| 54 | 51x |
calls <- lapply(expr, function(x) do.call(substitute, list(x, env = extras))) |
| 55 | 51x |
do.call( |
| 56 | 51x |
eval_code, |
| 57 | 51x |
utils::modifyList(extras, list(object = data, code = as.expression(calls))) |
| 58 |
) |
|
| 59 |
} |
|
| 60 | ||
| 61 | ||
| 62 |
#' @keywords internal |
|
| 63 |
#' |
|
| 64 |
#' @export |
|
| 65 |
within.qenv.error <- function(data, expr, ...) {
|
|
| 66 | 1x |
data |
| 67 |
} |
| 1 |
#' @export |
|
| 2 | ! |
length.qenv <- function(x) length(x@.xData) |
| 3 | ||
| 4 |
#' @export |
|
| 5 | 20x |
length.qenv.error <- function(x) 0 |
| 1 |
#' Instantiates a `qenv` environment |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' `r badge("stable")`
|
|
| 5 |
#' |
|
| 6 |
#' Instantiates a `qenv` environment. |
|
| 7 |
#' |
|
| 8 |
#' @details |
|
| 9 |
#' `qenv` class has following characteristics: |
|
| 10 |
#' |
|
| 11 |
#' - It inherits from the environment and methods such as [`$`], [get()], [ls()], [as.list()], |
|
| 12 |
#' [parent.env()] work out of the box. |
|
| 13 |
#' - `qenv` is a locked environment, and data modification is only possible through the [eval_code()] |
|
| 14 |
#' and [within.qenv()] functions. |
|
| 15 |
#' - It stores metadata about the code used to create the data (see [get_code()]). |
|
| 16 |
#' - It supports slicing (see [`subset-qenv`]) |
|
| 17 |
#' - It is immutable which means that each code evaluation does not modify the original `qenv` |
|
| 18 |
#' environment directly. See the following code: |
|
| 19 |
#' |
|
| 20 |
#' ``` |
|
| 21 |
#' q1 <- qenv() |
|
| 22 |
#' q2 <- eval_code(q1, "a <- 1") |
|
| 23 |
#' identical(q1, q2) # FALSE |
|
| 24 |
#' ``` |
|
| 25 |
#' |
|
| 26 |
#' @name qenv |
|
| 27 |
#' |
|
| 28 |
#' @return `qenv` environment. |
|
| 29 |
#' |
|
| 30 |
#' @seealso [eval_code()], [get_var()], [`subset-qenv`], [get_env()],[get_warnings()], [join()], [concat()] |
|
| 31 |
#' @examples |
|
| 32 |
#' q <- qenv() |
|
| 33 |
#' q2 <- within(q, a <- 1) |
|
| 34 |
#' ls(q2) |
|
| 35 |
#' q2$a |
|
| 36 |
#' @export |
|
| 37 |
qenv <- function() {
|
|
| 38 | 192x |
methods::new("qenv")
|
| 39 |
} |
| 1 |
#' Get warnings from `qenv` object |
|
| 2 |
#' |
|
| 3 |
#' Retrieve all warnings raised during code evaluation in a `qenv`. |
|
| 4 |
#' |
|
| 5 |
#' @param object (`qenv`) |
|
| 6 |
#' |
|
| 7 |
#' @return `character` containing warning information or `NULL` if no warnings. |
|
| 8 |
#' |
|
| 9 |
#' @examples |
|
| 10 |
#' data_q <- qenv() |
|
| 11 |
#' data_q <- eval_code(data_q, "iris_data <- iris") |
|
| 12 |
#' warning_qenv <- eval_code( |
|
| 13 |
#' data_q, |
|
| 14 |
#' bquote(p <- hist(iris_data[, .("Sepal.Length")], ff = ""))
|
|
| 15 |
#' ) |
|
| 16 |
#' cat(get_warnings(warning_qenv)) |
|
| 17 |
#' |
|
| 18 |
#' @name get_warnings |
|
| 19 |
#' @rdname get_warnings |
|
| 20 |
#' @aliases get_warnings,qenv-method |
|
| 21 |
#' @aliases get_warnings,qenv.error-method |
|
| 22 |
#' @aliases get_warnings,NULL-method |
|
| 23 |
#' |
|
| 24 |
#' @export |
|
| 25 |
setGeneric("get_warnings", function(object) {
|
|
| 26 | 9x |
dev_suppress(object) |
| 27 | 9x |
standardGeneric("get_warnings")
|
| 28 |
}) |
|
| 29 | ||
| 30 |
setMethod("get_warnings", signature = "qenv", function(object) {
|
|
| 31 | 7x |
get_warn_message_util(object, "warning") |
| 32 |
}) |
|
| 33 | ||
| 34 |
setMethod("get_warnings", signature = "qenv.error", function(object) {
|
|
| 35 | 1x |
NULL |
| 36 |
}) |
|
| 37 | ||
| 38 |
setMethod("get_warnings", "NULL", function(object) {
|
|
| 39 | 1x |
NULL |
| 40 |
}) |
| 1 |
#' Get outputs |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 4 |
#' |
|
| 5 |
#' `eval_code` evaluates code silently so plots and prints don't show up in the console or graphic devices. |
|
| 6 |
#' If one wants to use an output outside of the `qenv` (e.g. use a graph in `renderPlot`) then use `get_outputs`. |
|
| 7 |
#' @param object (`qenv`) |
|
| 8 |
#' @return list of outputs generated in a `qenv`` |
|
| 9 |
#' @examples |
|
| 10 |
#' q <- eval_code( |
|
| 11 |
#' qenv(), |
|
| 12 |
#' quote({
|
|
| 13 |
#' a <- 1 |
|
| 14 |
#' print("I'm an output")
|
|
| 15 |
#' plot(1) |
|
| 16 |
#' }) |
|
| 17 |
#' ) |
|
| 18 |
#' get_outputs(q) |
|
| 19 |
#' |
|
| 20 |
#' @aliases get_outputs,qenv-method |
|
| 21 |
#' |
|
| 22 |
#' @export |
|
| 23 | 17x |
setGeneric("get_outputs", function(object) standardGeneric("get_outputs"))
|
| 24 | ||
| 25 |
setMethod("get_outputs", signature = "qenv", function(object) {
|
|
| 26 | 17x |
Reduce( |
| 27 | 17x |
function(x, y) c(x, attr(y, "outputs")), |
| 28 | 17x |
init = list(), |
| 29 | 17x |
x = object@code |
| 30 |
) |
|
| 31 |
}) |