introduce teal_data class - #178
Conversation
any data possible
adjust to teal
`ddl` implementation alternative to #161 . Complemented by [this PR](insightsengineering/teal#922). In order to simplify the user (app dev) experience, I tried to streamline the logic. In order to create a `ddl` connector module, one has to: 1. use `input_template` to create the module: enumerate input widgets 2. provide a function, `on_submit`, to be run when `"submit"` button is clicked; function takes input values wrapped in a list called `input` and body refers to input values with `input$<value>` or `input[["<value>"]]` 3. optionally provide mask for input values that will be used in code of resulting `tdata` object 4. specify names of data sets for compatibility with `teal` (I don't like it) 5. optionally specify join keys as one would previously, for compatibility with `teal`; defaults to empty `teal.data::join_keys()` When inputs are submitted, `on_submit` is passed to a function that extracts the body, substitutes `input` placeholders with input values and evaluates the code to obtain data sets in a separate environment. Then it replaces the input values in the code with ones provided in `mask` (if any) and uses the environment and the masked code to create `tdata`. Much like in the solution proposed on branch `refactor`, the user provides code to obtain data sets and replacements for input values, and data is created in separate environment, which is then used to create `tdata` with masked code. Unlike that solution, the user specifies everything in one place, rather than having to define module ui, module server that runs a post-processing function, the post-processing function itself, etc. This is easier to understand **for me**. Another difference is that the user provides code as code with `input$` references, not text with `glue` syntax (`{ value }`). This is done move focus to masking rather than have the user think about "online" and "offline" arguments. It also uses pure base R. #### MOCK DATABASE CONNECTION ``` pullme <- function(username, password) { if (username == "user" && password == "pass") { message("connection established") } else { stop("invalid credentials") } } closeme <- function() { message("connection closed") } ``` #### MODULE DEFINITION ``` library(shiny) thefun <- function(input) { on.exit(try(closeme())) pullme(username = input$user, password = input$pass) adsl <- scda::synthetic_cdisc_data('latest')$adsl adtte <- scda::synthetic_cdisc_data('latest')$adtte } themask <- list( user = quote(askpass("who are you?")), pass = quote(askpass("password please")) ) module <- input_template( on_submit = thefun, mask = themask, datanames = c("adsl", "adtte"), textInput("user", "username", value = "user", placeholder = "who goes there?"), passwordInput("pass", "password", value = "pass", placeholder = "friend or foe?"), actionButton("submit", "get it") ) ``` #### AN APP ``` devtools::load_all("../teal.slice") devtools::load_all("../teal") devtools::load_all(".") ui <- fluidPage( tagList( module$ui("id"), uiOutput("val") ) ) server <- function(input, output, session) { tdata <- module$server("id") output[["value"]] <- renderPrint({ tdata() }) output[["code"]] <- renderPrint({ cat(teal.code::get_code(tdata()), sep = "\n") }) output[["val"]] <- renderUI({ req(tdata()) tagList( verbatimTextOutput("value"), verbatimTextOutput("code") ) }) } if (interactive()) shinyApp(ui, server) ``` #### A TEAL APP ``` funny_module <- function (label = "Filter states", datanames = "all") { checkmate::assert_string(label) module( label = label, datanames = datanames, ui = function(id, ...) { ns <- NS(id) div( h2("The following filter calls are generated:"), verbatimTextOutput(ns("filter_states")), verbatimTextOutput(ns("filter_calls")), actionButton(ns("reset"), "reset_to_default") ) }, server = function(input, output, session, data, filter_panel_api) { checkmate::assert_class(data, "tdata") observeEvent(input$reset, set_filter_state(filter_panel_api, default_filters)) output$filter_states <- renderPrint({ logger::log_trace("rendering text1") filter_panel_api %>% get_filter_state() }) output$filter_calls <- renderText({ logger::log_trace("rendering text2") attr(data, "code")() }) } ) } devtools::load_all("../teal.slice") devtools::load_all("../teal") devtools::load_all(".") app <- init( data = module, modules = modules( funny_module("funny1"), funny_module("funny2", datanames = "adtte") # will limit datanames to ADTTE and ADSL (parent) ) ) shinyApp(app$ui, app$server) ```
ddl WIP
POC
tdata -> teal_data
mask_args -> input_mask
online_args -> input
ddl from teal.data to teal
teal_data class only
remove to_relational for classes which doesn't need transformation
specify datanames when more objects in environment
Fixes the broken tests due to the teal_data refactor --------- Co-authored-by: go_gonzo <dawid.kaledkowski@gmail.com>
reload NAMESPACE
|
Even though this branch is green, these tests fail on my machine 🤔 |
averissimo
left a comment
There was a problem hiding this comment.
teal_data looks very good at the current state!
Some minor comments, below.. The test/testhat/test-TealDatasetConnector.R is the only one that seems important to merge.
My only big grip is one layer down with using S4 classes, in part by having exposed slots in qenv and by consequence on teal_data class.
It seems more beneficial to keep those fields as private and using active bindings to "expose" those as read-only.
However, converting qenv to R6 is a whole new big project and may introduce caveats as well (hence the use of S4... this predates my joining of the project and I'm not familiar with all the reasons).
@averissimo suggestions Co-authored-by: André Veríssimo <211358+averissimo@users.noreply.github.com> Signed-off-by: Dawid Kałędkowski <6959016+gogonzo@users.noreply.github.com>
| #' The `dataname` is extrapolated from the name (or fallback to the value itself if | ||
| #' it's a `character(1)`). |
| if (name %in% names(default_cdisc_keys)) { | ||
| # Set default primary keys | ||
| keys_list <- default_cdisc_keys[[name]] | ||
| join_keys[name] <- keys_list$primary | ||
|
|
||
| if (!is.null(keys_list$parent) && !is.null(keys_list$foreign)) { | ||
| join_keys[name, keys_list$parent] <- keys_list$foreign | ||
| } | ||
| } |
There was a problem hiding this comment.
This block should go into the block in line 424. The logic will be reflected better and the return(NULL) statements can be removed. The last case becomes redundant altogether.
There was a problem hiding this comment.
Not true. If name is not null then previous if-else doesn't exit (don't return NULL)
There was a problem hiding this comment.
How about this?
if (checkmate::test_class(item, "JoinKeySet")) {
join_keys$set(item)
} else {
if ((is.null(name) || identical(trimws(name), "")) && is.character(item)) {
name <- item
}
if (name %in% names(default_cdisc_keys)) {
# Set default primary keys
keys_list <- default_cdisc_keys[[name]]
join_keys[name] <- keys_list$primary
if (!is.null(keys_list$parent) && !is.null(keys_list$foreign)) {
join_keys[name, keys_list$parent] <- keys_list$foreign
}
}
}
On another note, I think using lapply like this is missing the point of lapply. A loop could actually be easier to read and we wouldn't have to consider return(NULL)s.
| #' @rdname teal_data-class | ||
| #' | ||
| #' @slot env (`environment`) environment containing data sets and possibly auxiliary variables | ||
| #' Access variables with [get_var()] or [`[[`]. |
There was a problem hiding this comment.
| #' Access variables with [get_var()] or [`[[`]. | |
| #' Access variables with [`get_var`] or [`[[`]. |
There was a problem hiding this comment.
what is wrong with [get_var()]
There was a problem hiding this comment.
It's a function call, usually docs mention function names, and in code format.
| #' Access with [get_warnings()]. | ||
| #' @slot messages (`character`) messages raised when evaluating code. | ||
| #' @slot join_keys (`JoinKeys`) object specifying joining keys for data sets in `@env`. | ||
| #' Access or modify with [get_join_keys()]. |
There was a problem hiding this comment.
| #' Access or modify with [get_join_keys()]. | |
| #' Access or modify with [`get_join_keys`]. |
There was a problem hiding this comment.
+1 for using () in function names
There was a problem hiding this comment.
The distinction between topic and function seems unclear to me. Is a function NOT a topic? What is a topic then?
Here's my view:
The docs should read "Change datanames slot with datanames function."
One changes the slot with datanames<object> <- <new_datanames> not with datanames(). If you insist on the latter, you lose consistency with [[.
Look at help pages for environment, get, assign, etc. Hardly a foo() in sight.
There was a problem hiding this comment.
Plus I always thought that table is an example, not a directive.
There was a problem hiding this comment.
Ok, so a topic would be a help page and one can be dedicated to a concept or a function or a group of functions or anything in between. Correct?
| #' Access or modify with [get_join_keys()]. | ||
| #' @slot datanames (`character`) vector of names of data sets in `@env`. | ||
| #' Used internally to distinguish them from auxiliary variables. | ||
| #' Access or modify with [datanames()]. |
There was a problem hiding this comment.
| #' Access or modify with [datanames()]. | |
| #' Access or modify with [`datanames`]. |
There was a problem hiding this comment.
I see, datanames is a topic
using [fun()] in roxygen2 docs
initialize empty teal_data


Part of:
Closes:
new_teal_dataarguments #169Other PRs:
install
teal_data class
teal_dataclass (teal_data-class.R) is en extension ofqenv.teal_datacontains extra slotsdatanamesandjoin_keys. Besides, allteal.codemethods works withteal_dataobject (eval_code, print etc.)TealData$serverreturnsteal_datanow - to have single object type in teal (not inteal_module)cdisc_data()andteal_data()returnTealDataorteal_data