Skip to content

Commit

Permalink
479 add renv::snapshot() for .lockfile with future + `shiny::Ex…
Browse files Browse the repository at this point in the history
…tendedTask` (#1232)

Fixes insightsengineering/coredev-tasks#479
Alternative for #1224

This PR includes new imports
- `{renv}` 
- so that we can use a function that creates a `.lockfile` for future
reproducibility
- `{future}` 
- so that we can run `renv::snapshot()` as a `shiny::ExtendedTask()$new`
in a parallel process to the shiny session

Tested with

```r
# pass your own .lockfile
# options(teal.renv.lockfile = "session.lock")

# allow to create the .lockfile based on DESCRIPTION file
# renv::settings$snapshot.type("explicit")

app <- init(
  data = teal_data(iris = iris),
  modules = example_module(label = "example teal module")
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}
```

---------

Signed-off-by: Marcin <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: kartikeya kirar <[email protected]>
Co-authored-by: André Veríssimo <[email protected]>
Co-authored-by: Pawel Rucki <[email protected]>
  • Loading branch information
6 people authored Jun 20, 2024
1 parent 7da5d29 commit 04825a1
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
additional-env-vars: |
_R_CHECK_CRAN_INCOMING_REMOTE_=false
TESTING_DEPTH=5
_R_CHECK_EXAMPLE_TIMING_THRESHOLD_=6
additional-r-cmd-check-params: --as-cran
enforce-note-blocklist: true
note-blocklist: |
Expand All @@ -47,6 +48,8 @@ jobs:
secrets:
REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }}
with:
additional-env-vars: |
_R_CHECK_EXAMPLE_TIMING_THRESHOLD_=6
enforce-note-blocklist: true
publish-unit-test-report-gh-pages: false
junit-xml-comparison: false
Expand Down
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ repos:
- davidgohel/flextable # Error: package 'flextable' is not available
- davidgohel/gdtools # for flextable
- checkmate
- future
- jsonlite
- lifecycle
- logger
- magrittr
- methods
- promises
- renv
- rlang
- shiny
- shinyjs
Expand Down
10 changes: 7 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,19 @@ URL: https://insightsengineering.github.io/teal/,
BugReports: https://github.com/insightsengineering/teal/issues
Depends:
R (>= 4.0),
shiny (>= 1.7.0),
shiny (>= 1.8.1),
teal.data (>= 0.5.0),
teal.slice (>= 0.5.0)
Imports:
checkmate (>= 2.1.0),
future (>= 1.33.2),
jsonlite,
lifecycle (>= 0.2.0),
logger (>= 0.2.0),
magrittr (>= 1.5),
methods,
promises (>= 1.3.0),
renv (>= 1.0.7),
rlang (>= 1.0.0),
shinyjs,
stats,
Expand All @@ -71,8 +74,8 @@ VignetteBuilder:
RdMacros:
lifecycle
Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data,
insightsengineering/teal.slice, mllg/checkmate, jeroen/jsonlite,
r-lib/lifecycle, daroczig/logger, tidyverse/magrittr, r-lib/rlang,
insightsengineering/teal.slice, mllg/checkmate, HenrikBengtsson/future, jeroen/jsonlite,
r-lib/lifecycle, daroczig/logger, tidyverse/magrittr, rstudio/promises, rstudio/renv, r-lib/rlang,
daattali/shinyjs, insightsengineering/teal.logger,
insightsengineering/teal.reporter, insightsengineering/teal.widgets,
rstudio/bslib, yihui/knitr, bioc::MultiAssayExperiment, r-lib/R6,
Expand Down Expand Up @@ -108,6 +111,7 @@ Collate:
'teal_data_module.R'
'teal_data_module-eval_code.R'
'teal_data_module-within.R'
'teal_lockfile.R'
'teal_reporter.R'
'teal_slices-store.R'
'teal_slices.R'
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
### Enhancement
* Provided progress bar for modules loading and data filtering during teal app startup.

### New features

* Possibility to download lockfile to restore app session for reproducibility.

### Miscellaneous
* Filter mapping display is no longer coupled to the snapshot manager.

Expand Down
3 changes: 3 additions & 0 deletions R/init.R
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ init <- function(data,
# log
teal.logger::log_system_info()

# invoke lockfile creation
teal_lockfile()

# argument transformations
## `modules` - landing module
landing <- extract_module(modules, "teal_module_landing")
Expand Down
4 changes: 4 additions & 0 deletions R/module_teal.R
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ ui_teal <- function(id,
tags$div(
footer,
teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"),
br(),
downloadLink(ns("lockFile"), "Download .lock file"),
textOutput(ns("identifier"))
)
)
Expand All @@ -134,6 +136,8 @@ srv_teal <- function(id, modules, teal_data_rv, filter = teal_slices()) {
title = "SessionInfo"
)

output$lockFile <- teal_lockfile_downloadhandler()

# `JavaScript` code
run_js_files(files = "init.js")

Expand Down
9 changes: 7 additions & 2 deletions R/module_teal_with_splash.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#' This is necessary because the filter panel and modules depend on the data to initialize.
#'
#' `teal_with_splash` follows the `shiny` module convention.
#' [`init()`] is a wrapper around this that assumes that `teal` it is
#' [`init()`] is a wrapper around this that assumes that `teal` is
#' the top-level module and cannot be embedded.
#'
#' Note: It is no longer recommended to embed `teal` in `shiny` apps as a module.
Expand All @@ -26,6 +26,12 @@
#' will be displayed in the `teal` application. See [modules()] and [module()] for
#' more details.
#' @inheritParams shiny::moduleServer
#'
#' @section Reproducibility:
#' Reproducibility is supported by multiple features. `teal` includes a `utils::sessioInfo()` output to allow to compare
#' packages used in the session. It also allows to create `renv` lockfile to support project setup reproducibility.
#' For more information about lockfile creation visit [`teal_lockfile()`].
#'
#' @return
#' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not.
#' @name module_teal_with_splash
Expand Down Expand Up @@ -198,7 +204,6 @@ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) {
NULL
})


res <- srv_teal(id = "teal", modules = modules, teal_data_rv = teal_data_rv_validate, filter = filter)
logger::log_trace("srv_teal_with_splash initialized module with data.")

Expand Down
114 changes: 114 additions & 0 deletions R/teal_lockfile.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#' Generate lockfile for application reproducibility
#'
#' This function is invoked during [teal::init] to create `renv`-compatible lockfile for use within the application.
#'
#' The function leverages [renv::snapshot()], which offers multiple methods for lockfile creation.
#'
#' - User-specified:
#' - **Pre-computed lockfile**: Users can provide their own pre-computed lockfile by specifying the path via
#' `teal.renv.lockfile` option. Automatic lockfile computation is skipped in such case.
#' - Automatically computed:
#' - **Working directory lockfile**: If `teal.renv.lockfile` is not set, `teal` will, by default, create an
#' `implicit` type lockfile that uses `renv::dependencies()` to detect all R packages in the current project's
#' working directory.
#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working
#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows
#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the
#' `DESCRIPTION` fields included in the lockfile.
#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set
#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option.
#'
#' @section lockfile usage:
#' After creating the lockfile, you can restore the application environment using `renv::restore()`.
#'
#' @seealso [renv::snapshot()], [renv::restore()].
#'
#' @return Nothing. This function is executed for its side effect of creating a lockfile used in the `teal` application.
#'
#' @keywords internal
teal_lockfile <- function() {
lockfile_path <- "teal_app.lock"
# If user has setup the file, there is no need to compute a new one.
user_lockfile <- getOption("teal.renv.lockfile", "")
if (!identical(user_lockfile, "")) {
if (file.exists(user_lockfile)) {
file.copy(user_lockfile, lockfile_path)
return(invisible(NULL))
} else {
stop("lockfile provided through options('teal.renv.lockfile') does not exist.")
}
}

if (!(is_in_test() || is_r_cmd_check())) {
old_plan <- future::plan()
# If there is already a parallel (non-sequential) backend, reuse it.
if (inherits(old_plan, "sequential")) {
future::plan(future::multisession, workers = 2)
}

lockfile_task <- ExtendedTask$new(create_renv_lockfile)
lockfile_task$invoke(close = inherits(old_plan, "sequential"), lockfile_path)
logger::log_trace("lockfile creation invoked.")
}
}

create_renv_lockfile <- function(close = FALSE, lockfile_path = NULL) {
checkmate::assert_flag(close)
checkmate::assert_string(lockfile_path, na.ok = TRUE)
promise <- promises::future_promise({
# Below we can not use a file created in tempdir() directory.
# If a file is created in tempdir(), it gets deleted on 'then(onFulfilled' part.
shiny::onStop(function() file.remove(lockfile_path))

renv_logs <- utils::capture.output(
renv::snapshot(
lockfile = lockfile_path,
prompt = FALSE,
force = TRUE
# type = is taken from renv::settings$snapshot.type()
)
)
if (any(grepl("Lockfile written", renv_logs))) {
logger::log_trace("lockfile created successfully.")
} else {
logger::log_trace("lockfile created with issues.")
}

lockfile_path
})
if (close) {
# If the initial backend was only sequential, bring it back.
promises::then(promise, onFulfilled = function() {
future::plan(future::sequential)
})
}
promise
}

teal_lockfile_downloadhandler <- function() {
downloadHandler(
filename = function() {
"renv.lock"
},
content = function(file) {
teal_lockfile <- "teal_app.lock"
iter <- 1
while (!file.exists(teal_lockfile) && iter <= 100) {
logger::log_trace("lockfile not created yet, retrying...")
Sys.sleep(0.25)
iter <- iter + 1 # max wait time is 25 seconds
}
file.copy(teal_lockfile, file)
file
},
contentType = "application/json"
)
}

is_r_cmd_check <- function() {
("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv()))
}

is_in_test <- function() {
identical(Sys.getenv("TESTTHAT"), "true")
}
1 change: 1 addition & 0 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ tabsetted
themer
theming
uncheck
lockfile
9 changes: 8 additions & 1 deletion man/module_teal_with_splash.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions man/teal_lockfile.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions tests/testthat/test-utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,14 @@ testthat::test_that("defunction recursively goes down a list", {
y
)
})

testthat::test_that("create_renv_lockfile creates a lock file during the execution", {
old_plan <- future::plan(future::sequential)
withr::defer(future::plan(old_plan))

renv_file_name <- "teal_app.lock"
withr::defer(file.remove(renv_file_name))
promise <- create_renv_lockfile(TRUE, renv_file_name)

testthat::expect_true(file.exists(renv_file_name))
})
4 changes: 4 additions & 0 deletions vignettes/teal-options.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ This indicates whether to print the `JavaScript` console logs to the `R` console

Default: `FALSE`.

### `teal.renv.lockfile` (`character`)

The path to the pre-computed `renv` lockfile that will be shared through teal app. To read more about lockfile usage creation check `?teal::teal_lockfile`.


# Deprecated options

Expand Down

0 comments on commit 04825a1

Please sign in to comment.