Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce the new DDL (ddl class) #926

Closed
wants to merge 55 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5c17ccf
following new teal_data class
gogonzo Oct 4, 2023
d743513
ddl
gogonzo Oct 4, 2023
4039648
fix
gogonzo Oct 4, 2023
4051ded
reverting breaking changes and supporting teal_data
gogonzo Oct 5, 2023
36fc0fb
reverting breaking changes and supporting teal_data
gogonzo Oct 5, 2023
b97f63e
filtered data constructor in teal internals
gogonzo Oct 6, 2023
ccbbe67
new_teal_data env to data
gogonzo Oct 6, 2023
c3eab5a
filtered data constructor in teal internals
gogonzo Oct 6, 2023
8164671
new_teal_data env to data
gogonzo Oct 6, 2023
354a79d
[skip actions] Roxygen Man Pages Auto Update
dependabot-preview[bot] Oct 6, 2023
0185fb7
Merge branch 'main' into teal_data@main
gogonzo Oct 17, 2023
0d065e1
Merge branch 'main' into teal_data@main
gogonzo Oct 17, 2023
09d1bd4
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 17, 2023
43d1dc3
modifyList instead of a loop
gogonzo Oct 18, 2023
752c239
impossible to not create teal_data when using new_teal_data constructor
gogonzo Oct 18, 2023
34bad8c
fix tests
gogonzo Oct 19, 2023
e8c9d86
Merge remote-tracking branch 'origin/main' into teal_data@main
gogonzo Oct 19, 2023
8b3a082
Merge remote-tracking branch 'origin/main' into teal_data@main
gogonzo Oct 23, 2023
bc52e12
Merge remote-tracking branch 'origin/teal_data@main' into ddl@teal_da…
gogonzo Oct 23, 2023
47d0a2f
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 23, 2023
fc83084
fix 1 after merge
gogonzo Oct 23, 2023
2f11384
fix 2 after merge
gogonzo Oct 23, 2023
6d3d5f0
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 23, 2023
ec575f9
teal_data instead of new_teal_data
gogonzo Oct 23, 2023
9cd52ba
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 24, 2023
0df1711
suggestion from @chlebowa
gogonzo Oct 24, 2023
37c42e2
wip
gogonzo Oct 25, 2023
a256bbd
Merge remote-tracking branch 'origin/main' into teal_data@main
gogonzo Oct 25, 2023
d2e8802
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 25, 2023
c1cf841
remove generic get_join_keys (duplicated with teal.data)
gogonzo Oct 25, 2023
29d171c
add docs
gogonzo Oct 25, 2023
62cb664
Merge 29d171cd631a655cd53df650810987ec9321a360 into 8b2653dc1b7f67120…
gogonzo Oct 25, 2023
a035f84
[skip actions] Restyle files
github-actions[bot] Oct 25, 2023
c7ce543
fix hashing of ddl
gogonzo Oct 25, 2023
df2e3ee
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 25, 2023
b0451b2
fix checks
gogonzo Oct 25, 2023
96f8b1f
Merge branch 'main' into teal_data@main
gogonzo Oct 26, 2023
1e061f1
fix pkgdown
gogonzo Oct 26, 2023
56424e9
fix pkgdown
gogonzo Oct 27, 2023
6ec2c22
resolve_modules_datanames to utils.R
gogonzo Oct 27, 2023
77d9702
Merge 6ec2c22d9c6bc559f5aa4f8bb5e1019afa41c00c into a3cbbe13498c8a9ab…
gogonzo Oct 27, 2023
45d2985
[skip actions] Restyle files
github-actions[bot] Oct 27, 2023
1a6e11d
rerun
gogonzo Oct 27, 2023
e5ef022
fix spelling
gogonzo Oct 27, 2023
c593734
skipping lint of a long function
gogonzo Oct 27, 2023
2b13b23
addressing old comments
gogonzo Oct 27, 2023
f3d2b37
Merge branch 'teal_data@main' into ddl@teal_data@main
gogonzo Oct 29, 2023
716d5c5
- export ddl_run
gogonzo Oct 30, 2023
b591476
Update docs for `ddl@teal_data@main` (#948)
m7pr Oct 30, 2023
16f5ad5
@teal_data@main:
gogonzo Oct 30, 2023
2422903
fix
gogonzo Oct 30, 2023
c334a76
Merge remote-tracking branch 'origin/main' into ddl@teal_data@main
gogonzo Oct 30, 2023
3eaf0b6
fix namespace open_conn
gogonzo Oct 30, 2023
e421874
tidyup code and docs
gogonzo Oct 31, 2023
90c6f1f
adding regular expression to parse some edge cases. (#951)
kartikeyakirar Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
Collate:
'ddl-class.R'
'ddl-modules.R'
'ddl-run.R'
'dummy_functions.R'
'get_rcode_utils.R'
'include_css_js.R'
Expand Down
8 changes: 8 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

S3method(c,teal_slices)
S3method(get_code,tdata)
S3method(get_dataname,ddl)
S3method(get_join_keys,ddl)
S3method(get_join_keys,tdata)
S3method(get_metadata,default)
S3method(get_metadata,tdata)
Expand All @@ -18,6 +20,9 @@ S3method(ui_nested_tabs,teal_modules)
export("%>%")
export(TealReportCard)
export(as.teal_slices)
export(close_conn)
export(ddl)
kartikeyakirar marked this conversation as resolved.
Show resolved Hide resolved
export(ddl_run)
export(example_module)
export(get_code_tdata)
export(get_metadata)
Expand All @@ -26,10 +31,13 @@ export(landing_popup_module)
export(module)
export(modules)
export(new_tdata)
export(open_conn)
export(report_card_template)
export(reporter_previewer_module)
export(show_rcode_modal)
export(srv_teal_with_splash)
export(submit_button_server)
export(submit_button_ui)
export(tdata2env)
export(teal_slices)
export(ui_teal_with_splash)
Expand Down
143 changes: 143 additions & 0 deletions R/ddl-class.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#' DDL object
#'
#' Object to execute custom DDL code in the shiny session.
#'
#' @section Creating reproducible data:
#' `ddl` object can be used to create reproducible data in the shiny session. `ddl$server` function
#' can execute any R code and return [`teal.data::teal_data-class`]. For reproducibility purposes,
#' we recommend to initialize empty `teal_data` object and evaluate necessary code with `eval_code` or `within`.
#' ```r
#' function(id, ...) {
#' moduleServer(id, function(input, output, session) {
#' eventReactive(input$submit, {
#' data <- teal_data() |> within({
#' # code to be run when app user presses submit
#' })
#' })
#' })
#' }
#' ```
#' Obtained data is passed further in the `teal` app with `code` which can be used to recreate the objects.
#'
#' @section Code masking:
#' `ddl` object can be used in a way that evaluated code is different than the code
#' returned in `teal_data` object. Typically occurs when app user is asked to input a
#' password and we'd like to skip this input in the reproducible code. Possibly, users password
#' could be substituted with `askpass::askpass()` call, so the returned code is still executable but secure.
#' `ddl` developer must understand that this is a security risk and should be handled with care.
#' To make sure that the code is reproducible, `ddl` object should be used with `input_mask` argument.
#' `teal` provides convenience function [ddl_run()] which handles evaluation of the code, masking
#' and creating `teal_data` object. Such `server` function could look like this:
#'
#' ```
#' server = function(id, ...) {
#' moduleServer(id, function(input, output, session) {
#' reactive({
#' ddl_run(input = input, ...)
#' })
#' })
#' }
#' ```
#'
#' If `ddl` developer values more control, then might be interested in using `...` explicitly,
#' and create `teal_data` object manually.
#'
#' @param ui (`shiny.tag`)\cr
#' `shiny` user-interface module containing inputs whose `id` correspond to the arguments in the `code`.
#'
#' @param server (`function`)\cr
#' `shiny` server module [`teal.data::teal_data-class`] possibly wrapped in a [reactive()].
#' `server` function should have `id` and `...` as formals. Where:
#' - `id` is a `shiny` module id, and
#' - `...` passes arguments from the `ddl` object (`code`, `input_mask`, `datanames`, `join_keys`).
#' See section `Code masking`.
#'
#' @param expr (optional `expression`)\cr
#' Syntactically valid R expression to be executed in the shiny session.
#' Shouldn't be specified when `code` is specified.
#'
#' @param code (optional `character` or `language`)\cr
#' Object containing (defused) syntactically valid R expression to be executed in the shiny session.
#' Shouldn't be specified when `expr` is specified.
#'
#' @param input_mask (optional `named list`)\cr
#' arguments to be substituted in the `code`. These (named) list elements are going to replace
#' symbols in the code prefixed with `input$` or `input[["`. Typically `input_mask` is used
#' to mask username or password with `list(password = quote(askpass::askpass()))`.
#' See section `code masking` for more details.
#'
#' @param datanames (optional `character`)\cr
#' Names of the datasets created by evaluation of the `code`. By default, `datanames`
#' are obtained from the `join_keys` or from results of the `code` evaluation.
#' If `code` evaluation creates objects which are not considered as datasets, they
#' should be omitted from `datanames` to avoid errors.
#'
#' @inheritParams teal.data::teal_data
#'
#' @export
ddl <- function(expr,
code,
input_mask = list(),
ui = submit_button_ui,
server = submit_button_server,
join_keys = teal.data::join_keys(),
datanames = names(join_keys$get())) {
checkmate::assert_list(input_mask)
checkmate::check_function(ui, args = "id")
checkmate::check_function(server, args = c("id", "..."))
checkmate::check_class(join_keys, "JoinKeys")
checkmate::check_character(datanames, min.len = 1)

out <- structure(
list(ui = ui, server = server),
input_mask = input_mask,
datanames = datanames,
join_keys = join_keys,
class = "ddl"
)

if (!missing(expr) || !missing(code)) {
# this is intended to be used with input mask
# but in the same time we can't forbid user to use it
# without input_mask. Some users might prefer to use ddl_run
# to automaticaly handle their code.
# Q: can NEST bear responsibility for reproducibility of the masked code?
if (!missing(expr)) {
code <- substitute(expr)
}
if (is.character(code)) {
code <- parse(text = code)
}
attr(out, "code") <- code
}

out
}

# methods from teal.data ----
# to be removed soon

#' Get data names from `ddl`
#' @rdname get_dataname
#' @param x (`ddl`) object
#' @export
get_dataname.ddl <- function(x) {
attr(x, "datanames")
}

#' @rdname get_join_keys
#' @export
get_join_keys.ddl <- function(data) {
attr(data, "join_keys")
}

# todo: to remove before merge -------------
#' @export
open_conn <- function(username, password) {
if (password != "pass") stop("Invalid credentials. 'pass' is the password") else TRUE
}
#' @export
close_conn <- function(conn) {
message("closed")
return(NULL)
}
59 changes: 59 additions & 0 deletions R/ddl-modules.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#' Run code and mask inputs
#'
#' Delayed Data Loading module with login and password input.
#'
#' @name submit_button_module
#'
#'
#' @param id (`character`) `shiny` module id.
#' @param ... (`list`) arguments passed to `ddl_run` function.
#' @return `shiny` module
NULL

#' @rdname submit_button_module
#' @export
submit_button_ui <- function(id) {
ns <- NS(id)
actionButton(inputId = ns("submit"), label = "Submit")
}

#' @rdname submit_button_module
#' @export
submit_button_server <- function(id, ...) {
moduleServer(id, function(input, output, session) {
tdata <- eventReactive(input$submit, {
ddl_run(input = input, ...)
})

# would need to make sure we handle reactivity correctly here as teal::init expects not reactive teal_data...
return(tdata)
})
}
Comment on lines +15 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I right to assume that right now they are not complete and we will implement the shiny module such that the UI disappears after clicking the submit button?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More or less, yes.


#' Wrapper for `ui` and `server` in `ddl` object
#'
#' Convenience wrapper for `ui` and `server` functions in `ddl` object.
#' On the `server` side, function calls `shiny` module and adjusts the arguments
#' to the formals of the `server` function.
#' @param id (`character`) `shiny` module id.
#' @param x (`ddl`) object.
#' @name ddl_module
#' @return `shiny` module
#' @keywords internal
NULL

#' @rdname ddl_module
#' @keywords internal
ddl_server <- function(id, x) {
# subset attributes to only those that are arguments of the server function
args <- names(formals(x$server))
attrs <- attributes(x)
attrs <- attrs[setdiff(names(attrs), c("id", "class", "names"))]
do.call(x$server, c(list(id = id), attrs))
}

#' @rdname ddl_module
#' @keywords internal
ddl_ui <- function(id, x) {
x$ui(id = id)
}
82 changes: 82 additions & 0 deletions R/ddl-run.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#' Run code and mask inputs
#'
#' Function runs the `code`, masks the `code` and creates `teal_data` object.
#' @param input (`list`) containing inputs to be used in the `code`
#' @param code (`language`) code to be executed
#' @param input_mask (`list`) containing inputs to be masked in the `code`
#' @param datanames (`character`) names of the objects to be created from the code evaluation
#' @param join_keys (`join_keys`) object
#'
#' @return `teal_data` object
#'
#' @export
ddl_run <- function(input = list(),
code,
input_mask = list(),
join_keys = teal.data::join_keys(),
datanames = names(join_keys$get())) {
checkmate::assert_list(input)
if (inherits(input, "reactivevalues")) {
input <- shiny::reactiveValuesToList(input)
}
data <- teal_data(join_keys = join_keys)

# evaluate code and substitute input
data <- teal.code::eval_code(data, .substitute_inputs(code, args = input))

if (identical(ls(data@env), character(0))) {
warning(
"Evaluation of `ddl` code haven't created any objects.\n",
"Please make sure that the code is syntactically correct and creates necessary data."
)
}

if (!missing(input_mask)) {
# mask dynamic inputs with mask
input <- utils::modifyList(input, input_mask)

# replace code of teal_data with masked code
# question: warnings and errors are not masked, is it ok?
data@code <- format_expression(.substitute_inputs(code, args = input))
}

if (length(datanames)) {
datanames(data) <- datanames
}
if (length(datanames(data)) == 0) {
datanames(data) <- ls(data@env)
}

data
}

#' substitute inputs in the code
#'
#' Function replaces symbols in the provided code prefixed with `input$` or `input[["`
#' by values of the `args` argument.
#'
#' @param code (`language`) code to substitute
#' @param args (`list`) named list or arguments
.substitute_inputs <- function(code, args) {
code <- if (identical(as.list(code)[[1L]], as.symbol("{"))) {
as.list(code)[-1L]
} else {
code
}

code_strings <- vapply(code, deparse1, character(1L))
# Replace input$ with .()
code_strings <- gsub("input\\$(\\w+\\.?\\w*)", "\\.(\\1)", code_strings)
code_strings <- gsub("(input\\$)(`[^`]+`)", "\\.(\\2)", code_strings)

# Replace input[[ with .()
code_strings <- gsub("(input\\[\\[\")(\\w+\\.?\\w*)(\"\\]\\])", "\\.(\\2\\)", code_strings)
code_strings <- gsub("(input\\[\\[\")(\\w+\\-\\w+)\"\\]\\]", ".(`\\2`)", code_strings)
Comment on lines +68 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Replace input$ with .()
code_strings <- gsub("input\\$(\\w+\\.?\\w*)", "\\.(\\1)", code_strings)
code_strings <- gsub("(input\\$)(`[^`]+`)", "\\.(\\2)", code_strings)
# Replace input[[ with .()
code_strings <- gsub("(input\\[\\[\")(\\w+\\.?\\w*)(\"\\]\\])", "\\.(\\2\\)", code_strings)
code_strings <- gsub("(input\\[\\[\")(\\w+\\-\\w+)\"\\]\\]", ".(`\\2`)", code_strings)
code_strings <- gsub("(input\\$)([a-z0-9_.`-]+)", "\\.(\\2\\)", code_strings)
code_strings <- gsub("(input\\[\\[\")([a-z0-9_.`-]+)(\"\\]\\])", "\\.(\\2\\)", code_strings)
code_strings <- gsub("\\.\\(([^)`]*?)-([^)`]*?)\\)", ".(`\\1-\\2`)", code_strings)

input[["var-name"]] are converted .(var_name), addtional regaular expression add backticks around variable names to to ensure they are valid for substitution in R.


# Use bquote to obtain code with input values and masking values.
as.expression(
lapply(code_strings, function(x) {
do.call(bquote, list(str2lang(x), list2env(args)))
})
)
}
5 changes: 2 additions & 3 deletions R/init.R
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,10 @@ init <- function(data,
footer = tags$p(),
id = character(0)) {
logger::log_trace("init initializing teal app with: data ({ class(data)[1] }).")

if (!inherits(data, c("TealData", "teal_data"))) {
if (!inherits(data, c("TealData", "teal_data", "ddl"))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we now allow the data argument of the teal::init() to take up a ddl class I think this should be added to the roxygen comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! just saw that we're not looking for doc suggestions at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to roxygen argument description should follow this change

data <- teal.data::to_relational_data(data = data)
}
checkmate::assert_multi_class(data, c("TealData", "teal_data"))
checkmate::assert_multi_class(data, c("TealData", "teal_data", "ddl"))
checkmate::assert_multi_class(modules, c("teal_module", "list", "teal_modules"))
checkmate::assert_string(title, null.ok = TRUE)
checkmate::assert(
Expand Down
8 changes: 6 additions & 2 deletions R/module_teal_with_splash.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ui_teal_with_splash <- function(id,
title,
header = tags$p("Add Title Here"),
footer = tags$p("Add Footer Here")) {
checkmate::assert_multi_class(data, c("TealDataAbstract", "teal_data"))
checkmate::assert_multi_class(data, c("TealDataAbstract", "teal_data", "ddl"))
ns <- NS(id)

# Startup splash screen for delayed loading
Expand All @@ -31,6 +31,8 @@ ui_teal_with_splash <- function(id,
# Shiny app does not time out.
splash_ui <- if (inherits(data, "teal_data")) {
div()
} else if (inherits(data, "ddl")) {
ddl_ui(id = ns("startapp_module"), x = data)
} else if (inherits(data, "TealDataAbstract") && teal.data::is_pulled(data)) {
div()
} else {
Expand All @@ -56,7 +58,7 @@ ui_teal_with_splash <- function(id,
#' If data is not loaded yet, `reactive` returns `NULL`.
#' @export
srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) {
checkmate::assert_multi_class(data, c("TealDataAbstract", "teal_data"))
checkmate::assert_multi_class(data, c("TealDataAbstract", "teal_data", "ddl"))
moduleServer(id, function(input, output, session) {
logger::log_trace("srv_teal_with_splash initializing module with data { toString(get_dataname(data))}.")

Expand All @@ -68,6 +70,8 @@ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) {
# either passed to teal::init or returned from ddl
raw_data <- if (inherits(data, "teal_data")) {
reactiveVal(data)
} else if (inherits(data, "ddl")) {
ddl_server(id = "startapp_module", x = data)
} else if (inherits(data, "TealDataAbstract") && teal.data::is_pulled(data)) {
new_data <- do.call(
teal.data::teal_data,
Expand Down
3 changes: 3 additions & 0 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice")
coalesce_r <- getFromNamespace("coalesce_r", "teal.slice")
# all *Block objects are private in teal.reporter
RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint

# use non-exported function from teal.code
format_expression <- getFromNamespace("format_expression", "teal.code")
Loading
Loading