| 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 | 98x | checkmate::assert_list(code, "character") |
| 30 | 98x | checkmate::assert_character(names, any.missing = FALSE) |
| 31 | ||
| 32 | 98x | graph <- lapply(code, attr, "dependency") |
| 33 | ||
| 34 | 98x | if (check_code_names) { |
| 35 | 97x | symbols <- unlist(lapply(graph, function(call) { |
| 36 | 267x | ind <- match("<-", call, nomatch = length(call) + 1L) |
| 37 | 267x | call[seq_len(ind - 1L)] |
| 38 | })) | |
| 39 | ||
| 40 | 97x | 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 | 98x | if (length(code) == 0) { |
| 46 | 1x | return(code) |
| 47 | } | |
| 48 | ||
| 49 | 97x | ind <- unlist(lapply(names, function(x) graph_parser(x, graph))) |
| 50 | ||
| 51 | 97x | lib_ind <- detect_libraries(graph) |
| 52 | ||
| 53 | 97x | code_ids <- sort(unique(c(lib_ind, ind))) |
| 54 | 97x | 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 | 825x | checkmate::check_data_frame(call_pd) |
| 74 | 825x | checkmate::check_names(call_pd, must.include = c("token", "text")) |
| 75 | 825x | checkmate::check_string(text) |
| 76 | ||
| 77 | 825x | ans <- which(call_pd$token == "SYMBOL_FUNCTION_CALL" & call_pd$text == text) |
| 78 | 825x | if (length(ans)) { |
| 79 | 11x | ans |
| 80 | } else { | |
| 81 | 814x | 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 | 1074x | calls <- lapply( |
| 98 | 1074x | pd[pd$parent == 0 & (pd$token != "COMMENT" | grepl("@linksto", pd$text, fixed = TRUE)), "id"], |
| 99 | 1074x | function(parent) { |
| 100 | 1223x | rbind( |
| 101 | 1223x | pd[pd$id == parent, ], |
| 102 | 1223x | get_children(pd = pd, parent = parent) |
| 103 | ) | |
| 104 | } | |
| 105 | ) | |
| 106 | 1074x | calls <- Filter(function(call) !(nrow(call) == 1 && call$token == "';'"), calls) |
| 107 | 1074x | calls <- Filter(Negate(is.null), calls) |
| 108 | 1074x | calls <- fix_shifted_comments(calls) |
| 109 | 1074x | calls <- remove_custom_assign(calls, c(":=")) |
| 110 | 1074x | fix_arrows(calls) |
| 111 | } | |
| 112 | ||
| 113 | #' @keywords internal | |
| 114 | #' @noRd | |
| 115 | get_children <- function(pd, parent) { | |
| 116 | 12964x | idx_children <- abs(pd$parent) == parent |
| 117 | 12964x | children <- pd[idx_children, ] |
| 118 | 12964x | if (nrow(children) == 0) { |
| 119 | 7401x | return(NULL) |
| 120 | } | |
| 121 | ||
| 122 | 5563x | if (parent > 0) { |
| 123 | 5563x | 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 | 1074x | if (length(calls) >= 2) { |
| 134 | 107x | for (i in 2:length(calls)) { |
| 135 | 186x | comment_idx <- grep("@linksto", calls[[i]][, "text"]) |
| 136 | 186x | 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 | 1074x | 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 | 1074x | checkmate::assert_list(calls) |
| 155 | 1074x | checkmate::assert_character(exclude, null.ok = TRUE) |
| 156 | 1074x | lapply(calls, function(call) { |
| 157 | 1216x | if (!is.null(exclude)) { |
| 158 | 1216x | 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 | 1074x | checkmate::assert_list(calls) |
| 171 | 1074x | lapply(calls, function(call) { |
| 172 | 1216x | sym_fun <- call$token == "SYMBOL_FUNCTION_CALL" |
| 173 | 1216x | call[sym_fun, ] <- sub_arrows(call[sym_fun, ]) |
| 174 | 1216x | call |
| 175 | }) | |
| 176 | } | |
| 177 | ||
| 178 | #' Execution of assignment operator substitutions for a call. | |
| 179 | #' @keywords internal | |
| 180 | #' @noRd | |
| 181 | sub_arrows <- function(call) { | |
| 182 | 1216x | checkmate::assert_data_frame(call) |
| 183 | 1216x | map <- data.frame( |
| 184 | 1216x | row.names = c("<-", "<<-", "="), |
| 185 | 1216x | token = rep("LEFT_ASSIGN", 3), |
| 186 | 1216x | text = rep("<-", 3) |
| 187 | ) | |
| 188 | 1216x | sub_ids <- call$text %in% rownames(map) |
| 189 | 1216x | call[sub_ids, c("token", "text")] <- map[call$text[sub_ids], ] |
| 190 | 1216x | 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 | 414x | 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 | 403x | function_id <- x[x$token == "FUNCTION", "parent"] |
| 218 | 403x | if (length(function_id)) { |
| 219 | 20x | x$id %in% get_children(x, function_id[1])$id |
| 220 | } else { | |
| 221 | 383x | rep(FALSE, nrow(x)) |
| 222 | } | |
| 223 | } | |
| 224 | 414x | in_parenthesis <- function(x) { |
| 225 | 315x | if (any(x$token %in% c("LBB", "'['"))) { |
| 226 | 22x | id_start <- min(x$id[x$token %in% c("LBB", "'['")]) |
| 227 | 22x | id_end <- min(x$id[x$token == "']'"]) |
| 228 | 22x | 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 | 414x | data_call <- find_call(pd, "data") |
| 234 | 414x | if (data_call) { |
| 235 | 3x | sym <- pd[data_call + 1, "text"] |
| 236 | 3x | return(c(gsub("^['\"]|['\"]$", "", sym), "<-")) |
| 237 | } | |
| 238 | # Handle assign(x = ). | |
| 239 | 411x | assign_call <- find_call(pd, "assign") |
| 240 | 411x | 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 | 403x | x <- pd[!is_in_function(pd), ] |
| 276 | 403x | sym_cond <- which(x$token %in% c("SPECIAL", "SYMBOL", "SYMBOL_FUNCTION_CALL")) |
| 277 | 403x | sym_fc_cond <- which(x$token == "SYMBOL_FUNCTION_CALL") |
| 278 | ||
| 279 | 403x | 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 | 386x | dollar_ids <- x[x$token %in% c("'$'", "'@'"), "id"] |
| 285 | 386x | if (length(dollar_ids)) { |
| 286 | 13x | object_ids <- x[sym_cond, "id"] |
| 287 | 13x | after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids] |
| 288 | 13x | sym_cond <- setdiff(sym_cond, which(x$id %in% after_dollar)) |
| 289 | } | |
| 290 | ||
| 291 | 386x | assign_cond <- grep("ASSIGN", x$token) |
| 292 | 386x | if (!length(assign_cond)) { |
| 293 | 71x | return(c("<-", unique(x[sym_cond, "text"]))) |
| 294 | } | |
| 295 | ||
| 296 | # For cases like 'eval(expression(c <- b + 2))' removes 'eval(expression('. | |
| 297 | 315x | 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 | 315x | 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 | 315x | after <- match(min(x$id[assign_cond]), sort(x$id[c(min(assign_cond), sym_cond)])) - 1 |
| 305 | 315x | ans <- append(x[sym_cond, "text"], "<-", after = max(1, after)) |
| 306 | 315x | ans <- move_functions_after_arrow(ans, unique(x[sym_fc_cond, "text"])) |
| 307 | 315x | roll <- in_parenthesis(pd) |
| 308 | 315x | if (length(roll)) { |
| 309 | # detect elements appeared in parenthesis and move them on RHS | |
| 310 | # but only their first appearance | |
| 311 | # as the same object can appear as regular object and the one used in parenthesis | |
| 312 | 8x | result <- ans |
| 313 | 8x | for (elem in roll) { |
| 314 | 11x | idx <- which(result == elem)[1] |
| 315 | 11x | if (!is.na(idx)) { |
| 316 | 9x | result <- result[-idx] |
| 317 | } | |
| 318 | } | |
| 319 | 8x | c(result, roll) |
| 320 | } else { | |
| 321 | 307x | ans |
| 322 | } | |
| 323 | } | |
| 324 | ||
| 325 | #' Moves function names to the right side of dependency graph | |
| 326 | #' | |
| 327 | #' Changes status of the function call from dependent to dependency if occurs in the lhs. | |
| 328 | #' Technically, it means to move function names after the dependency operator. | |
| 329 | #' For example, for `attributes(a) <- b` the dependency graph should look like `c("a", "<-", "b", "attributes")`. | |
| 330 | #' | |
| 331 | #' @param ans `character` vector of object names in dependency graph. | |
| 332 | #' @param functions `character` vector of function names. | |
| 333 | #' | |
| 334 | #' @return | |
| 335 | #' A character vector. | |
| 336 | #' @keywords internal | |
| 337 | #' @noRd | |
| 338 | move_functions_after_arrow <- function(ans, functions) { | |
| 339 | 315x | arrow_pos <- which(ans == "<-") |
| 340 | 315x | if (length(arrow_pos) == 0) { |
| 341 | ! | return(ans) |
| 342 | } | |
| 343 | 315x | if (length(functions) == 0) { |
| 344 | 253x | return(ans) |
| 345 | } | |
| 346 | 62x | ans_pre <- ans[1:arrow_pos] |
| 347 | # it's setdiff but without the removal of duplicates | |
| 348 | # do not use setdiff(ans_pre, functions) | |
| 349 | # as it removes duplicates from ans_pre even if they do not appear in functions | |
| 350 | # check setdiff(c("A", "A"), "B") - gives "A", where we want to keep c("A", "A") | |
| 351 | 62x | for (fun in functions) { |
| 352 | 9x | if (any(ans_pre == fun)) ans_pre <- ans_pre[-match(fun, ans_pre)] |
| 353 | } | |
| 354 | 62x | after_arrow <- if (arrow_pos < length(ans)) { |
| 355 | 60x | ans[(arrow_pos + 1):length(ans)] |
| 356 | } | |
| 357 | 62x | c(ans_pre, after_arrow) |
| 358 | } | |
| 359 | ||
| 360 | #' Extract side effects | |
| 361 | #' | |
| 362 | #' Extracts all object names from the code that are marked with `@linksto` tag. | |
| 363 | #' | |
| 364 | #' The code may contain functions calls that create side effects, e.g. modify the environment. | |
| 365 | #' Static code analysis may be insufficient to determine which objects are created or modified by such a function call. | |
| 366 | #' The `@linksto` comment tag is introduced to mark a call as having a (side) effect on one or more objects. | |
| 367 | #' With this tag a complete object dependency structure can be established. | |
| 368 | #' Read more about side effects and the usage of `@linksto` tag in [`get_code_dependencies()`] function. | |
| 369 | #' | |
| 370 | #' @param pd `data.frame`; | |
| 371 | #' one of the results of `utils::getParseData()` split into subsets representing individual calls; | |
| 372 | #' created by `extract_calls()` function | |
| 373 | #' | |
| 374 | #' @return | |
| 375 | #' A character vector of names of objects | |
| 376 | #' depending a call tagged with `@linksto` in a corresponding element of `pd`. | |
| 377 | #' | |
| 378 | #' @keywords internal | |
| 379 | #' @noRd | |
| 380 | extract_side_effects <- function(pd) { | |
| 381 | 401x | linksto <- grep("@linksto", pd[pd$token == "COMMENT", "text"], value = TRUE) |
| 382 | 401x | unlist(strsplit(sub("\\s*#.*@linksto\\s+", "", linksto), "\\s+")) |
| 383 | } | |
| 384 | ||
| 385 | #' @param parsed_code results of `parse(text = code, keep.source = TRUE` (parsed text) | |
| 386 | #' @keywords internal | |
| 387 | #' @noRd | |
| 388 | extract_dependency <- function(parsed_code) { | |
| 389 | 437x | full_pd <- normalize_pd(utils::getParseData(parsed_code)) |
| 390 | 437x | reordered_full_pd <- extract_calls(full_pd) |
| 391 | ||
| 392 | # Early return on empty code | |
| 393 | 437x | if (length(reordered_full_pd) == 0L) { |
| 394 | 36x | return(NULL) |
| 395 | } | |
| 396 | ||
| 397 | 401x | if (length(parsed_code) == 0L) { |
| 398 | 1x | return(extract_side_effects(reordered_full_pd[[1]])) |
| 399 | } | |
| 400 | 400x | expr_ix <- lapply(parsed_code[[1]], class) == "{" |
| 401 | ||
| 402 | # Build queue of expressions to parse individually | |
| 403 | 400x | queue <- list() |
| 404 | 400x | parsed_code_list <- if (all(!expr_ix)) { |
| 405 | 392x | list(parsed_code) |
| 406 | } else { | |
| 407 | 8x | queue <- as.list(parsed_code[[1]][expr_ix]) |
| 408 | 8x | new_list <- parsed_code[[1]] |
| 409 | 8x | new_list[expr_ix] <- NULL |
| 410 | 8x | list(parse(text = as.expression(new_list), keep.source = TRUE)) |
| 411 | } | |
| 412 | ||
| 413 | 400x | while (length(queue) > 0) { |
| 414 | 22x | current <- queue[[1]] |
| 415 | 22x | queue <- queue[-1] |
| 416 | 22x | if (identical(current[[1L]], as.name("{"))) { |
| 417 | 8x | queue <- append(queue, as.list(current)[-1L]) |
| 418 | } else { | |
| 419 | 14x | parsed_code_list[[length(parsed_code_list) + 1]] <- parse(text = as.expression(current), keep.source = TRUE) |
| 420 | } | |
| 421 | } | |
| 422 | ||
| 423 | 400x | parsed_occurences <- lapply( |
| 424 | 400x | parsed_code_list, |
| 425 | 400x | function(parsed_code) { |
| 426 | 414x | pd <- normalize_pd(utils::getParseData(parsed_code)) |
| 427 | 414x | reordered_pd <- extract_calls(pd) |
| 428 | 414x | if (length(reordered_pd) > 0) { |
| 429 | # extract_calls is needed to reorder the pd so that assignment operator comes before symbol names | |
| 430 | # extract_calls is needed also to substitute assignment operators into specific format with fix_arrows | |
| 431 | # extract_calls is needed to omit empty calls that contain only one token `"';'"` | |
| 432 | # This cleaning is needed as extract_occurrence assumes arrows are fixed, and order is different | |
| 433 | # than in original pd | |
| 434 | 414x | extract_occurrence(reordered_pd[[1]]) |
| 435 | } | |
| 436 | } | |
| 437 | ) | |
| 438 | ||
| 439 | # Merge results together | |
| 440 | 400x | result <- Reduce( |
| 441 | 400x | function(u, v) { |
| 442 | 414x | ix <- if ("<-" %in% v) min(which(v == "<-")) else 0 |
| 443 | 414x | u$left_side <- c(u$left_side, v[seq_len(max(0, ix - 1))]) |
| 444 | 414x | u$right_side <- c( |
| 445 | 414x | u$right_side, |
| 446 | 414x | if (ix == length(v)) character(0L) else v[seq(ix + 1, max(ix + 1, length(v)))] |
| 447 | ) | |
| 448 | 414x | u |
| 449 | }, | |
| 450 | 400x | init = list(left_side = character(0L), right_side = character(0L)), |
| 451 | 400x | x = parsed_occurences |
| 452 | ) | |
| 453 | ||
| 454 | 400x | c(extract_side_effects(reordered_full_pd[[1]]), result$left_side, "<-", result$right_side) |
| 455 | } | |
| 456 | ||
| 457 | # graph_parser ---- | |
| 458 | ||
| 459 | #' Return the indices of calls needed to reproduce an object | |
| 460 | #' | |
| 461 | #' @param x The name of the object to return code for. | |
| 462 | #' @param graph A result of `code_graph()`. | |
| 463 | #' | |
| 464 | #' @return | |
| 465 | #' Integer vector of indices that can be applied to `graph` to obtain all calls required to reproduce object `x`. | |
| 466 | #' | |
| 467 | #' @keywords internal | |
| 468 | #' @noRd | |
| 469 | graph_parser <- function(x, graph) { | |
| 470 | # x occurrences (lhs) | |
| 471 | 398x | occurrence <- vapply( |
| 472 | 398x | graph, function(call) { |
| 473 | 667x | ind <- match("<-", call, nomatch = length(call) + 1L) |
| 474 | 667x | x %in% call[seq_len(ind - 1L)] |
| 475 | }, | |
| 476 | 398x | logical(1) |
| 477 | ) | |
| 478 | ||
| 479 | # x-dependent objects (rhs) | |
| 480 | 398x | dependencies <- lapply(graph[occurrence], function(call) { |
| 481 | 184x | ind <- match("<-", call, nomatch = 0L) |
| 482 | 184x | call[(ind + 1L):length(call)] |
| 483 | }) | |
| 484 | 398x | dependencies <- setdiff(unlist(dependencies), x) |
| 485 | ||
| 486 | 398x | dependency_occurrences <- lapply(dependencies, function(dependency) { |
| 487 | # track down dependencies and where they occur on the lhs in previous calls | |
| 488 | 286x | last_x_occurrence <- max(which(occurrence)) |
| 489 | 286x | reduced_graph <- utils::head(graph[seq_len(last_x_occurrence)], -1) |
| 490 | 286x | c(graph_parser(dependency, reduced_graph), last_x_occurrence) |
| 491 | }) | |
| 492 | ||
| 493 | 398x | sort(unique(c(which(occurrence), unlist(dependency_occurrences)))) |
| 494 | } | |
| 495 | ||
| 496 | ||
| 497 | # default_side_effects -------------------------------------------------------------------------------------------- | |
| 498 | ||
| 499 | #' Detect library calls | |
| 500 | #' | |
| 501 | #' Detects `library()` and `require()` function calls. | |
| 502 | #' | |
| 503 | #' @param `graph` the dependency graph, result of `lapply(code, attr, "dependency")` | |
| 504 | #' | |
| 505 | #' @return | |
| 506 | #' Integer vector of indices that can be applied to `graph` to obtain all calls containing | |
| 507 | #' `library()` or `require()` calls that are always returned for reproducibility. | |
| 508 | #' | |
| 509 | #' @keywords internal | |
| 510 | #' @noRd | |
| 511 | detect_libraries <- function(graph) { | |
| 512 | 97x | defaults <- c("library", "require") |
| 513 | ||
| 514 | 97x | which( |
| 515 | 97x | unlist( |
| 516 | 97x | lapply( |
| 517 | 97x | graph, function(x) { |
| 518 | 268x | any(grepl(pattern = paste(defaults, collapse = "|"), x = x)) |
| 519 | } | |
| 520 | ) | |
| 521 | ) | |
| 522 | ) | |
| 523 | } | |
| 524 | ||
| 525 | ||
| 526 | # utils ----------------------------------------------------------------------------------------------------------- | |
| 527 | ||
| 528 | ||
| 529 | #' Normalize parsed data removing backticks from symbols | |
| 530 | #' | |
| 531 | #' @param pd `data.frame` resulting from `utils::getParseData()` call. | |
| 532 | #' | |
| 533 | #' @return `data.frame` with backticks removed from `text` column for `SYMBOL` tokens. | |
| 534 | #' | |
| 535 | #' @keywords internal | |
| 536 | #' @noRd | |
| 537 | normalize_pd <- function(pd) { | |
| 538 | # Remove backticks from SYMBOL tokens | |
| 539 | 1074x | symbol_index <- grepl("^SYMBOL.*$", pd$token) |
| 540 | 1074x | pd[symbol_index, "text"] <- gsub("^`(.*)`$", "\\1", pd[symbol_index, "text"]) |
| 541 | ||
| 542 | 1074x | pd |
| 543 | } | |
| 544 | ||
| 545 | ||
| 546 | # split_code ------------------------------------------------------------------------------------------------------ | |
| 547 | ||
| 548 | ||
| 549 | #' Get line/column in the source where the calls end | |
| 550 | #' | |
| 551 | #' | |
| 552 | #' @param code `character(1)` | |
| 553 | #' | |
| 554 | #' @return `matrix` with `colnames = c("line", "col")` | |
| 555 | #' | |
| 556 | #' @keywords internal | |
| 557 | #' @noRd | |
| 558 | get_call_breaks <- function(code) { | |
| 559 | 223x | parsed_code <- parse(text = code, keep.source = TRUE) |
| 560 | 223x | pd <- utils::getParseData(parsed_code) |
| 561 | 223x | pd <- normalize_pd(pd) |
| 562 | 223x | pd <- pd[pd$token != "';'", ] |
| 563 | 223x | call_breaks <- t(sapply( |
| 564 | 223x | extract_calls(pd), |
| 565 | 223x | function(x) { |
| 566 | 401x | matrix(c(max(x$line2), max(x$col2[x$line2 == max(x$line2)]))) |
| 567 | } | |
| 568 | )) | |
| 569 | 223x | call_breaks <- call_breaks[-nrow(call_breaks), , drop = FALSE] # breaks in between needed only |
| 570 | 223x | if (nrow(call_breaks) == 0L) { |
| 571 | 122x | call_breaks <- matrix(numeric(0), ncol = 2) |
| 572 | } | |
| 573 | 223x | colnames(call_breaks) <- c("line", "col") |
| 574 | 223x | call_breaks |
| 575 | } | |
| 576 | ||
| 577 | #' Split code by calls | |
| 578 | #' | |
| 579 | #' @param code `character` with the code. | |
| 580 | #' | |
| 581 | #' @return list of `character`s of the length equal to the number of calls in `code`. | |
| 582 | #' | |
| 583 | #' @keywords internal | |
| 584 | #' @noRd | |
| 585 | split_code <- function(code) { | |
| 586 | 223x | call_breaks <- get_call_breaks(code) |
| 587 | 223x | if (nrow(call_breaks) == 0) { |
| 588 | 122x | return(code) |
| 589 | } | |
| 590 | 101x | call_breaks <- call_breaks[order(call_breaks[, "line"], call_breaks[, "col"]), , drop = FALSE] |
| 591 | 101x | code_split <- strsplit(code, split = "\n", fixed = TRUE)[[1]] |
| 592 | 101x | char_count_lines <- c(0, cumsum(sapply(code_split, nchar, USE.NAMES = FALSE) + 1), -1)[seq_along(code_split)] |
| 593 | ||
| 594 | 101x | idx_start <- c( |
| 595 | 101x | 0, # first call starts in the beginning of src |
| 596 | 101x | char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"] + 1 |
| 597 | ) | |
| 598 | 101x | idx_end <- c( |
| 599 | 101x | char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"], |
| 600 | 101x | nchar(code) # last call end in the end of src |
| 601 | ) | |
| 602 | 101x | new_code <- substring(code, idx_start, idx_end) |
| 603 | ||
| 604 | # line split happens before call terminator (it could be `;` or `\n`) and the terminator goes to the next line | |
| 605 | # we need to move remove leading and add \n instead when combining calls | |
| 606 | 101x | c(new_code[1], gsub("^[\t ]*(\n|;)", "", new_code[-1])) |
| 607 | } |
| 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 | 151x | grDevices::pdf(nullfile()) |
| 21 | 151x | on.exit(grDevices::dev.off()) |
| 22 | 151x | 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 | 293x | if (is.atomic(x) || is.symbol(x)) { |
| 44 | 9x | return(list(x)) |
| 45 | } | |
| 46 | 284x | if (is.call(x)) { |
| 47 | 187x | if (identical(as.list(x)[[1L]], as.symbol("{"))) { |
| 48 | 58x | as.list(x)[-1L] |
| 49 | } else { | |
| 50 | 129x | list(x) |
| 51 | } | |
| 52 | } else { | |
| 53 | 97x | 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 | #' 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 | 131x | dev_suppress(object) |
| 101 | 131x | standardGeneric("get_code") |
| 102 | }) | |
| 103 | ||
| 104 | setMethod("get_code", signature = "qenv", function(object, deparse = TRUE, names = NULL, ...) { | |
| 105 | 129x | checkmate::assert_flag(deparse) |
| 106 | 129x | checkmate::assert_character(names, min.len = 1L, null.ok = TRUE) |
| 107 | ||
| 108 | # Normalize in case special it is backticked | |
| 109 | 129x | if (!is.null(names)) { |
| 110 | 86x | names <- gsub("^`(.*)`$", "\\1", names) |
| 111 | } | |
| 112 | ||
| 113 | 129x | code <- if (!is.null(names)) { |
| 114 | 86x | get_code_dependency(object@code, names, ...) |
| 115 | } else { | |
| 116 | 43x | object@code |
| 117 | } | |
| 118 | ||
| 119 | 129x | if (deparse) { |
| 120 | 127x | 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 | #' 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 | #' 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 | 227x | setGeneric("eval_code", function(object, code, ...) standardGeneric("eval_code")) |
| 30 | ||
| 31 | setMethod("eval_code", signature = c(object = "qenv"), function(object, code, ...) { | |
| 32 | 227x | if (!is.language(code) && !is.character(code)) { |
| 33 | 3x | stop("eval_code accepts code being language or character") |
| 34 | } | |
| 35 | 224x | code <- .preprocess_code(code) |
| 36 | # preprocess code to ensure it is a character vector | |
| 37 | 224x | .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 | 224x | if (identical(trimws(code), "") || length(code) == 0) { |
| 45 | 2x | return(object) |
| 46 | } | |
| 47 | 222x | code <- paste(split_code(code), collapse = "\n") |
| 48 | ||
| 49 | 222x | object@.xData <- rlang::env_clone(object@.xData, parent = parent.env(object@.xData)) |
| 50 | 222x | parsed_code <- parse(text = code, keep.source = TRUE) |
| 51 | ||
| 52 | 222x | old <- evaluate::inject_funs( |
| 53 | 222x | library = function(...) { |
| 54 | 4x | x <- library(...) |
| 55 | 4x | if (!identical(parent.env(object@.xData), parent.env(.GlobalEnv))) { |
| 56 | 2x | parent.env(object@.xData) <- parent.env(.GlobalEnv) |
| 57 | } | |
| 58 | 4x | invisible(x) |
| 59 | } | |
| 60 | ) | |
| 61 | 222x | out <- evaluate::evaluate( |
| 62 | 222x | code, |
| 63 | 222x | envir = object@.xData, |
| 64 | 222x | stop_on_error = 1, |
| 65 | 222x | output_handler = evaluate::new_output_handler(value = identity) |
| 66 | ) | |
| 67 | 222x | out <- evaluate::trim_intermediate_plots(out) |
| 68 | ||
| 69 | 222x | evaluate::inject_funs(old) # remove library() override |
| 70 | ||
| 71 | 222x | new_code <- list() |
| 72 | 222x | for (this in out) { |
| 73 | 507x | if (inherits(this, "source")) { |
| 74 | 431x | this_code <- gsub("\n$", "", this$src) |
| 75 | 431x | attr(this_code, "dependency") <- extract_dependency(parse(text = this_code, keep.source = TRUE)) |
| 76 | 431x | new_code <- c(new_code, stats::setNames(list(this_code), sample.int(.Machine$integer.max, size = 1))) |
| 77 | } else { | |
| 78 | 76x | last_code <- new_code[[length(new_code)]] |
| 79 | 76x | 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 | 62x | attr(last_code, "outputs") <- c(attr(last_code, "outputs"), list(this)) |
| 93 | 62x | new_code[[length(new_code)]] <- last_code |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | 208x | object@code <- c(object@code, new_code) |
| 98 | 208x | lockEnvironment(object@.xData, bindings = TRUE) |
| 99 | 208x | object |
| 100 | } | |
| 101 | ||
| 102 | 224x | 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 | 153x | if (is.expression(code) && length(attr(code, "wholeSrcref"))) { |
| 106 | 2x | paste(attr(code, "wholeSrcref"), collapse = "\n") |
| 107 | } else { | |
| 108 | 151x | paste( |
| 109 | 151x | vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)), |
| 110 | 151x | collapse = "\n" |
| 111 | ) | |
| 112 | } | |
| 113 | }) |
| 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 | 208x | parent <- parent.env(.GlobalEnv) |
| 35 | 208x | new_xdata <- if (rlang::is_missing(.xData)) { |
| 36 | 206x | new.env(parent = parent) |
| 37 | } else { | |
| 38 | 2x | checkmate::assert_environment(.xData) |
| 39 | 1x | rlang::env_clone(.xData, parent = parent) |
| 40 | } | |
| 41 | 207x | 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 | 207x | methods::callNextMethod( |
| 47 | 207x | .Object, |
| 48 | 207x | new_xdata, # Mandatory use of unnamed environment arg |
| 49 | 207x | 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 | 224x | dots <- rlang::list2(...) |
| 72 | 224x | if (!checkmate::test_list(dots[-1], types = c("qenv", "qenv.error"))) { |
| 73 | 207x | 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 | #' @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 | 205x | 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 | #' 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 | #' Functions that trigger side effects like `options` or `set.seed` can be linked to specific objects for further code retrieval (with `get_code`), but only through `eval_code` where code input as `character`. `within` works on `expressions` that do not preserve comments, hence you can not use `# @linksto` tag explained in `get_code`. | |
| 7 | #' @alias within | |
| 8 | #' @section Using language objects with `within`: | |
| 9 | #' Passing language objects to `expr` is generally not intended but can be achieved with `do.call`. | |
| 10 | #' Only single `expression`s will work and substitution is not available. See examples. | |
| 11 | #' | |
| 12 | #' @param data (`qenv`) | |
| 13 | #' @param expr (`expression`) to evaluate. Must be inline code, see `Using language objects...` | |
| 14 | #' @param ... named argument value will substitute a symbol in the `expr` matched by the name. | |
| 15 | #' For practical usage see Examples section below. | |
| 16 | #' | |
| 17 | #' @examples | |
| 18 | #' # evaluate code using within | |
| 19 | #' q <- qenv() | |
| 20 | #' q <- within(q, { | |
| 21 | #' i <- iris | |
| 22 | #' }) | |
| 23 | #' q <- within(q, { | |
| 24 | #' m <- mtcars | |
| 25 | #' f <- faithful | |
| 26 | #' }) | |
| 27 | #' q | |
| 28 | #' get_code(q) | |
| 29 | #' | |
| 30 | #' # inject values into code | |
| 31 | #' q <- qenv() | |
| 32 | #' q <- within(q, i <- iris) | |
| 33 | #' within(q, print(dim(subset(i, Species == "virginica")))) | |
| 34 | #' within(q, print(dim(subset(i, Species == species)))) # fails | |
| 35 | #' within(q, print(dim(subset(i, Species == species))), species = "versicolor") | |
| 36 | #' species_external <- "versicolor" | |
| 37 | #' within(q, print(dim(subset(i, Species == species))), species = species_external) | |
| 38 | #' | |
| 39 | #' # pass language objects | |
| 40 | #' expr <- expression(i <- iris, m <- mtcars) | |
| 41 | #' within(q, expr) # fails | |
| 42 | #' do.call(within, list(q, expr)) | |
| 43 | #' | |
| 44 | #' exprlist <- list(expression(i <- iris), expression(m <- mtcars)) | |
| 45 | #' within(q, exprlist) # fails | |
| 46 | #' do.call(within, list(q, do.call(c, exprlist))) | |
| 47 | #' | |
| 48 | #' @export | |
| 49 | #' | |
| 50 | within.qenv <- function(data, expr, ...) { | |
| 51 | 64x | expr <- as.expression(substitute(expr)) |
| 52 | 64x | extras <- list(...) |
| 53 | ||
| 54 | # Inject extra values into expressions. | |
| 55 | 64x | calls <- lapply(expr, function(x) do.call(substitute, list(x, env = extras))) |
| 56 | 64x | do.call( |
| 57 | 64x | eval_code, |
| 58 | 64x | utils::modifyList(extras, list(object = data, code = as.expression(calls))) |
| 59 | ) | |
| 60 | } | |
| 61 | ||
| 62 | ||
| 63 | #' @keywords internal | |
| 64 | #' | |
| 65 | #' @export | |
| 66 | within.qenv.error <- function(data, expr, ...) { | |
| 67 | 1x | data |
| 68 | } |
| 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 | 16x | setGeneric("get_outputs", function(object) standardGeneric("get_outputs")) |
| 24 | ||
| 25 | setMethod("get_outputs", signature = "qenv", function(object) { | |
| 26 | 16x | Reduce( |
| 27 | 16x | function(x, y) c(x, attr(y, "outputs")), |
| 28 | 16x | init = list(), |
| 29 | 16x | x = object@code |
| 30 | ) | |
| 31 | }) |