(original) (raw)
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 | #' Retrieve variables from the `qenv` environment. | |
9 | #' | |
10 | #' @param object,x (`qenv`) | |
11 | #' @param var,i (`character(1)`) variable name. | |
12 | #' | |
13 | #' @return The value of required variable (`var`) within `qenv` object. | |
14 | #' | |
15 | #' @examples | |
16 | #' q <- qenv() | |
17 | #' q1 <- eval_code(q, code = quote(a <- 1)) | |
18 | #' q2 <- eval_code(q1, code = "b <- a") | |
19 | #' get_var(q2, "b") | |
20 | #' | |
21 | #' @aliases get_var,qenv,character-method | |
22 | #' @aliases get_var,qenv.error,ANY-method | |
23 | #' | |
24 | #' @export | |
25 | setGeneric("get_var", function(object, var) { | |
26 | 5_x_ | dev_suppress(object) |
27 | 5_x_ | standardGeneric("get_var") |
28 | }) | |
29 | ||
30 | setMethod("get_var", signature = c("qenv", "character"), function(object, var) { | |
31 | 4_x_ | lifecycle::deprecate_soft("0.6.0", "get_var()", "base::get()") |
32 | 4_x_ | tryCatch( |
33 | 4_x_ | get(var, envir = object@.xData, inherits = FALSE), |
34 | 4_x_ | error = function(e) { |
35 | 3_x_ | message(conditionMessage(e)) |
36 | 3_x_ | NULL |
37 | } | |
38 | ) | |
39 | }) | |
40 | ||
41 | setMethod("get_var", signature = c("qenv.error", "ANY"), function(object, var) { | |
42 | 1_x_ | stop(errorCondition( |
43 | 1_x_ | list(message = conditionMessage(object)), |
44 | 1_x_ | class = c("validation", "try-error", "simpleError") |
45 | )) | |
46 | }) | |
47 | ||
48 | #' @rdname get_var | |
49 | #' @export | |
50 | `[[.qenv.error` <- function(x, i) { | |
51 | 1_x_ | stop(errorCondition( |
52 | 1_x_ | list(message = conditionMessage(x)), |
53 | 1_x_ | class = c("validation", "try-error", "simpleError") |
54 | )) | |
55 | } | |
56 | ||
57 | #' @export | |
58 | 4_x_ | names.qenv.error <- function(x) NULL |
59 | ||
60 | #' @export | |
61 | `$.qenv.error` <- function(x, name) { | |
62 | # Must allow access of elements in qenv.error object (message, call, trace, ...) | |
63 | # Otherwise, it will enter an infinite recursion with the `conditionMessage(x)` call. | |
64 | 9_x_ | if (exists(name, x)) { |
65 | 8_x_ | return(NextMethod("$", x)) |
66 | } | |
67 | ||
68 | 1_x_ | class(x) <- setdiff(class(x), "qenv.error") |
69 | 1_x_ | stop(errorCondition( |
70 | 1_x_ | list(message = conditionMessage(x)), |
71 | 1_x_ | class = c("validation", "try-error", "simpleError") |
72 | )) | |
73 | } |
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 | 75_x_ | checkmate::assert_list(code, "character") |
30 | 75_x_ | checkmate::assert_character(names, any.missing = FALSE) |
31 | ||
32 | 75_x_ | graph <- lapply(code, attr, "dependency") |
33 | ||
34 | 75_x_ | if (check_code_names) { |
35 | 74_x_ | symbols <- unlist(lapply(graph, function(call) { |
36 | 227_x_ | ind <- match("<-", call, nomatch = length(call) + 1L) |
37 | 227_x_ | call[seq_len(ind - 1L)] |
38 | })) | |
39 | ||
40 | 74_x_ | if (!all(names %in% unique(symbols))) { |
41 | 8_x_ | warning("Object(s) not found in code: ", toString(setdiff(names, symbols)), ".", call. = FALSE) |
42 | } | |
43 | } | |
44 | ||
45 | 75_x_ | if (length(code) == 0) { |
46 | 1_x_ | return(code) |
47 | } | |
48 | ||
49 | 74_x_ | ind <- unlist(lapply(names, function(x) graph_parser(x, graph))) |
50 | ||
51 | 74_x_ | lib_ind <- detect_libraries(graph) |
52 | ||
53 | 74_x_ | code_ids <- sort(unique(c(lib_ind, ind))) |
54 | 74_x_ | 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 | 643_x_ | checkmate::check_data_frame(call_pd) |
74 | 643_x_ | checkmate::check_names(call_pd, must.include = c("token", "text")) |
75 | 643_x_ | checkmate::check_string(text) |
76 | ||
77 | 643_x_ | ans <- which(call_pd$token == "SYMBOL_FUNCTION_CALL" & call_pd$text == text) |
78 | 643_x_ | if (length(ans)) { |
79 | 8_x_ | ans |
80 | } else { | |
81 | 635_x_ | 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 | 545_x_ | calls <- lapply( |
98 | 545_x_ | pd[pd$parent == 0 & (pd$token != "COMMENT" | grepl("@linksto", pd$text, fixed = TRUE)), "id"], |
99 | 545_x_ | function(parent) { |
100 | 662_x_ | rbind( |
101 | 662_x_ | pd[pd$id == parent, ], |
102 | 662_x_ | get_children(pd = pd, parent = parent) |
103 | ) | |
104 | } | |
105 | ) | |
106 | 545_x_ | calls <- Filter(function(call) !(nrow(call) == 1 && call$token == "';'"), calls) |
107 | 545_x_ | calls <- Filter(Negate(is.null), calls) |
108 | 545_x_ | calls <- fix_shifted_comments(calls) |
109 | 545_x_ | calls <- remove_custom_assign(calls, c(":=")) |
110 | 545_x_ | fix_arrows(calls) |
111 | } | |
112 | ||
113 | #' @keywords internal | |
114 | #' @noRd | |
115 | get_children <- function(pd, parent) { | |
116 | 6438_x_ | idx_children <- abs(pd$parent) == parent |
117 | 6438_x_ | children <- pd[idx_children, ] |
118 | 6438_x_ | if (nrow(children) == 0) { |
119 | 3647_x_ | return(NULL) |
120 | } | |
121 | ||
122 | 2791_x_ | if (parent > 0) { |
123 | 2791_x_ | 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 | 545_x_ | if (length(calls) >= 2) { |
134 | 55_x_ | for (i in 2:length(calls)) { |
135 | 118_x_ | comment_idx <- grep("@linksto", calls[[i]][, "text"]) |
136 | 118_x_ | if (isTRUE(comment_idx[1] <= 2)) { |
137 | 6_x_ | calls[[i - 1]] <- rbind( |
138 | 6_x_ | calls[[i - 1]], |
139 | 6_x_ | calls[[i]][comment_idx[1], ] |
140 | ) | |
141 | 6_x_ | calls[[i]] <- calls[[i]][-comment_idx[1], ] |
142 | } | |
143 | } | |
144 | } | |
145 | 545_x_ | 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 | 545_x_ | checkmate::assert_list(calls) |
155 | 545_x_ | checkmate::assert_character(exclude, null.ok = TRUE) |
156 | 545_x_ | lapply(calls, function(call) { |
157 | 658_x_ | if (!is.null(exclude)) { |
158 | 658_x_ | 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 | 545_x_ | checkmate::assert_list(calls) |
171 | 545_x_ | lapply(calls, function(call) { |
172 | 658_x_ | sym_fun <- call$token == "SYMBOL_FUNCTION_CALL" |
173 | 658_x_ | call[sym_fun, ] <- sub_arrows(call[sym_fun, ]) |
174 | 658_x_ | call |
175 | }) | |
176 | } | |
177 | ||
178 | #' Execution of assignment operator substitutions for a call. | |
179 | #' @keywords internal | |
180 | #' @noRd | |
181 | sub_arrows <- function(call) { | |
182 | 658_x_ | checkmate::assert_data_frame(call) |
183 | 658_x_ | map <- data.frame( |
184 | 658_x_ | row.names = c("<-", "<<-", "="), |
185 | 658_x_ | token = rep("LEFT_ASSIGN", 3), |
186 | 658_x_ | text = rep("<-", 3) |
187 | ) | |
188 | 658_x_ | sub_ids <- call$text %in% rownames(map) |
189 | 658_x_ | call[sub_ids, c("token", "text")] <- map[call$text[sub_ids], ] |
190 | 658_x_ | 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 | 323_x_ | 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 | 315_x_ | function_id <- x[x$token == "FUNCTION", "parent"] |
218 | 315_x_ | if (length(function_id)) { |
219 | 18_x_ | x$id %in% get_children(x, function_id[1])$id |
220 | } else { | |
221 | 297_x_ | rep(FALSE, nrow(x)) |
222 | } | |
223 | } | |
224 | 323_x_ | in_parenthesis <- function(x) { |
225 | 255_x_ | if (any(x$token %in% c("LBB", "'['"))) { |
226 | 7_x_ | id_start <- min(x$id[x$token %in% c("LBB", "'['")]) |
227 | 7_x_ | id_end <- min(x$id[x$token == "']'"]) |
228 | 7_x_ | 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 | 323_x_ | data_call <- find_call(pd, "data") |
234 | 323_x_ | if (data_call) { |
235 | 3_x_ | sym <- pd[data_call + 1, "text"] |
236 | 3_x_ | return(c(gsub("^['\"]|['\"]$", "", sym), "<-")) |
237 | } | |
238 | # Handle assign(x = ). | |
239 | 320_x_ | assign_call <- find_call(pd, "assign") |
240 | 320_x_ | 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 | 5_x_ | if (any(pd$token == "SYMBOL_SUB")) { |
245 | 4_x_ | params <- pd[pd$token %in% c("SYMBOL_SUB", "','", "EQ_SUB"), "text"] |
246 | # Remove sequence of "=", ",". | |
247 | 4_x_ | if (length(params > 1)) { |
248 | 4_x_ | remove <- integer(0) |
249 | 4_x_ | for (i in 2:length(params)) { |
250 | 20_x_ | if (params[i - 1] == "=" && params[i] == ",") { |
251 | 4_x_ | remove <- c(remove, i - 1, i) |
252 | } | |
253 | } | |
254 | 3_x_ | if (length(remove)) params <- params[-remove] |
255 | } | |
256 | 4_x_ | pos <- match("x", setdiff(params, ","), nomatch = match(",", params, nomatch = 0)) |
257 | 4_x_ | 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 | 1_x_ | pos <- 1 |
269 | } | |
270 | 5_x_ | sym <- pd[assign_call + pos, "text"] |
271 | 5_x_ | return(c(gsub("^['\"]|['\"]$", "", sym), "<-")) |
272 | } | |
273 | ||
274 | # What occurs in a function body is not tracked. | |
275 | 315_x_ | x <- pd[!is_in_function(pd), ] |
276 | 315_x_ | sym_cond <- which(x$token %in% c("SPECIAL", "SYMBOL", "SYMBOL_FUNCTION_CALL")) |
277 | 315_x_ | sym_fc_cond <- which(x$token == "SYMBOL_FUNCTION_CALL") |
278 | ||
279 | 315_x_ | if (length(sym_cond) == 0) { |
280 | 18_x_ | 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 ′sID−2soweneedtoremoveallIDsthathaveID='s ID-2 so we need to remove all IDs that have ID = ′sID−2soweneedtoremoveallIDsthathaveID=ID - 2. | |
284 | 297_x_ | dollar_ids <- x[x$token %in% c("'$'", "'@'"), "id"] |
285 | 297_x_ | if (length(dollar_ids)) { |
286 | 6_x_ | object_ids <- x[sym_cond, "id"] |
287 | 6_x_ | after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids] |
288 | 6_x_ | sym_cond <- setdiff(sym_cond, which(x$id %in% after_dollar)) |
289 | } | |
290 | ||
291 | 297_x_ | assign_cond <- grep("ASSIGN", x$token) |
292 | 297_x_ | if (!length(assign_cond)) { |
293 | 42_x_ | return(c("<-", unique(x[sym_cond, "text"]))) |
294 | } | |
295 | ||
296 | # For cases like 'eval(expression(c <- b + 2))' removes 'eval(expression('. | |
297 | 255_x_ | 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 | 255_x_ | if (unique(x$text[assign_cond]) == "->") { # What if there are 2 assignments: e.g. a <- b -> c. |
301 | 1_x_ | sym_cond <- rev(sym_cond) |
302 | } | |
303 | ||
304 | 255_x_ | after <- match(min(x$id[assign_cond]), sort(x$id[c(min(assign_cond), sym_cond)])) - 1 |
305 | 255_x_ | ans <- append(x[sym_cond, "text"], "<-", after = max(1, after)) |
306 | 255_x_ | roll <- in_parenthesis(pd) |
307 | 255_x_ | if (length(roll)) { |
308 | 3_x_ | c(setdiff(ans, roll), roll) |
309 | } else { | |
310 | 252_x_ | ans |
311 | } | |
312 | } | |
313 | ||
314 | #' Extract side effects | |
315 | #' | |
316 | #' Extracts all object names from the code that are marked with `@linksto` tag. | |
317 | #' | |
318 | #' The code may contain functions calls that create side effects, e.g. modify the environment. | |
319 | #' Static code analysis may be insufficient to determine which objects are created or modified by such a function call. | |
320 | #' The `@linksto` comment tag is introduced to mark a call as having a (side) effect on one or more objects. | |
321 | #' With this tag a complete object dependency structure can be established. | |
322 | #' Read more about side effects and the usage of `@linksto` tag in [`get_code_dependencies()`] function. | |
323 | #' | |
324 | #' @param pd `data.frame`; | |
325 | #' one of the results of `utils::getParseData()` split into subsets representing individual calls; | |
326 | #' created by `extract_calls()` function | |
327 | #' | |
328 | #' @return | |
329 | #' A character vector of names of objects | |
330 | #' depending a call tagged with `@linksto` in a corresponding element of `pd`. | |
331 | #' | |
332 | #' @keywords internal | |
333 | #' @noRd | |
334 | extract_side_effects <- function(pd) { | |
335 | 323_x_ | linksto <- grep("@linksto", pd[pd$token == "COMMENT", "text"], value = TRUE) |
336 | 323_x_ | unlist(strsplit(sub("\\s*#.*@linksto\\s+", "", linksto), "\\s+")) |
337 | } | |
338 | ||
339 | #' @param parsed_code results of `parse(text = code, keep.source = TRUE` (parsed text) | |
340 | #' @keywords internal | |
341 | #' @noRd | |
342 | extract_dependency <- function(parsed_code) { | |
343 | 324_x_ | pd <- normalize_pd(utils::getParseData(parsed_code)) |
344 | 324_x_ | reordered_pd <- extract_calls(pd) |
345 | 324_x_ | if (length(reordered_pd) > 0) { |
346 | # extract_calls is needed to reorder the pd so that assignment operator comes before symbol names | |
347 | # extract_calls is needed also to substitute assignment operators into specific format with fix_arrows | |
348 | # extract_calls is needed to omit empty calls that contain only one token `"';'"` | |
349 | # This cleaning is needed as extract_occurrence assumes arrows are fixed, and order is different than in original pd | |
350 | 323_x_ | c(extract_side_effects(reordered_pd[[1]]), extract_occurrence(reordered_pd[[1]])) |
351 | } | |
352 | } | |
353 | ||
354 | # graph_parser ---- | |
355 | ||
356 | #' Return the indices of calls needed to reproduce an object | |
357 | #' | |
358 | #' @param x The name of the object to return code for. | |
359 | #' @param graph A result of `code_graph()`. | |
360 | #' | |
361 | #' @return | |
362 | #' Integer vector of indices that can be applied to `graph` to obtain all calls required to reproduce object `x`. | |
363 | #' | |
364 | #' @keywords internal | |
365 | #' @noRd | |
366 | graph_parser <- function(x, graph) { | |
367 | # x occurrences (lhs) | |
368 | 318_x_ | occurrence <- vapply( |
369 | 318_x_ | graph, function(call) { |
370 | 577_x_ | ind <- match("<-", call, nomatch = length(call) + 1L) |
371 | 577_x_ | x %in% call[seq_len(ind - 1L)] |
372 | }, | |
373 | 318_x_ | logical(1) |
374 | ) | |
375 | ||
376 | # x-dependent objects (rhs) | |
377 | 318_x_ | dependencies <- lapply(graph[occurrence], function(call) { |
378 | 146_x_ | ind <- match("<-", call, nomatch = 0L) |
379 | 146_x_ | call[(ind + 1L):length(call)] |
380 | }) | |
381 | 318_x_ | dependencies <- setdiff(unlist(dependencies), x) |
382 | ||
383 | 318_x_ | dependency_occurrences <- lapply(dependencies, function(dependency) { |
384 | # track down dependencies and where they occur on the lhs in previous calls | |
385 | 229_x_ | last_x_occurrence <- max(which(occurrence)) |
386 | 229_x_ | reduced_graph <- utils::head(graph[seq_len(last_x_occurrence)], -1) |
387 | 229_x_ | c(graph_parser(dependency, reduced_graph), last_x_occurrence) |
388 | }) | |
389 | ||
390 | 318_x_ | sort(unique(c(which(occurrence), unlist(dependency_occurrences)))) |
391 | } | |
392 | ||
393 | ||
394 | # default_side_effects -------------------------------------------------------------------------------------------- | |
395 | ||
396 | #' Detect library calls | |
397 | #' | |
398 | #' Detects `library()` and `require()` function calls. | |
399 | #' | |
400 | #' @param `graph` the dependency graph, result of `lapply(code, attr, "dependency")` | |
401 | #' | |
402 | #' @return | |
403 | #' Integer vector of indices that can be applied to `graph` to obtain all calls containing | |
404 | #' `library()` or `require()` calls that are always returned for reproducibility. | |
405 | #' | |
406 | #' @keywords internal | |
407 | #' @noRd | |
408 | detect_libraries <- function(graph) { | |
409 | 74_x_ | defaults <- c("library", "require") |
410 | ||
411 | 74_x_ | which( |
412 | 74_x_ | unlist( |
413 | 74_x_ | lapply( |
414 | 74_x_ | graph, function(x) { |
415 | 228_x_ | any(grepl(pattern = paste(defaults, collapse = "|"), x = x)) |
416 | } | |
417 | ) | |
418 | ) | |
419 | ) | |
420 | } | |
421 | ||
422 | ||
423 | # utils ----------------------------------------------------------------------------------------------------------- | |
424 | ||
425 | ||
426 | #' Normalize parsed data removing backticks from symbols | |
427 | #' | |
428 | #' @param pd `data.frame` resulting from `utils::getParseData()` call. | |
429 | #' | |
430 | #' @return `data.frame` with backticks removed from `text` column for `SYMBOL` tokens. | |
431 | #' | |
432 | #' @keywords internal | |
433 | #' @noRd | |
434 | normalize_pd <- function(pd) { | |
435 | # Remove backticks from SYMBOL tokens | |
436 | 545_x_ | symbol_index <- grepl("^SYMBOL.*$", pd$token) |
437 | 545_x_ | pd[symbol_index, "text"] <- gsub("^`(.*)`$", "\\1", pd[symbol_index, "text"]) |
438 | ||
439 | 545_x_ | pd |
440 | } | |
441 | ||
442 | ||
443 | # split_code ------------------------------------------------------------------------------------------------------ | |
444 | ||
445 | ||
446 | #' Get line/column in the source where the calls end | |
447 | #' | |
448 | #' | |
449 | #' @param code `character(1)` | |
450 | #' | |
451 | #' @return `matrix` with `colnames = c("line", "col")` | |
452 | #' | |
453 | #' @keywords internal | |
454 | #' @noRd | |
455 | get_call_breaks <- function(code) { | |
456 | 221_x_ | parsed_code <- parse(text = code, keep.source = TRUE) |
457 | 221_x_ | pd <- utils::getParseData(parsed_code) |
458 | 221_x_ | pd <- normalize_pd(pd) |
459 | 221_x_ | pd <- pd[pd$token != "';'", ] |
460 | 221_x_ | call_breaks <- t(sapply( |
461 | 221_x_ | extract_calls(pd), |
462 | 221_x_ | function(x) { |
463 | 335_x_ | matrix(c(max(x$line2), max(x$col2[x$line2 == max(x$line2)]))) |
464 | } | |
465 | )) | |
466 | 221_x_ | call_breaks <- call_breaks[-nrow(call_breaks), , drop = FALSE] # breaks in between needed only |
467 | 221_x_ | colnames(call_breaks) <- c("line", "col") |
468 | 221_x_ | call_breaks |
469 | } | |
470 | ||
471 | #' Split code by calls | |
472 | #' | |
473 | #' @param code `character` with the code. | |
474 | #' | |
475 | #' @return list of `character`s of the length equal to the number of calls in `code`. | |
476 | #' | |
477 | #' @keywords internal | |
478 | #' @noRd | |
479 | split_code <- function(code) { | |
480 | 221_x_ | call_breaks <- get_call_breaks(code) |
481 | 221_x_ | if (nrow(call_breaks) == 0) { |
482 | 169_x_ | return(code) |
483 | } | |
484 | 52_x_ | call_breaks <- call_breaks[order(call_breaks[, "line"], call_breaks[, "col"]), , drop = FALSE] |
485 | 52_x_ | code_split <- strsplit(code, split = "\n", fixed = TRUE)[[1]] |
486 | 52_x_ | char_count_lines <- c(0, cumsum(sapply(code_split, nchar, USE.NAMES = FALSE) + 1), -1)[seq_along(code_split)] |
487 | ||
488 | 52_x_ | idx_start <- c( |
489 | 52_x_ | 0, # first call starts in the beginning of src |
490 | 52_x_ | char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"] + 1 |
491 | ) | |
492 | 52_x_ | idx_end <- c( |
493 | 52_x_ | char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"], |
494 | 52_x_ | nchar(code) # last call end in the end of src |
495 | ) | |
496 | 52_x_ | new_code <- substring(code, idx_start, idx_end) |
497 | ||
498 | # line split happens before call terminator (it could be `;` or `\n`) and the terminator goes to the next line | |
499 | # we need to move remove leading and add \n instead when combining calls | |
500 | 52_x_ | c(new_code[1], gsub("^[\t ]*(\n|;)", "", new_code[-1])) |
501 | } |
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 | 9_x_ | setGeneric("concat", function(x, y) standardGeneric("concat")) |
33 | ||
34 | setMethod("concat", signature = c("qenv", "qenv"), function(x, y) { | |
35 | 5_x_ | y@code <- c(x@code, y@code) |
36 | ||
37 | # insert (and overwrite) objects from y to x | |
38 | 5_x_ | y@.xData <- rlang::env_clone(y@.xData, parent = parent.env(.GlobalEnv)) |
39 | 5_x_ | rlang::env_coalesce(env = y@.xData, from = x@.xData) |
40 | 5_x_ | y |
41 | }) | |
42 | ||
43 | setMethod("concat", signature = c("qenv.error", "ANY"), function(x, y) { | |
44 | 3_x_ | x |
45 | }) | |
46 | ||
47 | setMethod("concat", signature = c("qenv", "qenv.error"), function(x, y) { | |
48 | 1_x_ | 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 | #' | |
13 | #' @return | |
14 | #' `qenv` environment with `code/expr` evaluated or `qenv.error` if evaluation fails. | |
15 | #' | |
16 | #' @examples | |
17 | #' # evaluate code in qenv | |
18 | #' q <- qenv() | |
19 | #' q <- eval_code(q, "a <- 1") | |
20 | #' q <- eval_code(q, "b <- 2L # with comment") | |
21 | #' q <- eval_code(q, quote(library(checkmate))) | |
22 | #' q <- eval_code(q, expression(assert_number(a))) | |
23 | #' | |
24 | #' @aliases eval_code,qenv,character-method | |
25 | #' @aliases eval_code,qenv,language-method | |
26 | #' @aliases eval_code,qenv,expression-method | |
27 | #' @aliases eval_code,qenv.error,ANY-method | |
28 | #' | |
29 | #' @export | |
30 | 424_x_ | setGeneric("eval_code", function(object, code) standardGeneric("eval_code")) |
31 | ||
32 | setMethod("eval_code", signature = c("qenv", "character"), function(object, code) { | |
33 | 222_x_ | parsed_code <- parse(text = code, keep.source = TRUE) |
34 | 222_x_ | object@.xData <- rlang::env_clone(object@.xData, parent = parent.env(.GlobalEnv)) |
35 | 222_x_ | if (length(parsed_code) == 0) { |
36 | # empty code, or just comments | |
37 | 2_x_ | attr(code, "dependency") <- extract_dependency(parsed_code) # in case comment contains @linksto tag |
38 | 2_x_ | object@code <- c(object@code, stats::setNames(list(code), sample.int(.Machine$integer.max, size = 1))) |
39 | 2_x_ | return(object) |
40 | } | |
41 | 220_x_ | code_split <- split_code(paste(code, collapse = "\n")) |
42 | 220_x_ | for (i in seq_along(code_split)) { |
43 | 329_x_ | current_code <- code_split[[i]] |
44 | 329_x_ | current_call <- parse(text = current_code, keep.source = TRUE) |
45 | ||
46 | # Using withCallingHandlers to capture warnings and messages. | |
47 | # Using tryCatch to capture the error and abort further evaluation. | |
48 | 329_x_ | x <- withCallingHandlers( |
49 | 329_x_ | tryCatch( |
50 | { | |
51 | 329_x_ | eval(current_call, envir = object@.xData) |
52 | 316_x_ | if (!identical(parent.env(object@.xData), parent.env(.GlobalEnv))) { |
53 | # needed to make sure that @.xData is always a sibling of .GlobalEnv | |
54 | # could be changed when any new package is added to search path (through library or require call) | |
55 | 3_x_ | parent.env(object@.xData) <- parent.env(.GlobalEnv) |
56 | } | |
57 | 316_x_ | NULL |
58 | }, | |
59 | 329_x_ | error = function(e) { |
60 | 13_x_ | errorCondition( |
61 | 13_x_ | message = sprintf( |
62 | 13_x_ | "%s \n when evaluating qenv code:\n%s", |
63 | 13_x_ | .ansi_strip(conditionMessage(e)), |
64 | 13_x_ | current_code |
65 | ), | |
66 | 13_x_ | class = c("qenv.error", "try-error", "simpleError"), |
67 | 13_x_ | trace = unlist(c(object@code, list(current_code))) |
68 | ) | |
69 | } | |
70 | ), | |
71 | 329_x_ | warning = function(w) { |
72 | 10_x_ | attr(current_code, "warning") <<- .ansi_strip(sprintf("> %s\n", conditionMessage(w))) |
73 | 10_x_ | invokeRestart("muffleWarning") |
74 | }, | |
75 | 329_x_ | message = function(m) { |
76 | 15_x_ | attr(current_code, "message") <<- .ansi_strip(sprintf("> %s", conditionMessage(m))) |
77 | 15_x_ | invokeRestart("muffleMessage") |
78 | } | |
79 | ) | |
80 | ||
81 | 329_x_ | if (!is.null(x)) { |
82 | 13_x_ | return(x) |
83 | } | |
84 | 316_x_ | attr(current_code, "dependency") <- extract_dependency(current_call) |
85 | 316_x_ | object@code <- c(object@code, stats::setNames(list(current_code), sample.int(.Machine$integer.max, size = 1))) |
86 | } | |
87 | ||
88 | 207_x_ | lockEnvironment(object@.xData, bindings = TRUE) |
89 | 207_x_ | object |
90 | }) | |
91 | ||
92 | setMethod("eval_code", signature = c("qenv", "language"), function(object, code) { | |
93 | 150_x_ | eval_code(object, code = paste(vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)), collapse = "\n")) |
94 | }) | |
95 | ||
96 | setMethod("eval_code", signature = c("qenv", "expression"), function(object, code) { | |
97 | 51_x_ | srcref <- attr(code, "wholeSrcref") |
98 | 51_x_ | if (length(srcref)) { |
99 | 2_x_ | eval_code(object, code = paste(attr(code, "wholeSrcref"), collapse = "\n")) |
100 | } else { | |
101 | 49_x_ | Reduce(eval_code, init = object, x = code) |
102 | } | |
103 | }) | |
104 | ||
105 | setMethod("eval_code", signature = c("qenv.error", "ANY"), function(object, code) { | |
106 | ! | object |
107 | }) | |
108 | ||
109 | # if cli is installed rlang adds terminal printing characters | |
110 | # which need to be removed | |
111 | .ansi_strip <- function(chr) { | |
112 | 38_x_ | if (requireNamespace("cli", quietly = TRUE)) { |
113 | 38_x_ | cli::ansi_strip(chr) |
114 | } else { | |
115 | ! | chr |
116 | } | |
117 | } | |
118 | ||
119 | get_code_attr <- function(qenv, attr) { | |
120 | 2_x_ | unlist(lapply(qenv@code, function(x) attr(x, attr))) |
121 | } |
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 | 7_x_ | dev_suppress(object) |
27 | 7_x_ | standardGeneric("get_messages") |
28 | }) | |
29 | ||
30 | setMethod("get_messages", signature = "qenv", function(object) { | |
31 | 5_x_ | get_warn_message_util(object, "message") |
32 | }) | |
33 | ||
34 | setMethod("get_messages", signature = "qenv.error", function(object) { | |
35 | 1_x_ | NULL |
36 | }) | |
37 | ||
38 | setMethod("get_messages", "NULL", function(object) { | |
39 | 1_x_ | NULL |
40 | }) |
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 | 14_x_ | standardGeneric("get_env") |
23 | }) | |
24 | ||
25 | 14_x_ | setMethod("get_env", "qenv", function(object) object@.xData) |
26 | ||
27 | ! | setMethod("get_env", "qenv.error", function(object) object) |
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 | 126_x_ | grDevices::pdf(nullfile()) |
21 | 126_x_ | on.exit(grDevices::dev.off()) |
22 | 126_x_ | 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 | 188_x_ | if (is.atomic(x) | |
44 | 5_x_ | return(list(x)) |
45 | } | |
46 | 183_x_ | if (is.call(x)) { |
47 | 169_x_ | if (identical(as.list(x)[[1L]], as.symbol("{"))) { |
48 | 8_x_ | as.list(x)[-1L] |
49 | } else { | |
50 | 161_x_ | list(x) |
51 | } | |
52 | } else { | |
53 | 14_x_ | 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 | 10_x_ | checkmate::matchArg(what, choices = c("warning", "message")) |
65 | 10_x_ | messages <- lapply(object@code, "attr", what) |
66 | 10_x_ | idx_warn <- which(sapply(messages, function(x) !is.null(x) && !identical(x, ""))) |
67 | 10_x_ | if (!any(idx_warn)) { |
68 | 2_x_ | return(NULL) |
69 | } | |
70 | 8_x_ | messages <- messages[idx_warn] |
71 | 8_x_ | code <- object@code[idx_warn] |
72 | ||
73 | 8_x_ | lines <- mapply( |
74 | 8_x_ | warn = messages, |
75 | 8_x_ | expr = code, |
76 | 8_x_ | function(warn, expr) { |
77 | 12_x_ | sprintf("%swhen running code:\n%s", warn, expr) |
78 | } | |
79 | ) | |
80 | ||
81 | 8_x_ | sprintf( |
82 | 8_x_ | "~~~ %ss ~~~\n\n%s\n\n~~~ Trace ~~~\n\n%s", |
83 | 8_x_ | tools::toTitleCase(what), |
84 | 8_x_ | paste(lines, collapse = "\n\n"), |
85 | 8_x_ | paste(get_code(object), collapse = "\n") |
86 | ) | |
87 | } |
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 | 170_x_ | new_xdata <- if (rlang::is_missing(.xData)) { |
35 | 168_x_ | new.env(parent = parent.env(.GlobalEnv)) |
36 | } else { | |
37 | 2_x_ | checkmate::assert_environment(.xData) |
38 | 1_x_ | rlang::env_clone(.xData, parent = parent.env(.GlobalEnv)) |
39 | } | |
40 | 169_x_ | lockEnvironment(new_xdata, bindings = TRUE) |
41 | ||
42 | # .xData needs to be unnamed as the `.environment` constructor allows at | |
43 | # most 1 unnamed formal argument of class `environment`. | |
44 | # See methods::findMethods("initialize")$.environment | |
45 | 169_x_ | methods::callNextMethod( |
46 | 169_x_ | .Object, |
47 | 169_x_ | new_xdata, # Mandatory use of unnamed environment arg |
48 | 169_x_ | code = code, ... |
49 | ) | |
50 | } | |
51 | ) | |
52 | ||
53 | #' It takes a `qenv` class and returns `TRUE` if the input is valid | |
54 | #' @name qenv-class | |
55 | #' @keywords internal | |
56 | setValidity("qenv", function(object) { | |
57 | if (any(duplicated(names(object@code)))) { | |
58 | "@code must have unique names." | |
59 | } else if (!environmentIsLocked(object@.xData)) { | |
60 | "@.xData must be locked." | |
61 | } else { | |
62 | TRUE | |
63 | } | |
64 | }) |
1 | #' Join `qenv` objects | |
---|---|---|
2 | #' | |
3 | #' @description | |
4 | #' Checks and merges two `qenv` objects into one `qenv` object. | |
5 | #' | |
6 | #' The `join()` function is superseded by the `c()` function. | |
7 | #' | |
8 | #' @details | |
9 | #' Any common code at the start of the `qenvs` is only placed once at the start of the joined `qenv`. | |
10 | #' This allows consistent behavior when joining `qenvs` which share a common ancestor. | |
11 | #' See below for an example. | |
12 | #' | |
13 | #' There are some situations where `join()` cannot be properly performed, such as these three scenarios: | |
14 | #' 1. Both `qenv` objects contain an object of the same name but are not identical. | |
15 | #' | |
16 | #' Example: | |
17 | #' | |
18 | #' ```r | |
19 | #' x <- eval_code(qenv(), expression(mtcars1 <- mtcars)) | |
20 | #' y <- eval_code(qenv(), expression(mtcars1 <- mtcars['wt'])) | |
21 | #' | |
22 | #' z <- c(x, y) | |
23 | #' # Error message will occur | |
24 | #' ``` | |
25 | #' In this example, `mtcars1` object exists in both `x` and `y` objects but the content are not identical. | |
26 | #' `mtcars1` in the `x qenv` object has more columns than `mtcars1` in the `y qenv` object (only has one column). | |
27 | #' | |
28 | #' 2. `join()` will look for identical code elements in both `qenv` objects. | |
29 | #' The index position of these code elements must be the same to determine the evaluation order. | |
30 | #' Otherwise, `join()` will throw an error message. | |
31 | #' | |
32 | #' Example: | |
33 | #' ```r | |
34 | #' common_q <- eval_code(qenv(), expression(v <- 1)) | |
35 | #' x <- eval_code( | |
36 | #' common_q, | |
37 | #' "x <- v" | |
38 | #' ) | |
39 | #' y <- eval_code( | |
40 | #' common_q, | |
41 | #' "y <- v" | |
42 | #' ) | |
43 | #' z <- eval_code( | |
44 | #' y, | |
45 | #' "z <- v" | |
46 | #' ) | |
47 | #' q <- c(x, y) | |
48 | #' join_q <- c(q, z) | |
49 | #' # Error message will occur | |
50 | #' | |
51 | #' # Check the order of evaluation based on the id slot | |
52 | #' ``` | |
53 | #' The error occurs because the index position of common code elements in the two objects is not the same. | |
54 | #' | |
55 | #' 3. The usage of temporary variable in the code expression could cause `join()` to fail. | |
56 | #' | |
57 | #' Example: | |
58 | #' ```r | |
59 | #' common_q <- qenv() | |
60 | #' x <- eval_code( | |
61 | #' common_q, | |
62 | #' "x <- numeric(0) | |
63 | #' for (i in 1:2) { | |
64 | #' x <- c(x, i) | |
65 | #' }" | |
66 | #' ) | |
67 | #' y <- eval_code( | |
68 | #' common_q, | |
69 | #' "y <- numeric(0) | |
70 | #' for (i in 1:3) { | |
71 | #' y <- c(y, i) | |
72 | #' }" | |
73 | #' ) | |
74 | #' q <- join(x,y) | |
75 | #' # Error message will occur | |
76 | #' | |
77 | #' # Check the value of temporary variable i in both objects | |
78 | #' x$i # Output: 2 | |
79 | #' y$i # Output: 3 | |
80 | #' ``` | |
81 | #' `c()` fails to provide a proper result because of the temporary variable `i` exists | |
82 | #' in both objects but has different value. | |
83 | #' To fix this, we can set `i <- NULL` in the code expression for both objects. | |
84 | #' ```r | |
85 | #' common_q <- qenv() | |
86 | #' x <- eval_code( | |
87 | #' common_q, | |
88 | #' "x <- numeric(0) | |
89 | #' for (i in 1:2) { | |
90 | #' x <- c(x, i) | |
91 | #' } | |
92 | #' # dummy i variable to fix it | |
93 | #' i <- NULL" | |
94 | #' ) | |
95 | #' y <- eval_code( | |
96 | #' common_q, | |
97 | #' "y <- numeric(0) | |
98 | #' for (i in 1:3) { | |
99 | #' y <- c(y, i) | |
100 | #' } | |
101 | #' # dummy i variable to fix it | |
102 | #' i <- NULL" | |
103 | #' ) | |
104 | #' q <- c(x,y) | |
105 | #' ``` | |
106 | #' | |
107 | #' @param x (`qenv`) | |
108 | #' @param y (`qenv`) | |
109 | #' | |
110 | #' @return `qenv` object. | |
111 | #' | |
112 | #' @examples | |
113 | #' q <- qenv() | |
114 | #' q1 <- eval_code(q, expression(iris1 <- iris, mtcars1 <- mtcars)) | |
115 | #' q2 <- q1 | |
116 | #' q1 <- eval_code(q1, "iris2 <- iris") | |
117 | #' q2 <- eval_code(q2, "mtcars2 <- mtcars") | |
118 | #' qq <- join(q1, q2) | |
119 | #' cat(get_code(qq)) | |
120 | #' | |
121 | #' common_q <- eval_code(q, quote(x <- 1)) | |
122 | #' y_q <- eval_code(common_q, quote(y <- x * 2)) | |
123 | #' z_q <- eval_code(common_q, quote(z <- x * 3)) | |
124 | #' join_q <- join(y_q, z_q) | |
125 | #' # get_code only has "x <- 1" occurring once | |
126 | #' cat(get_code(join_q)) | |
127 | #' | |
128 | #' @include qenv-errors.R | |
129 | #' | |
130 | #' @name join | |
131 | #' @rdname join | |
132 | #' @aliases join,qenv,qenv-method | |
133 | #' @aliases join,qenv,qenv.error-method | |
134 | #' @aliases join,qenv.error,ANY-method | |
135 | #' | |
136 | #' @export | |
137 | ! | setGeneric("join", function(x, y) standardGeneric("join")) |
138 | ||
139 | setMethod("join", signature = c("qenv", "qenv"), function(x, y) { | |
140 | ! | lifecycle::deprecate_soft("0.6.0", "join()", "c()") |
141 | ! | c(x, y) |
142 | }) | |
143 | ||
144 | setMethod("join", signature = c("qenv", "qenv.error"), function(x, y) { | |
145 | ! | lifecycle::deprecate_soft("0.6.0", "join()", "c()") |
146 | ! | y |
147 | }) | |
148 | ||
149 | setMethod("join", signature = c("qenv.error", "ANY"), function(x, y) { | |
150 | ! | lifecycle::deprecate_soft("0.6.0", "join()", "c()") |
151 | ! | x |
152 | }) |
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 `" 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 ()` | |
75 | #' - objects used to iterate over in `for` loops: `for(i in )` | |
76 | #' - creating and evaluating language objects, _e.g._ `eval()` | |
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 | 105_x_ | dev_suppress(object) |
101 | 105_x_ | standardGeneric("get_code") |
102 | }) | |
103 | ||
104 | setMethod("get_code", signature = "qenv", function(object, deparse = TRUE, names = NULL, ...) { | |
105 | 103_x_ | checkmate::assert_flag(deparse) |
106 | 103_x_ | checkmate::assert_character(names, min.len = 1L, null.ok = TRUE) |
107 | ||
108 | # Normalize in case special it is backticked | |
109 | 103_x_ | if (!is.null(names)) { |
110 | 63_x_ | names <- gsub("^`(.*)`$", "\\1", names) |
111 | } | |
112 | ||
113 | 103_x_ | code <- if (!is.null(names)) { |
114 | 63_x_ | get_code_dependency(object@code, names, ...) |
115 | } else { | |
116 | 40_x_ | object@code |
117 | } | |
118 | ||
119 | 103_x_ | if (deparse) { |
120 | 101_x_ | paste(unlist(code), collapse = "\n") |
121 | } else { | |
122 | 2_x_ | parse(text = paste(c("{", unlist(code), "}"), collapse = "\n"), keep.source = TRUE) |
123 | } | |
124 | }) | |
125 | ||
126 | setMethod("get_code", signature = "qenv.error", function(object, ...) { | |
127 | 2_x_ | stop( |
128 | 2_x_ | errorCondition( |
129 | 2_x_ | sprintf( |
130 | 2_x_ | "%s\n\ntrace: \n %s\n", |
131 | 2_x_ | conditionMessage(object), |
132 | 2_x_ | paste(object$trace, collapse = "\n ") |
133 | ), | |
134 | 2_x_ | class = c("validation", "try-error", "simpleError") |
135 | ) | |
136 | ) | |
137 | }) |
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 | 16_x_ | checkmate::assert_class(x, "qenv") |
11 | 16_x_ | checkmate::assert_class(y, "qenv") |
12 | ||
13 | 16_x_ | common_names <- intersect(rlang::env_names(x@.xData), rlang::env_names(y@.xData)) |
14 | 16_x_ | is_overwritten <- vapply(common_names, function(el) { |
15 | 13_x_ | !identical(get(el, x@.xData), get(el, y@.xData)) |
16 | 16_x_ | }, logical(1)) |
17 | 16_x_ | if (any(is_overwritten)) { |
18 | 2_x_ | return( |
19 | 2_x_ | paste( |
20 | 2_x_ | "Not possible to join qenv objects if anything in their environment has been modified.\n", |
21 | 2_x_ | "Following object(s) have been modified:\n - ", |
22 | 2_x_ | paste(common_names[is_overwritten], collapse = "\n - ") |
23 | ) | |
24 | ) | |
25 | } | |
26 | ||
27 | 14_x_ | x_id <- names(x@code) |
28 | 14_x_ | y_id <- names(y@code) |
29 | ||
30 | 14_x_ | shared_ids <- intersect(x_id, y_id) |
31 | 14_x_ | if (length(shared_ids) == 0) { |
32 | 8_x_ | return(TRUE) |
33 | } | |
34 | ||
35 | 6_x_ | shared_in_x <- match(shared_ids, x_id) |
36 | 6_x_ | shared_in_y <- match(shared_ids, y_id) |
37 | ||
38 | # indices of shared ids should be 1:n in both slots | |
39 | 6_x_ | if (identical(shared_in_x, shared_in_y) && identical(shared_in_x, seq_along(shared_ids))) { |
40 | 4_x_ | TRUE |
41 | 2_x_ | } else if (!identical(shared_in_x, shared_in_y)) { |
42 | 1_x_ | paste( |
43 | 1_x_ | "The common shared code of the qenvs does not occur in the same position in both qenv objects", |
44 | 1_x_ | "so they cannot be joined together as it's impossible to determine the evaluation's order.", |
45 | 1_x_ | collapse = "" |
46 | ) | |
47 | } else { | |
48 | 1_x_ | paste( |
49 | 1_x_ | "There is code in the qenv objects before their common shared code", |
50 | 1_x_ | "which means these objects cannot be joined.", |
51 | 1_x_ | 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 | 186_x_ | dots <- rlang::list2(...) |
72 | 186_x_ | if (!checkmate::test_list(dots[-1], types = c("qenv", "qenv.error"))) { |
73 | 169_x_ | return(NextMethod(c, dots[[1]])) |
74 | } | |
75 | ||
76 | 17_x_ | first_non_qenv_ix <- which.min(vapply(dots, inherits, what = "qenv", logical(1))) |
77 | 17_x_ | if (first_non_qenv_ix > 1) { |
78 | 1_x_ | return(dots[[first_non_qenv_ix]]) |
79 | } | |
80 | ||
81 | 16_x_ | Reduce( |
82 | 16_x_ | x = dots[-1], |
83 | 16_x_ | init = dots[[1]], |
84 | 16_x_ | f = function(x, y) { |
85 | 16_x_ | join_validation <- .check_joinable(x, y) |
86 | ||
87 | # join expressions | |
88 | 16_x_ | if (!isTRUE(join_validation)) { |
89 | 4_x_ | stop(join_validation) |
90 | } | |
91 | ||
92 | 12_x_ | x@code <- utils::modifyList(x@code, y@code) |
93 | ||
94 | # insert (and overwrite) objects from y to x | |
95 | 12_x_ | x@.xData <- rlang::env_clone(x@.xData, parent = parent.env(.GlobalEnv)) |
96 | 12_x_ | rlang::env_coalesce(env = x@.xData, from = y@.xData) |
97 | 12_x_ | x |
98 | } | |
99 | ) | |
100 | } | |
101 | ||
102 | #' @rdname join | |
103 | #' @export | |
104 | c.qenv.error <- function(...) { | |
105 | 3_x_ | rlang::list2(...)[[1]] |
106 | } |
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 | 12_x_ | checkmate::assert_character(names, any.missing = FALSE) |
22 | 12_x_ | possible_names <- ls(get_env(x), all.names = TRUE) |
23 | 12_x_ | names_corrected <- intersect(names, possible_names) |
24 | 12_x_ | env <- if (length(names_corrected)) { |
25 | 9_x_ | names_missing <- setdiff(names, possible_names) |
26 | 9_x_ | if (length(names_missing)) { |
27 | 2_x_ | warning( |
28 | 2_x_ | sprintf( |
29 | 2_x_ | "Some 'names' do not exist in the environment of the '%s'. Skipping those: %s.", |
30 | 2_x_ | class(x)[1], |
31 | 2_x_ | paste(names_missing, collapse = ", ") |
32 | ) | |
33 | ) | |
34 | } | |
35 | 9_x_ | list2env(as.list(x, all.names = TRUE)[names_corrected], parent = parent.env(.GlobalEnv)) |
36 | } else { | |
37 | 3_x_ | warning( |
38 | 3_x_ | sprintf( |
39 | 3_x_ | "None of 'names' exist in the environment of the '%1$s'. Returning empty '%1$s'.", |
40 | 3_x_ | class(x)[1] |
41 | ), | |
42 | 3_x_ | call. = FALSE |
43 | ) | |
44 | 3_x_ | new.env(parent = parent.env(.GlobalEnv)) |
45 | } | |
46 | 12_x_ | lockEnvironment(env) |
47 | 12_x_ | x@.xData <- env |
48 | ||
49 | 12_x_ | normalized_names <- gsub("^`(.*)`$", "\\1", names) |
50 | 12_x_ | x@code <- get_code_dependency(x@code, names = normalized_names, ...) |
51 | ||
52 | 12_x_ | x |
53 | } |
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 | 167_x_ | methods::new("qenv") |
39 | } |
1 | #' @details | |
---|---|---|
2 | #' `within()` is a convenience method that wraps `eval_code` to provide a simplified way of passing expression. | |
3 | #' `within` accepts only inline expressions (both simple and compound) and allows to substitute `expr` | |
4 | #' with `...` named argument values. | |
5 | #' | |
6 | #' @section Using language objects with `within`: | |
7 | #' Passing language objects to `expr` is generally not intended but can be achieved with `do.call`. | |
8 | #' Only single `expression`s will work and substitution is not available. See examples. | |
9 | #' | |
10 | #' @param data (`qenv`) | |
11 | #' @param expr (`expression`) to evaluate. Must be inline code, see `Using language objects...` | |
12 | #' @param ... named argument value will substitute a symbol in the `expr` matched by the name. | |
13 | #' For practical usage see Examples section below. | |
14 | #' | |
15 | #' @examples | |
16 | #' # evaluate code using within | |
17 | #' q <- qenv() | |
18 | #' q <- within(q, { | |
19 | #' i <- iris | |
20 | #' }) | |
21 | #' q <- within(q, { | |
22 | #' m <- mtcars | |
23 | #' f <- faithful | |
24 | #' }) | |
25 | #' q | |
26 | #' get_code(q) | |
27 | #' | |
28 | #' # inject values into code | |
29 | #' q <- qenv() | |
30 | #' q <- within(q, i <- iris) | |
31 | #' within(q, print(dim(subset(i, Species == "virginica")))) | |
32 | #' within(q, print(dim(subset(i, Species == species)))) # fails | |
33 | #' within(q, print(dim(subset(i, Species == species))), species = "versicolor") | |
34 | #' species_external <- "versicolor" | |
35 | #' within(q, print(dim(subset(i, Species == species))), species = species_external) | |
36 | #' | |
37 | #' # pass language objects | |
38 | #' expr <- expression(i <- iris, m <- mtcars) | |
39 | #' within(q, expr) # fails | |
40 | #' do.call(within, list(q, expr)) | |
41 | #' | |
42 | #' exprlist <- list(expression(i <- iris), expression(m <- mtcars)) | |
43 | #' within(q, exprlist) # fails | |
44 | #' do.call(within, list(q, do.call(c, exprlist))) | |
45 | #' | |
46 | #' @rdname eval_code | |
47 | #' | |
48 | #' @export | |
49 | #' | |
50 | within.qenv <- function(data, expr, ...) { | |
51 | 42_x_ | expr <- substitute(expr) |
52 | 42_x_ | extras <- list(...) |
53 | ||
54 | # Add braces for consistency. | |
55 | 42_x_ | if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { |
56 | 13_x_ | expr <- call("{", expr) |
57 | } | |
58 | ||
59 | 42_x_ | calls <- as.list(expr)[-1] |
60 | ||
61 | # Inject extra values into expressions. | |
62 | 42_x_ | calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) |
63 | ||
64 | 42_x_ | eval_code(object = data, code = as.expression(calls)) |
65 | } | |
66 | ||
67 | ||
68 | #' @keywords internal | |
69 | #' | |
70 | #' @export | |
71 | within.qenv.error <- function(data, expr, ...) { | |
72 | 1_x_ | data |
73 | } |
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 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 | 7_x_ | dev_suppress(object) |
27 | 7_x_ | standardGeneric("get_warnings") |
28 | }) | |
29 | ||
30 | setMethod("get_warnings", signature = "qenv", function(object) { | |
31 | 5_x_ | get_warn_message_util(object, "warning") |
32 | }) | |
33 | ||
34 | setMethod("get_warnings", signature = "qenv.error", function(object) { | |
35 | 1_x_ | NULL |
36 | }) | |
37 | ||
38 | setMethod("get_warnings", "NULL", function(object) { | |
39 | 1_x_ | NULL |
40 | }) |
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 | #' @export | |
---|---|---|
2 | ! | length.qenv <- function(x) length(x@.xData) |
3 | ||
4 | #' @export | |
5 | 22_x_ | length.qenv.error <- function(x) 0 |