127 with(in) qenv - #136
Conversation
…ng/teal.code into 127_qenv_refactor@main
|
|
||
| # Add braces to expressions. Necessary for proper storage of some expressions (e.g. rm(x)). | ||
| if (!is.null(expr) && !grepl("^\\{", deparse1(expr))) { | ||
| expr <- call("{", expr) |
There was a problem hiding this comment.
Simple expressions should be enclosed in braces to store the code properly. Non-braced expressions are handled for convenience but it may be safer to use braces at all times.
Have you considered showing a message or warning every 8 hours to discourage this behaviour?
| expr <- call("{", expr) | |
| rlang::inform( | |
| "Please refrain from using expressions that are not surrounded by braces (use `{ ... }` even for a single line expression)", | |
| .frequency = "regularly", | |
| .frequency_id = "curly_expr_info" | |
| ) | |
| expr <- call("{", expr) |
There was a problem hiding this comment.
I hate rlang 😉
No, I haven't. Thanks for the suggestion.
averissimo
left a comment
There was a problem hiding this comment.
This looks really good!! 👍
I played around with it and found some (possible) issues with text expressions.
- [text param] Single expression spanning more than 1 line
- [text param] Compound expression
- [text param] Ends with newline
Regarding the first 2 I commented on the code, the last one is just a minor detail 😅
All 3 problems seem to go away when using str2expression, but I'm guessing there's a reason not to use that function
testthat::test_that("with.qenv Supports text expression that snds with a newline", {
str_expr <- "1 + 1
2 * 3
"
script_path <- withr::local_file(tempfile())
writeLines(str_expr, script_path)
q <- qenv()
with(q, text = readLines(script_path)) |>
testthat::expect_no_error()
})|
Thanks @averissimo! Since this is not a priority now I wasn't expecting a proper review yet so I'm happy to see some interest :) |
|
Right, the reason I decided against Perhaps I missed something. Where exactly did you use |
|
@chlebowa Sorry if I jumped the gun with the review.. this looks really good and I couldn't help myself to test it a bit I changed the ℹ️ See the comment on the code with a crude proposal that may need some polishing
I think we can we actually run each expression separately, see the toy example below: text1 <- "1 + 2
3*3
a <- 2 +
3
a * 5"
text2 <- "{9 + 2
8*3
c <- 2 +
4
c * 10}"
cat_expr <- \(text) {
expr <- str2expression(text)
if (length(expr) == 0) return(invisible(NULL))
if (length(expr) == 1 && isTRUE(expr[[1]][[1]] == "{")) {
expr <- expr[[1]][seq(2, length(expr[[1]]))]
}
if (length(expr) > 1) {
expr |>
purrr::walk(\(.x) cat("Expression:", deparse1(.x), "\n"))
} else {
# Otherwise treat it as single expression
cat("Expression:", deparse1(expr[[1]]), "\n")
}
invisible(NULL)
}
cat_expr(text1)
cat_expr(text2)
cat_expr("1+1")
cat_expr("")
cat_expr("NULL")
cat_expr("NA") |
By all means, be my guest 🙂 Ok, I see now that if the then the above can be dropped. Well done 👍 One question though: do we want to tolerate such cases (i.e. strings containing invalid expressions) or should we forbid them? |
Code Coverage SummaryDiff against mainResults for commit: d913882 Minimum allowed coverage is ♻️ This comment has been updated with latest results |
|
So, properly parsing all kinds of expressions (that came to mind) took some more effort but I think i got it right now. The idea is compound expressions are always disassembled into simple ones and those are evaluated sequentially, so that evaluation stops when an error is encountered. On top of that, all simple expressions are stored (and printed) in the same neat form, regardless of how they were passed (braces, semicolons, line breaks, etc.). |
gogonzo
left a comment
There was a problem hiding this comment.
- I like the idea with qenv being an S3 environment.
- This
with.qenvexposes us to some risks. - Beware that qenv will be inherited in teal.data. Is it safe to use S3 in the way
class(x) <- c("tdata", "qenv", "environment")and just register new methods fortdata?
| with.quenv <- function(data, expr, text, ...) { | ||
| code <- .prepare_code(if (!missing(expr)) substitute(expr), if (!missing(text)) text) | ||
| extras <- list(...) | ||
| lapply(code, .eval_one, envir = data, enclos = parent.frame(), extras = extras) | ||
| invisible(NULL) | ||
| } |
There was a problem hiding this comment.
I don't like with.quenv method, it is not compatible with generic. with.default returns result of the evaluation and here with.quenv modifies environment of the object. It's dangerous in our case where we pass object from one reactive to another - it's vulnerable that latter reactive might change object present in previous reactives (we risk double evaluation).
|
|
||
| #' @rdname quenv | ||
| #' @export | ||
| within.quenv <- function(data, expr, text, ...) { |
There was a problem hiding this comment.
| within.quenv <- function(data, expr, text, ...) { | |
| within.quenv <- function(data, expr, text, ...) { |
expr, text and ... in a separate fields to avoid evaluation for sake of dispatch? I think this is reasonable alternative to have multiple arguments instead of dispatch.
| quenv <- function() { | ||
| ans <- new.env() | ||
| attr(ans, "code") <- list() | ||
| attr(ans, "errors") <- list() | ||
| attr(ans, "warnings") <- list() | ||
| attr(ans, "messages") <- list() | ||
| class(ans) <- c("quenv", class(ans)) | ||
| ans | ||
| } |
There was a problem hiding this comment.
| quenv <- function() { | |
| ans <- new.env() | |
| attr(ans, "code") <- list() | |
| attr(ans, "errors") <- list() | |
| attr(ans, "warnings") <- list() | |
| attr(ans, "messages") <- list() | |
| class(ans) <- c("quenv", class(ans)) | |
| ans | |
| } | |
| quenv <- function() { | |
| ans <- new.env() | |
| attr(ans, "code") <- list() | |
| attr(ans, "errors") <- list() | |
| attr(ans, "warnings") <- list() | |
| attr(ans, "messages") <- list() | |
| class(ans) <- c("quenv", class(ans)) | |
| ans | |
| } |
Smart move with qenv being an environment. Not sure yet what is better:
- qenv as S3 environment and protecting class by overwriting
$<-.qenv,[[<-.qenvbut potentially also other methods (which?eval?) - qenv as S4 and writing own
$,[[methods to get objects.
I like qenv as environment, I think inheritance and reusing the class in other packages should be the key for a decision.
| `$<-.quenv` <- function(x, name, value) { # nolint | ||
| stop( | ||
| "Direct assignment is forbidden as it cannot be tracked. ", | ||
| "Use `with( <quenv>, { <name> <- <value> })` instead." | ||
| ) | ||
| } |
There was a problem hiding this comment.
instead of $<-.quenv, [[<-.qenv it is possible to just lockEnvironment and unlock only inside within
gogonzo
left a comment
There was a problem hiding this comment.
To introduce within method we don't need to refactor whole package. We can add within.qenv <- function(data, expr, text, ...) function and decide some day about form of qenv.
|
Closing in favor of #149 |
Closes #127
CURRENT IMPLEMENTATION
(NOTE: the new class is called
quenvin the code to avoid conflicts with the existingqenv).qenvis a simple S3 class, an environment with some attributes:code,errors,warnings,messages.with.qenvmethod is used to evaluate code in theqenv.Syntax
Just pass an expression to the
exprargument inwithcall.Simple expressions should be enclosed in braces to store the code properly. Non-braced expressions are handled for convenience but it may be safer to use braces at all times.
Compound expressions are supported but consecutive lines are evaluated separately, in turn.
If a simple expression that is part of a compound expression raises an error, the following lines are not evaluated.
Code can also be passed as strings, both literals and values, through the
textargument.Strings passed to the
exprraise errors. Strings passed as parts of compound expressions are ignored.The
textargument can be used to evaluate code imported from a file.Evaluation
Expression are evaluated in the
qenv(per definition of thewithgeneric it is calleddata) and the enclosing environment is the defaultparent.frame(). This includes the global environment in the scope, which makes thei <- iriscalls possible.In order to use expressions obtained programmatically, i.e. pass values that are not defined neither in
data, nor in the parent frame, the expressions are first substituted in the current environment, i.e. the execution environment of thewithcall. Thus, we simply pass any extra values to the ellipsis:The same mechanism can utilize variables defined outside of the
withcall.Tracking
qenvhistoryEach (single line) expression is stored in the
codeattribute, which is a list of expressions. After running the first code block,attr(q, "code")will be a list containing two expressions, both of themi <- iris.The expression that is stored is the expression that is evaluated, after substitution of external values is complete. In the last two examples the subset expressions would be stored as
ii <- subset(i, Species == "versicolor")andii <- subset(i, Species == "virginica"), NOT asii <- subset(i, Species == species)andii <- subset(i, Species == input_value), respectively.Storing conditions
After an expression has been evaluated, messages from any conditions raised are stored in respective attributes, which are lists of character strings.
If no condition is raised, an empty string is stored so that each condition list has the same length as
codeand condition messages can be easily matched to the code that raised them.Utilities.
An elaborate
formatmethod prints a summary of contents, all expressions evaluated and all condition messages.get_codeandget_conditionfunctions are provided for convenient retrieval of attributes.Values are retrieved with the usual indexing operators,
$and[[.withinwithin.qenvworks exactly likewith.qenv, except it creates a deep copy of theqenvand returns the result.