Title: | Export Domain Logic from Shiny using Meta-Programming |
---|---|
Description: | Provides tools for capturing logic in a Shiny app and exposing it as code that can be run outside of Shiny (e.g., from an R console). It also provides tools for bundling both the code and results to the end user. |
Authors: | Joe Cheng [aut], Carson Sievert [cre, aut] , RStudio [cph] |
Maintainer: | Carson Sievert <[email protected]> |
License: | GPL-3 |
Version: | 0.2.1 |
Built: | 2024-09-21 04:23:09 UTC |
Source: | https://github.com/rstudio/shinymeta |
Produce a zip bundle of code and results
buildScriptBundle( code = NULL, output_zip_path, script_name = "script.R", include_files = list(), render = TRUE, render_args = list() ) buildRmdBundle( report_template, output_zip_path, vars = list(), include_files = list(), render = TRUE, render_args = list() )
buildScriptBundle( code = NULL, output_zip_path, script_name = "script.R", include_files = list(), render = TRUE, render_args = list() ) buildRmdBundle( report_template, output_zip_path, vars = list(), include_files = list(), render = TRUE, render_args = list() )
code |
A language object. |
output_zip_path |
A filename for the resulting zip bundle. |
script_name |
A name for the R script in the zip bundle. |
include_files |
A named list consisting of additional files that should be included in the zip bundle. The element names indicate the destination path within the bundle, specified as a relative path; the element values indicate the path to the actual file currently on disk, specified as either a relative or absolute path. |
render |
Whether or not to call |
render_args |
Arguments to provide to |
report_template |
Filename of an Rmd template to be expanded by |
vars |
A named list of variables passed along to |
The path to a generated file.
knitr::knit_expand
Show a shinyAce::aceEditor()
in a shiny::modalDialog()
.
displayCodeModal( code, title = NULL, clip = "clipboard", footer = shiny::modalButton("Dismiss"), size = c("m", "s", "l"), easyClose = TRUE, fade = TRUE, session = shiny::getDefaultReactiveDomain(), ... )
displayCodeModal( code, title = NULL, clip = "clipboard", footer = shiny::modalButton("Dismiss"), size = c("m", "s", "l"), easyClose = TRUE, fade = TRUE, session = shiny::getDefaultReactiveDomain(), ... )
code |
Either a language object or a character string. |
title |
An optional title for the dialog. |
clip |
An |
footer |
UI for footer. Use |
size |
One of |
easyClose |
If |
fade |
If |
session |
a shiny session object (the default should almost always be used). |
... |
arguments passed along to |
nothing. Call this function for its side effects.
if (interactive()) { library(shiny) ui <- fluidPage( sliderInput("n", label = "Number of samples", min = 10, max = 100, value = 30), actionButton("code", icon("code")), plotOutput("p") ) server <- function(input, output) { output$p <- metaRender(renderPlot, { plot(sample(..(input$n))) }) observeEvent(input$code, { code <- expandChain(output$p()) displayCodeModal(code) }) } shinyApp(ui, server) }
if (interactive()) { library(shiny) ui <- fluidPage( sliderInput("n", label = "Number of samples", min = 10, max = 100, value = 30), actionButton("code", icon("code")), plotOutput("p") ) server <- function(input, output) { output$p <- metaRender(renderPlot, { plot(sample(..(input$n))) }) observeEvent(input$code, { code <- expandChain(output$p()) displayCodeModal(code) }) } shinyApp(ui, server) }
Use expandChain
to write code out of one or more metaReactive objects.
Each meta-reactive object (expression, observer, or renderer) will cause not
only its own code to be written, but that of its dependencies as well.
newExpansionContext() expandChain(..., .expansionContext = newExpansionContext())
newExpansionContext() expandChain(..., .expansionContext = newExpansionContext())
... |
All arguments must be unnamed, and must be one of: 1) calls to
meta-reactive objects, 2) comment string (e.g. |
.expansionContext |
Accept the default value if calling |
There are two ways to extract code from meta objects (i.e. metaReactive()
,
metaObserve()
, and metaRender()
): withMetaMode()
and expandChain()
.
The simplest is withMetaMode(obj())
, which crawls the tree of meta-reactive
dependencies and expands each ..()
in place.
For example, consider these meta objects:
nums <- metaReactive({ runif(100) }) obs <- metaObserve({ summary(..(nums())) hist(..(nums())) })
When code is extracted using withMetaMode
:
withMetaMode(obs())
The result looks like this:
summary(runif(100)) plot(runif(100))
Notice how runif(100)
is inlined wherever ..(nums())
appears, which is not desirable if we wish to reuse the same
values for summary()
and plot()
.
The expandChain
function helps us workaround this issue
by assigning return values of metaReactive()
expressions to
a name, then replaces relevant expansion (e.g., ..(nums())
)
with the appropriate name (e.g. nums
).
expandChain(obs())
The result looks like this:
nums <- runif(100) summary(nums) plot(nums)
You can pass multiple meta objects and/or comments to expandChain
.
expandChain( "# Generate values", nums(), "# Summarize and plot", obs() )
Output:
# Load data nums <- runif(100) nums # Inspect data summary(nums) plot(nums)
You can suppress the printing of the nums
vector in the previous example by
wrapping the nums()
argument to expandChain()
with invisible(nums())
.
The return value of expandChain()
is a code object that's suitable for
printing or passing to displayCodeModal()
, buildScriptBundle()
, or
buildRmdBundle()
.
The return value of newExpansionContext
is an object that should be
passed to multiple expandChain()
calls.
expandChain()
callsSometimes we may have related meta objects that we want to generate code for, but we want the code for some objects in one code chunk, and the code for other objects in another code chunk; for example, you might be constructing an R Markdown report that has a specific place for each code chunk.
Within a single expandChain()
call, all metaReactive
objects are
guaranteed to only be declared once, even if they're declared on by multiple
meta objects; but since we're making two expandChain()
calls, we will end
up with duplicated code. To remove this duplication, we need the second
expandChain
call to know what code was emitted in the first expandChain
call.
We can achieve this by creating an "expansion context" and sharing it between the two calls.
exp_ctx <- newExpansionContext() chunk1 <- expandChain(.expansionContext = exp_ctx, invisible(nums()) ) chunk2 <- expandChain(.expansionContext = exp_ctx, obs() )
After this code is run, chunk1
contains only the definition of nums
and
chunk2
contains only the code for obs
.
metaReactive
objectsSometimes, when generating code, we want to completely replace the
implementation of a metaReactive
. For example, our Shiny app might contain
this logic, using shiny::fileInput()
:
data <- metaReactive2({ req(input$file_upload) metaExpr(read.csv(..(input$file_upload$datapath))) }) obs <- metaObserve({ summary(..(data())) })
Shiny's file input works by saving uploading files to a temp directory. The
file referred to by input$file_upload$datapath
won't be available when
another user tries to run the generated code.
You can use the expansion context object to swap out the implementation of
data
, or any other metaReactive
:
ec <- newExpansionContext() ec$substituteMetaReactive(data, function() { metaExpr(read.csv("data.csv")) }) expandChain(.expansionContext = ec, obs())
Result:
data <- read.csv("data.csv") summary(data)
Just make sure this code ends up in a script or Rmd bundle that includes the
uploaded file as data.csv
, and the user will be able to reproduce your
analysis.
The substituteMetaReactive
method takes two arguments: the metaReactive
object to substitute, and a function that takes zero arguments and returns a
quoted expression (for the nicest looking results, use metaExpr
to create
the expression). This function will be invoked the first time the
metaReactive
object is encountered (or if the metaReactive
is defined
with inline = TRUE
, then every time it is encountered).
https://rstudio.github.io/shinymeta/articles/code-generation.html
input <- list(dataset = "cars") # varname is only required if srcref aren't supported # (R CMD check disables them for some reason?) mr <- metaReactive({ get(..(input$dataset), "package:datasets") }) top <- metaReactive({ head(..(mr())) }) bottom <- metaReactive({ tail(..(mr())) }) obs <- metaObserve({ message("Top:") summary(..(top())) message("Bottom:") summary(..(bottom())) }) # Simple case expandChain(obs()) # Explicitly print top expandChain(top(), obs()) # Separate into two code chunks exp_ctx <- newExpansionContext() expandChain(.expansionContext = exp_ctx, invisible(top()), invisible(bottom())) expandChain(.expansionContext = exp_ctx, obs())
input <- list(dataset = "cars") # varname is only required if srcref aren't supported # (R CMD check disables them for some reason?) mr <- metaReactive({ get(..(input$dataset), "package:datasets") }) top <- metaReactive({ head(..(mr())) }) bottom <- metaReactive({ tail(..(mr())) }) obs <- metaObserve({ message("Top:") summary(..(top())) message("Bottom:") summary(..(bottom())) }) # Simple case expandChain(obs()) # Explicitly print top expandChain(top(), obs()) # Separate into two code chunks exp_ctx <- newExpansionContext() expandChain(.expansionContext = exp_ctx, invisible(top()), invisible(bottom())) expandChain(.expansionContext = exp_ctx, obs())
Turn unevaluated shinymeta expressions into (formatted or styled) text.
formatCode(code, width = 500L, formatter = styleText, ...) styleText(code, ...) deparseCode(code, width = 500L)
formatCode(code, width = 500L, formatter = styleText, ...) styleText(code, ...) deparseCode(code, width = 500L)
code |
Either an unevaluated expression or a deparsed code string. |
width |
The |
formatter |
a function that accepts deparsed code (a character string) as the first argument. |
... |
arguments passed along to the |
Before any formatting takes place, the unevaluated expression is
deparsed into a string via deparseCode()
, which ensures that
shinymeta comment strings (i.e., literal strings that appear on their own line,
and begin with one or more #
characters.) are turned into comments and
superfluous \{
are removed. After deparsing, the formatCode()
function then
calls the formatter
function on the deparsed string to format (aka style) the code string.
The default formatter
, styleText()
, uses styler::style_text()
with a couple differences:
Pipe operators (%>%
) are always followed by a line break.
If the token appearing after a line-break is a comma/operator, the line-break is removed.
Single-element character vector with formatted code
options(shiny.suppressMissingContextError = TRUE) x <- metaReactive({ "# Here's a comment" sample(5) %>% sum() }) code <- expandChain(x()) deparseCode(code) formatCode(code) formatCode(code, formatter = styler::style_text)
options(shiny.suppressMissingContextError = TRUE) x <- metaReactive({ "# Here's a comment" sample(5) %>% sum() }) code <- expandChain(x()) deparseCode(code) formatCode(code) formatCode(code, formatter = styler::style_text)
Most apps start out with setup code that is non-reactive, such as
library()
calls, loading of static data into local
variables, or source
-ing of supplemental R scripts.
metaAction
provides a convenient way to run such code for its side effects
(including declaring new variables) while making it easy to export that code
using expandChain()
. Note that metaAction
executes code directly in the
env
environment (which defaults to the caller's environment), so any local
variables that are declared in the expr
will be available outside of
metaAction
as well.
metaAction(expr, env = parent.frame(), quoted = FALSE)
metaAction(expr, env = parent.frame(), quoted = FALSE)
expr |
A code expression that will immediately be executed (before the
call to |
env |
An environment. |
quoted |
Is the expression quoted? This is useful when you want to use an expression
that is stored in a variable; to do so, it must be quoted with |
A function that, when called in meta mode (i.e. inside
expandChain()
), will return the code in quoted form. If this function is
ever called outside of meta mode, it throws an error, as it is definitely
being called incorrectly.
setup <- metaAction({ library(stats) "# Set the seed to ensure repeatable randomness" set.seed(100) x <- 1 y <- 2 }) # The action has executed print(x) print(y) # And also you can emit the code expandChain( setup() )
setup <- metaAction({ library(stats) "# Set the seed to ensure repeatable randomness" set.seed(100) x <- 1 y <- 2 }) # The action has executed print(x) print(y) # And also you can emit the code expandChain( setup() )
Mark an expression as a meta-expression
metaExpr( expr, env = parent.frame(), quoted = FALSE, localize = "auto", bindToReturn = FALSE )
metaExpr( expr, env = parent.frame(), quoted = FALSE, localize = "auto", bindToReturn = FALSE )
expr |
An expression (quoted or unquoted). |
env |
An environment. |
quoted |
Is the expression quoted? This is useful when you want to use an expression
that is stored in a variable; to do so, it must be quoted with |
localize |
Whether or not to wrap the returned expression in |
bindToReturn |
For non- |
If inside meta mode, a quoted form of expr
for use inside of
metaReactive2()
, metaObserve2()
, or metaRender2()
. Otherwise, in
normal execution, the result of evaluating expr
.
metaReactive2()
, metaObserve2()
, metaRender2()
, ..
Create a observe()
r that, when invoked with meta-mode activated
(i.e. called within withMetaMode()
or expandChain()
), returns a
partially evaluated code expression. Outside of meta-mode,
metaObserve()
is equivalent to observe()
(it fully evaluates the given expression).
metaObserve( expr, env = parent.frame(), quoted = FALSE, label = NULL, domain = getDefaultReactiveDomain(), localize = "auto", bindToReturn = FALSE ) metaObserve2( expr, env = parent.frame(), quoted = FALSE, label = NULL, domain = getDefaultReactiveDomain() )
metaObserve( expr, env = parent.frame(), quoted = FALSE, label = NULL, domain = getDefaultReactiveDomain(), localize = "auto", bindToReturn = FALSE ) metaObserve2( expr, env = parent.frame(), quoted = FALSE, label = NULL, domain = getDefaultReactiveDomain() )
expr |
An expression (quoted or unquoted). |
env |
The parent environment for the reactive expression. By default,
this is the calling environment, the same as when defining an ordinary
non-reactive expression. If |
quoted |
If it is |
label |
A label for the observer, useful for debugging. |
domain |
See domains. |
localize |
Whether or not to wrap the returned expression in |
bindToReturn |
For non- |
If you wish to capture specific code inside of expr
(e.g. ignore code
that has no meaning outside shiny, like req()
), use metaObserve2()
in combination
with metaExpr()
. When using metaObserve2()
, expr
must return a metaExpr()
.
A function that, when called in meta mode (i.e. inside
expandChain()
), will return the code in quoted form. If this function is
ever called outside of meta mode, it throws an error, as it is definitely
being called incorrectly.
# observers execute 'immediately' x <- 1 mo <- metaObserve({ x <<- x + 1 }) getFromNamespace("flushReact", "shiny")() print(x) # It only makes sense to invoke an meta-observer # if we're in meta-mode (i.e., generating code) expandChain(mo()) # Intentionally produces an error ## Not run: mo()
# observers execute 'immediately' x <- 1 mo <- metaObserve({ x <<- x + 1 }) getFromNamespace("flushReact", "shiny")() print(x) # It only makes sense to invoke an meta-observer # if we're in meta-mode (i.e., generating code) expandChain(mo()) # Intentionally produces an error ## Not run: mo()
Create a reactive()
that, when invoked with meta-mode activated
(i.e. called within withMetaMode()
or expandChain()
), returns a
code expression (instead of evaluating that expression and returning the value).
metaReactive( expr, env = parent.frame(), quoted = FALSE, varname = NULL, domain = shiny::getDefaultReactiveDomain(), inline = FALSE, localize = "auto", bindToReturn = FALSE ) metaReactive2( expr, env = parent.frame(), quoted = FALSE, varname = NULL, domain = shiny::getDefaultReactiveDomain(), inline = FALSE )
metaReactive( expr, env = parent.frame(), quoted = FALSE, varname = NULL, domain = shiny::getDefaultReactiveDomain(), inline = FALSE, localize = "auto", bindToReturn = FALSE ) metaReactive2( expr, env = parent.frame(), quoted = FALSE, varname = NULL, domain = shiny::getDefaultReactiveDomain(), inline = FALSE )
expr |
An expression (quoted or unquoted). |
env |
The parent environment for the reactive expression. By default,
this is the calling environment, the same as when defining an ordinary
non-reactive expression. If |
quoted |
If it is |
varname |
An R variable name that this object prefers to be named when
its code is extracted into an R script. (See also: |
domain |
See domains. |
inline |
If |
localize |
Whether or not to wrap the returned expression in |
bindToReturn |
For non- |
If you wish to capture specific code inside of expr
(e.g. ignore code
that has no meaning outside shiny, like req()
), use metaReactive2()
in combination
with metaExpr()
. When using metaReactive2()
, expr
must return a metaExpr()
.
If varname
is unspecified, srcrefs are used in attempt to infer the name
bound to the meta-reactive object. In order for this inference to work, the
keep.source
option must be TRUE
and expr
must begin with \{
.
A function that, when called in meta mode (i.e. inside
expandChain()
), will return the code in quoted form. When called outside
meta mode, it acts the same as a regular shiny::reactive()
expression
call.
library(shiny) options(shiny.suppressMissingContextError = TRUE) input <- list(x = 1) y <- metaReactive({ req(input$x) a <- ..(input$x) + 1 b <- a + 1 c + 1 }) withMetaMode(y()) expandChain(y()) y <- metaReactive2({ req(input$x) metaExpr({ a <- ..(input$x) + 1 b <- a + 1 c + 1 }, bindToReturn = TRUE) }) expandChain(y())
library(shiny) options(shiny.suppressMissingContextError = TRUE) input <- list(x = 1) y <- metaReactive({ req(input$x) a <- ..(input$x) + 1 b <- a + 1 c + 1 }) withMetaMode(y()) expandChain(y()) y <- metaReactive2({ req(input$x) metaExpr({ a <- ..(input$x) + 1 b <- a + 1 c + 1 }, bindToReturn = TRUE) }) expandChain(y())
Create a meta-reactive output that, when invoked with meta-mode activated
(i.e. called within expandChain()
or withMetaMode()
), returns a
code expression (instead of evaluating that expression and returning the value).
metaRender( renderFunc, expr, ..., env = parent.frame(), quoted = FALSE, localize = "auto", bindToReturn = FALSE ) metaRender2(renderFunc, expr, ..., env = parent.frame(), quoted = FALSE)
metaRender( renderFunc, expr, ..., env = parent.frame(), quoted = FALSE, localize = "auto", bindToReturn = FALSE ) metaRender2(renderFunc, expr, ..., env = parent.frame(), quoted = FALSE)
renderFunc |
A reactive output function (e.g., shiny::renderPlot, shiny::renderText, shiny::renderUI, etc). |
expr |
An expression that generates given output expected by |
... |
Other arguments passed along to |
env |
The parent environment for the reactive expression. By default,
this is the calling environment, the same as when defining an ordinary
non-reactive expression. If |
quoted |
If it is |
localize |
Whether or not to wrap the returned expression in |
bindToReturn |
For non- |
If you wish to capture specific code inside of expr
(e.g. ignore code
that has no meaning outside shiny, like req()
), use metaRender2()
in combination
with metaExpr()
. When using metaRender2()
, expr
must return a metaExpr()
.
Since package authors are allowed to create their own output rendering functions,
creating a meta-counterpart of an output renderer (e.g. renderPlot()
) needs to be
more general than prefixing meta
to the function name (as with metaReactive()
and metaObserve()
).
metaRender()
makes some assumptions about the arguments taken by the render function,
assumptions that we believe are true for all existing render functions.
If you encounter a render function that doesn't seem to work properly,
please let us know by filing an issue on GitHub.
An annotated render function, ready to be assigned to an output slot.
The function may also be called in meta mode (i.e., inside expandChain()
)
to return the code in quoted form.
if (interactive()) { library(shiny) library(shinymeta) ui <- fluidPage( selectInput("var", label = "Choose a variable", choices = names(cars)), verbatimTextOutput("Summary"), verbatimTextOutput("code") ) server <- function(input, output) { var <- metaReactive({ cars[[..(input$var)]] }) output$Summary <- metaRender(renderPrint, { summary(..(var())) }) output$code <- renderPrint({ expandChain(output$Summary()) }) } shinyApp(ui, server) }
if (interactive()) { library(shiny) library(shinymeta) ui <- fluidPage( selectInput("var", label = "Choose a variable", choices = names(cars)), verbatimTextOutput("Summary"), verbatimTextOutput("code") ) server <- function(input, output) { var <- metaReactive({ cars[[..(input$var)]] }) output$Summary <- metaRender(renderPrint, { summary(..(var())) }) output$code <- renderPrint({ expandChain(output$Summary()) }) } shinyApp(ui, server) }
Intended for overlaying a button over a shiny output, that when clicked,
displays code for reproducing that output. The button is
similar to an shiny::actionButton()
, but instead of providing an inputId
,
the id is determined by the id of the outputObj
. The name
of that input is a function of outputObj
's outputId
:
input$OUTPUTID_output_code
.
outputCodeButton( outputObj, label = "Show code", icon = shiny::icon("code"), width = NULL, ... )
outputCodeButton( outputObj, label = "Show code", icon = shiny::icon("code"), width = NULL, ... )
outputObj |
A shiny output container (e.g., shiny::plotOutput, shiny::textOutput, etc) |
label |
The contents of the button or link–usually a text label, but you could also use any other HTML, like an image. |
icon |
An optional |
width |
The width of the input, e.g. |
... |
Named attributes to be applied to the button or link. |
the outputObj
wrapped in a card-like HTML container.
if (interactive()) { library(shiny) ui <- fluidPage( sliderInput("n", label = "Number of samples", min = 10, max = 100, value = 30), outputCodeButton(plotOutput("p")) ) server <- function(input, output) { output$p <- metaRender(renderPlot, { plot(sample(..(input$n))) }) observeEvent(input$p_output_code, { code <- expandChain(output$p()) displayCodeModal(code) }) } shinyApp(ui, server) }
if (interactive()) { library(shiny) ui <- fluidPage( sliderInput("n", label = "Number of samples", min = 10, max = 100, value = 30), outputCodeButton(plotOutput("p")) ) server <- function(input, output) { output$p <- metaRender(renderPlot, { plot(sample(..(input$n))) }) observeEvent(input$p_output_code, { code <- expandChain(output$p()) displayCodeModal(code) }) } shinyApp(ui, server) }
Evaluate an expression with meta mode activated
withMetaMode(expr, mode = TRUE)
withMetaMode(expr, mode = TRUE)
expr |
an expression. |
mode |
whether or not to evaluate expression in meta mode. |
The result of evaluating expr
.