diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3928a8ed..0501af6f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -22,11 +22,12 @@ jobs:
echo "JOB PARALLELISM: ${CIRCLE_NODE_TOTAL}"
echo "CIRCLE_REPOSITORY_URL: ${CIRCLE_REPOSITORY_URL}"
echo $CIRCLE_JOB > circlejob.txt
+ git rev-parse HEAD | tr -d '\n' > commit.txt
- run:
name: 🚧 install R dependencies
command: |
- sudo Rscript -e 'install.packages("remotes"); remotes::install_github("plotly/dashR", dependencies=TRUE, upgrade=TRUE); install.packages(".", type="source", repos=NULL)'
+ sudo Rscript -e 'commit_hash <- readChar("commit.txt", file.info("commit.txt")$size); message("Preparing to install plotly/dashR ", commit_hash, " ..."); install.packages("remotes"); remotes::install_github("plotly/dashR", upgrade=TRUE, ref=commit_hash, force=TRUE)'
- run:
name: ⚙️ Integration tests
@@ -36,7 +37,7 @@ jobs:
git clone --depth 1 https://github.com/plotly/dash.git
cd dash && pip install -e .[testing] --quiet && cd ..
export PATH=$PATH:/home/circleci/.local/bin/
- pytest --cli-log-level DEBUG tests/integration/
+ pytest tests/integration/
- run:
name: 🔎 Unit tests
diff --git a/NAMESPACE b/NAMESPACE
index f600e6b9..717692bc 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -4,6 +4,7 @@ S3method(print,dash_component)
export(Dash)
export(dashNoUpdate)
export(createCallbackId)
+export(clientsideFunction)
export(input)
export(output)
export(state)
diff --git a/R/dash.R b/R/dash.R
index 2724d0ae..6eba02d9 100644
--- a/R/dash.R
+++ b/R/dash.R
@@ -21,13 +21,13 @@
#' of the HTML page).\cr
#' `server` \tab \tab The web server used to power the application.
#' Must be a [fiery::Fire] object.\cr
-#' `assets_folder` \tab \tab Character. A path, relative to the current working directory,
-#' for extra files to be used in the browser. Default is "assets". All .js and
+#' `assets_folder` \tab \tab Character. A path, relative to the current working directory,
+#' for extra files to be used in the browser. Default is "assets". All .js and
#' .css files will be loaded immediately unless excluded by `assets_ignore`,
#' and other files such as images will be served if requested. Default is `assets`. \cr
-#' `assets_url_path` \tab \tab Character. Specify the URL path for asset serving. Default is `assets`. \cr
-#' `assets_ignore` \tab \tab Character. A regular expression, to match assets to omit from
-#' immediate loading. Ignored files will still be served if specifically requested. You
+#' `assets_url_path` \tab \tab Character. Specify the URL path for asset serving. Default is `assets`. \cr
+#' `assets_ignore` \tab \tab Character. A regular expression, to match assets to omit from
+#' immediate loading. Ignored files will still be served if specifically requested. You
#' cannot use this to prevent access to sensitive files. \cr
#' `serve_locally` \tab \tab Whether to serve HTML dependencies locally or
#' remotely (via URL).\cr
@@ -39,7 +39,7 @@
#' `external_stylesheets` \tab \tab An optional list of valid URLs from which
#' to serve CSS for rendered pages.\cr
#' `suppress_callback_exceptions` \tab \tab Whether to relay warnings about
-#' possible layout mis-specifications when registering a callback. \cr
+#' possible layout mis-specifications when registering a callback. \cr
#' `components_cache_max_age` \tab \tab An integer value specifying the time
#' interval prior to expiring cached assets. The default is 2678400 seconds,
#' or 31 calendar days.
@@ -76,15 +76,22 @@
#' \describe{
#' \item{output}{a named list including a component `id` and `property`}
#' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`}
-#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments}
+#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, or a call to [clientsideFunction] including `namespace` and `function_name` arguments for a locally served JavaScript function}
#' }
#' The `output` argument defines which layout component property should
-#' receive the results (via the [output] object). The events that
+#' receive the results (via the [output] object). The events that
#' trigger the callback are then described by the [input] (and/or [state])
#' object(s) (which should reference layout components), which become
-#' argument values for the callback handler defined in `func`.
+#' argument values for R callback handlers defined in `func`.
+#'
+#' `func` may either be an anonymous R function, or a call to
+#' `clientsideFunction()`, which describes a locally served JavaScript
+#' function instead. The latter defines a "clientside callback", which
+#' updates components without passing data to and from the Dash backend.
+#' The latter may offer improved performance relative to callbacks written
+#' in R.
#' }
-#' \item{`run_server(host = Sys.getenv('DASH_HOST', "127.0.0.1"),
+#' \item{`run_server(host = Sys.getenv('DASH_HOST', "127.0.0.1"),
#' port = Sys.getenv('DASH_PORT', 8050), block = TRUE, showcase = FALSE, ...)`}{
#' Launch the application. If provided, `host`/`port` set
#' the `host`/`port` fields of the underlying [fiery::Fire] web
@@ -166,7 +173,7 @@ Dash <- R6::R6Class(
router <- routr::RouteStack$new()
# ensure that assets_folder is neither NULL nor character(0)
- if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
+ if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
warning(sprintf(
"The supplied assets folder, '%s', could not be found in the project directory.",
@@ -180,7 +187,7 @@ Dash <- R6::R6Class(
private$other <- private$asset_map$other
}
}
-
+
# ------------------------------------------------------------------------
# Set a sensible default logger
# ------------------------------------------------------------------------
@@ -198,7 +205,7 @@ Dash <- R6::R6Class(
route$add_handler("get", dash_layout, function(request, response, keys, ...) {
rendered_layout <- private$layout_render()
# pass the layout on to encode_plotly in case there are dccGraph
- # components which include Plotly.js figures for which we'll need to
+ # components which include Plotly.js figures for which we'll need to
# run plotly_build from the plotly package
lay <- encode_plotly(rendered_layout)
response$body <- to_JSON(lay, pretty = TRUE)
@@ -221,7 +228,8 @@ Dash <- R6::R6Class(
list(
inputs=callback_signature$inputs,
output=createCallbackId(callback_signature$output),
- state=callback_signature$state
+ state=callback_signature$state,
+ clientside_function=callback_signature$clientside_function
)
}, private$callback_map)
@@ -288,7 +296,7 @@ Dash <- R6::R6Class(
# reset callback context
private$callback_context_ <- NULL
-
+
# inspect the output_value to determine whether any outputs have no_update
# objects within them; these should not be updated
if (length(output_value) == 1 && class(output_value) == "no_update") {
@@ -297,38 +305,38 @@ Dash <- R6::R6Class(
}
else if (is.null(private$stack_message)) {
# pass on output_value to encode_plotly in case there are dccGraph
- # components which include Plotly.js figures for which we'll need to
+ # components which include Plotly.js figures for which we'll need to
# run plotly_build from the plotly package
output_value <- encode_plotly(output_value)
-
+
# for multiple outputs, have to format the response body like this, including 'multi' key:
# https://github.com/plotly/dash/blob/d9ddc877d6b15d9354bcef4141acca5d5fe6c07b/dash/dash.py#L1174-L1209
# for single outputs, the response body is formatted slightly differently:
# https://github.com/plotly/dash/blob/d9ddc877d6b15d9354bcef4141acca5d5fe6c07b/dash/dash.py#L1210-L1220
-
+
if (substr(request$body$output, 1, 2) == '..') {
# omit return objects of class "no_update" from output_value
updatable_outputs <- "no_update" != vapply(output_value, class, character(1))
output_value <- output_value[updatable_outputs]
-
+
# if multi-output callback, isolate the output IDs and properties
ids <- getIdProps(request$body$output)$ids[updatable_outputs]
props <- getIdProps(request$body$output)$props[updatable_outputs]
-
+
# prepare a response object which has list elements corresponding to ids
# which themselves contain named list elements corresponding to props
# then fill in nested list elements based on output_value
-
+
allprops <- setNames(vector("list", length(unique(ids))), unique(ids))
-
+
idmap <- setNames(ids, props)
-
+
for (id in unique(ids)) {
allprops[[id]] <- output_value[grep(id, ids)]
names(allprops[[id]]) <- names(idmap[which(idmap==id)])
}
-
+
resp <- list(
response = allprops,
multi = TRUE
@@ -340,7 +348,7 @@ Dash <- R6::R6Class(
)
)
}
-
+
response$body <- to_JSON(resp)
response$status <- 200L
response$type <- 'json'
@@ -364,8 +372,8 @@ Dash <- R6::R6Class(
# https://github.com/plotly/dash/blob/1249ffbd051bfb5fdbe439612cbec7fa8fff5ab5/dash/dash.py#L488
# https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
dash_suite <- paste0(self$config$routes_pathname_prefix, "_dash-component-suites/:package_name/:filename")
-
- route$add_handler("get", dash_suite, function(request, response, keys, ...) {
+
+ route$add_handler("get", dash_suite, function(request, response, keys, ...) {
filename <- basename(file.path(keys$filename))
dep_list <- c(private$dependencies_internal,
private$dependencies,
@@ -380,16 +388,16 @@ Dash <- R6::R6Class(
# return warning if a dependency goes unmatched, since the page
# will probably fail to render properly anyway without it
if (length(dep_pkg$rpkg_path) == 0) {
- warning(sprintf("The dependency '%s' could not be loaded; the file was not found.",
- filename),
+ warning(sprintf("The dependency '%s' could not be loaded; the file was not found.",
+ filename),
call. = FALSE)
-
+
response$body <- NULL
response$status <- 404L
} else {
dep_path <- system.file(dep_pkg$rpkg_path,
package = dep_pkg$rpkg_name)
-
+
response$body <- readLines(dep_path,
warn = FALSE,
encoding = "UTF-8")
@@ -408,11 +416,11 @@ Dash <- R6::R6Class(
# ensure slashes are not doubled
dash_assets <- sub("//", "/", dash_assets)
-
+
route$add_handler("get", dash_assets, function(request, response, keys, ...) {
# unfortunately, keys do not exist for wildcard headers in routr -- URL must be parsed
# e.g. for "http://127.0.0.1:8050/assets/stylesheet.css?m=1552591104"
- #
+ #
# the following regex pattern will return "/stylesheet.css":
assets_pattern <- paste0("(?<=",
gsub("/",
@@ -420,23 +428,23 @@ Dash <- R6::R6Class(
private$assets_url_path),
")([^?])+"
)
-
+
# now, identify vector positions for asset string matching pattern above
asset_match <- gregexpr(pattern = assets_pattern, request$url, perl=TRUE)
# use regmatches to retrieve only the substring following assets_url_path
asset_to_match <- unlist(regmatches(request$url, asset_match))
-
+
# now that we've parsed the URL, attempt to match the subpath in the map,
# then return the local absolute path to the asset
asset_path <- get_asset_path(private$asset_map,
asset_to_match)
-
+
# the following codeblock attempts to determine whether the requested
# content exists, if the data should be encoded as plain text or binary,
# and opens/closes a file handle if the type is assumed to be binary
if (!(is.null(asset_path)) && file.exists(asset_path)) {
- response$type <- request$headers[["Content-Type"]] %||%
- mime::guess_type(asset_to_match,
+ response$type <- request$headers[["Content-Type"]] %||%
+ mime::guess_type(asset_to_match,
empty = "application/octet-stream")
if (grepl("text|javascript", response$type)) {
@@ -444,13 +452,13 @@ Dash <- R6::R6Class(
warn = FALSE,
encoding = "UTF-8")
} else {
- file_handle <- file(asset_path, "rb")
+ file_handle <- file(asset_path, "rb")
response$body <- readBin(file_handle,
raw(),
file.size(asset_path))
close(file_handle)
}
-
+
response$set_header('Cache-Control',
sprintf('public, max-age=%s',
components_cache_max_age)
@@ -459,19 +467,19 @@ Dash <- R6::R6Class(
}
TRUE
})
-
+
dash_favicon <- paste0(self$config$routes_pathname_prefix, "_favicon.ico")
-
+
route$add_handler("get", dash_favicon, function(request, response, keys, ...) {
asset_path <- get_asset_path(private$asset_map,
"/favicon.ico")
-
+
file_handle <- file(asset_path, "rb")
response$body <- readBin(file_handle,
raw(),
file.size(asset_path))
close(file_handle)
-
+
response$set_header('Cache-Control',
sprintf('public, max-age=%s',
components_cache_max_age)
@@ -480,7 +488,7 @@ Dash <- R6::R6Class(
response$status <- 200L
TRUE
})
-
+
# Add a 'catchall' handler to redirect other requests to the index
dash_catchall <- paste0(self$config$routes_pathname_prefix, "*")
route$add_handler('get', dash_catchall, function(request, response, keys, ...) {
@@ -534,41 +542,48 @@ Dash <- R6::R6Class(
inputs <- params[vapply(params, function(x) 'input' %in% attr(x, "class"), FUN.VALUE=logical(1))]
state <- params[vapply(params, function(x) 'state' %in% attr(x, "class"), FUN.VALUE=logical(1))]
-
+
+ if (is.function(func)) {
+ clientside_function <- NULL
+ } else {
+ clientside_function <- func
+ func <- NULL
+ }
+
# register the callback_map
private$callback_map <- insertIntoCallbackMap(private$callback_map,
inputs,
output,
state,
- func)
-
+ func,
+ clientside_function)
},
# ------------------------------------------------------------------------
# request and return callback context
- # ------------------------------------------------------------------------
+ # ------------------------------------------------------------------------
callback_context = function() {
if (is.null(private$callback_context_)) {
warning("callback_context is undefined; callback_context may only be accessed within a callback.")
- }
+ }
private$callback_context_
},
-
+
# ------------------------------------------------------------------------
# convenient fiery wrappers
# ------------------------------------------------------------------------
- run_server = function(host = Sys.getenv('DASH_HOST', "127.0.0.1"),
- port = Sys.getenv('DASH_PORT', 8050),
- block = TRUE,
- showcase = FALSE,
- dev_tools_prune_errors = TRUE,
- debug = FALSE,
+ run_server = function(host = Sys.getenv('DASH_HOST', "127.0.0.1"),
+ port = Sys.getenv('DASH_PORT', 8050),
+ block = TRUE,
+ showcase = FALSE,
+ dev_tools_prune_errors = TRUE,
+ debug = FALSE,
dev_tools_ui = NULL,
dev_tools_props_check = NULL,
...) {
self$server$host <- host
self$server$port <- as.numeric(port)
-
+
if (is.null(dev_tools_ui) && debug || isTRUE(dev_tools_ui)) {
self$config$ui <- TRUE
} else {
@@ -583,7 +598,7 @@ Dash <- R6::R6Class(
private$prune_errors <- dev_tools_prune_errors
private$debug <- debug
-
+
self$server$ignite(block = block, showcase = showcase, ...)
}
),
@@ -592,7 +607,7 @@ Dash <- R6::R6Class(
# private fields defined on initiation
name = NULL,
serve_locally = NULL,
- assets_folder = NULL,
+ assets_folder = NULL,
assets_url_path = NULL,
assets_ignore = NULL,
routes_pathname_prefix = NULL,
@@ -602,15 +617,15 @@ Dash <- R6::R6Class(
css = NULL,
scripts = NULL,
other = NULL,
-
+
# initialize flags for debug mode and stack pruning,
debug = NULL,
prune_errors = NULL,
stack_message = NULL,
# callback context
- callback_context_ = NULL,
-
+ callback_context_ = NULL,
+
# fields for tracking HTML dependencies
dependencies = list(),
dependencies_user = list(),
@@ -660,7 +675,7 @@ Dash <- R6::R6Class(
# load package-level HTML dependencies from attached pkgs
metadataFns <- lapply(.packages(), getDashMetadata)
metadataFns <- metadataFns[lengths(metadataFns) != 0]
-
+
deps_layout <- lapply(metadataFns, function(dep) {
# the objective is to identify JS dependencies
# without requiring that a proprietary R format
@@ -706,18 +721,18 @@ Dash <- R6::R6Class(
walk_assets_directory = function(assets_dir = private$assets_folder) {
# obtain the full canonical path
asset_path <- normalizePath(file.path(assets_dir))
-
+
# remove multiple slashes if present
asset_path <- gsub("//+",
"/",
asset_path)
-
+
# collect all the file paths to all files in assets, walk
# directory tree recursively
files <- list.files(path = asset_path,
full.names = TRUE,
recursive = TRUE)
-
+
# if the user supplies an assets_ignore filter regex, use this
# to filter the file map to exclude anything that matches
if (private$assets_ignore != "") {
@@ -725,7 +740,7 @@ Dash <- R6::R6Class(
files,
perl = TRUE)]
}
-
+
# regex to match substring of absolute path
# the following lines escape out slashes, keeping subpath
# but without private$assets_folder included
@@ -735,17 +750,17 @@ Dash <- R6::R6Class(
private$assets_folder),
")([^?])+"
)
-
+
# if file extension is .css, add to stylesheets
sheet_paths <- files[tools::file_ext(files) == "css"]
-
+
# if file extension is .js, add to scripts
script_paths <- files[tools::file_ext(files) == "js"]
-
+
# file_paths includes all assets that are neither CSS nor JS
# this is to avoid duplicate entries in the map when flattened
file_paths <- files[!(tools::file_ext(files) %in% c("css", "js"))]
-
+
# for CSS, JavaScript, and everything to be served in assets, construct
# a map -- a list of three character string vectors, in which the elements
# are absolute (local system) paths to the assets being served, and the
@@ -762,7 +777,7 @@ Dash <- R6::R6Class(
} else {
css_map <- NULL
}
-
+
if (length(script_paths)) {
# first, sort the filenames alphanumerically
script_paths <- script_paths[order(basename(script_paths))]
@@ -775,7 +790,7 @@ Dash <- R6::R6Class(
} else {
scripts_map <- NULL
}
-
+
if (length(file_paths)) {
# first, sort the filenames alphanumerically
file_paths <- file_paths[order(basename(file_paths))]
@@ -788,9 +803,9 @@ Dash <- R6::R6Class(
} else {
other_files_map <- NULL
}
-
- return(list(css = css_map,
- scripts = scripts_map,
+
+ return(list(css = css_map,
+ scripts = scripts_map,
other = other_files_map))
},
@@ -819,12 +834,12 @@ Dash <- R6::R6Class(
# akin to https://github.com/plotly/dash/blob/d2ebc837/dash/dash.py#L338
# note discussion here https://github.com/plotly/dash/blob/d2ebc837/dash/dash.py#L279-L284
.index = NULL,
-
+
collect_resources = function() {
# Dash's own dependencies
# serve the dev version of dash-renderer when in debug mode
dependencies_all_internal <- .dash_js_metadata()
-
+
if (private$debug) {
depsSubset <- dependencies_all_internal[!names(dependencies_all_internal) %in% c("dash-renderer-prod",
"dash-renderer-map-prod",
@@ -834,9 +849,9 @@ Dash <- R6::R6Class(
"dash-renderer-map-dev",
"prop-types-dev")]
}
-
+
private$dependencies_internal <- depsSubset
-
+
# collect and resolve package dependencies
depsAll <- compact(c(
private$react_deps()[private$react_versions() %in% private$react_version_enabled()],
@@ -845,24 +860,24 @@ Dash <- R6::R6Class(
private$dependencies_user,
private$dependencies_internal[grepl(pattern = "dash-renderer", x = private$dependencies_internal)]
))
-
+
# normalizes local paths and keeps newer versions of duplicates
- depsAll <- depsAll[!vapply(depsAll,
+ depsAll <- depsAll[!vapply(depsAll,
function(v) {
!is.null(v[["script"]]) && tools::file_ext(v[["script"]]) == "map"
}, logical(1))]
-
+
# styleheets always go in header
css_deps <- compact(lapply(depsAll, function(dep) {
if (is.null(dep$stylesheet)) return(NULL)
dep$script <- NULL
dep
}))
-
- css_deps <- render_dependencies(css_deps,
- local = private$serve_locally,
+
+ css_deps <- render_dependencies(css_deps,
+ local = private$serve_locally,
prefix=self$config$requests_pathname_prefix)
-
+
# scripts go after dash-renderer dependencies (i.e., React),
# but before dash-renderer itself
scripts_deps <- compact(lapply(depsAll, function(dep) {
@@ -870,30 +885,30 @@ Dash <- R6::R6Class(
dep$stylesheet <- NULL
dep
}))
-
+
scripts_deps <- render_dependencies(scripts_deps,
- local = private$serve_locally,
+ local = private$serve_locally,
prefix=self$config$requests_pathname_prefix)
-
+
# collect CSS assets from dependencies
if (!(is.null(private$css))) {
css_assets <- generate_css_dist_html(href = paste0(private$assets_url_path, names(private$css)),
local = TRUE,
local_path = private$css,
prefix = self$config$requests_pathname_prefix)
- }
+ }
else {
css_assets <- NULL
}
-
+
# collect CSS assets from external_stylesheets
- css_external <- vapply(self$config$external_stylesheets,
- generate_css_dist_html,
+ css_external <- vapply(self$config$external_stylesheets,
+ generate_css_dist_html,
FUN.VALUE=character(1),
- local = FALSE)
-
+ local = FALSE)
+
# collect JS assets from dependencies
- #
+ #
if (!(is.null(private$scripts))) {
scripts_assets <- generate_js_dist_html(href = paste0(private$assets_url_path, names(private$scripts)),
local = TRUE,
@@ -905,9 +920,9 @@ Dash <- R6::R6Class(
# collect JS assets from external_scripts
scripts_external <- vapply(self$config$external_scripts,
- generate_js_dist_html,
+ generate_js_dist_html,
FUN.VALUE=character(1))
-
+
# create tag for favicon, if present
# other_files_map[names(other_files_map) %in% "/favicon.ico"]
if ("/favicon.ico" %in% names(private$other)) {
@@ -918,40 +933,40 @@ Dash <- R6::R6Class(
# set script tag to invoke a new dash_renderer
scripts_invoke_renderer <- sprintf("",
- "_dash-renderer",
- "application/javascript",
+ "_dash-renderer",
+ "application/javascript",
"var renderer = new DashRenderer();")
-
+
# serving order of CSS and JS tags: package -> external -> assets
css_tags <- paste(c(css_deps,
css_external,
css_assets),
collapse = "\n")
-
+
scripts_tags <- paste(c(scripts_deps,
scripts_external,
scripts_assets,
scripts_invoke_renderer),
collapse = "\n")
-
- return(list(css_tags = css_tags,
+
+ return(list(css_tags = css_tags,
scripts_tags = scripts_tags,
favicon = favicon))
},
-
+
index = function() {
# generate tags for all assets
all_tags <- private$collect_resources()
-
+
# retrieve favicon tag for serving in the index
favicon <- all_tags[["favicon"]]
-
+
# retrieve CSS tags for serving in the index
css_tags <- all_tags[["css_tags"]]
-
+
# retrieve script tags for serving in the index
scripts_tags <- all_tags[["scripts_tags"]]
-
+
private$.index <- sprintf(
'
diff --git a/R/utils.R b/R/utils.R
index 92d6e261..c69816aa 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -135,14 +135,14 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) {
} else {
"file"
}
-
+
# According to Dash convention, label react and react-dom as originating
# in dash_renderer package, even though all three are currently served
# up from the DashR package
if (dep$name %in% c("react", "react-dom", "prop-types")) {
dep$name <- "dash-renderer"
}
-
+
# The following lines inject _dash-component-suites into the src tags,
# as this is the current Dash convention. The dependency paths cannot
# be set solely at component library generation time, since hosted
@@ -156,13 +156,13 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) {
# parameter for cache busting
if (!is.null(dep$package)) {
if(!(is.null(dep$script))) {
- filename <- dep$script
+ filename <- dep$script
} else {
filename <- dep$stylesheet
}
-
+
dep_path <- paste(dep$src$file, filename, sep="/")
-
+
# the gsub line is to remove stray duplicate slashes, to
# permit exact string matching on pathnames
dep_path <- gsub("//+",
@@ -173,25 +173,25 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) {
package = dep$package)
if (!file.exists(full_path)) {
- warning(sprintf("The dependency path '%s' within the '%s' package is invalid; cannot find '%s'.",
+ warning(sprintf("The dependency path '%s' within the '%s' package is invalid; cannot find '%s'.",
full_path,
dep$package,
filename),
call. = FALSE)
}
-
+
modified <- as.integer(file.mtime(full_path))
} else {
modified <- as.integer(Sys.time())
}
-
+
# we don't want to serve the JavaScript source maps here,
# until we are able to provide full support for debug mode,
# as in Dash for Python
if ("script" %in% names(dep) && tools::file_ext(dep[["script"]]) != "map") {
if (!(is_local) & !(is.null(dep$src$href))) {
html <- generate_js_dist_html(href = dep$src$href)
-
+
} else {
dep[["script"]] <- paste0(path_prefix,
"_dash-component-suites/",
@@ -202,12 +202,12 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) {
dep$version,
"&m=",
modified)
-
+
html <- generate_js_dist_html(href = dep[["script"]], as_is = TRUE)
}
} else if (!(is_local) & "stylesheet" %in% names(dep) & src == "href") {
html <- generate_css_dist_html(href = paste(dep[["src"]][["href"]],
- dep[["stylesheet"]],
+ dep[["stylesheet"]],
sep="/"),
local = FALSE)
} else if ("stylesheet" %in% names(dep) & src == "file") {
@@ -216,21 +216,21 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) {
dep$name,
"/",
basename(dep[["stylesheet"]]))
-
+
if (!(is.null(dep$version))) {
if(!is.null(dep$package)) {
sheetpath <- paste0(dep[["stylesheet"]],
"?v=",
dep$version)
-
+
html <- generate_css_dist_html(href = sheetpath, as_is = TRUE)
} else {
sheetpath <- paste0(dep[["src"]][["file"]],
dep[["stylesheet"]],
"?v=",
dep$version)
-
- html <- generate_css_dist_html(href = sheetpath, as_is = TRUE)
+
+ html <- generate_css_dist_html(href = sheetpath, as_is = TRUE)
}
} else {
@@ -324,7 +324,7 @@ assert_no_names <- function (x)
# the following function attempts to prune remote CSS
# or local CSS/JS dependencies that either should not
# be resolved to local R package paths, or which have
-# insufficient information to do so.
+# insufficient information to do so.
#
# this attempts to avoid cryptic errors produced by
# get_package_mapping, which requires three parameters:
@@ -348,24 +348,25 @@ clean_dependencies <- function(deps) {
}
)
deps_with_file <- dep_list[!vapply(dep_list, is.null, logical(1))]
-
+
return(deps_with_file)
}
-insertIntoCallbackMap <- function(map, inputs, output, state, func) {
+insertIntoCallbackMap <- function(map, inputs, output, state, func, clientside_function) {
map[[createCallbackId(output)]] <- list(inputs=inputs,
output=output,
state=state,
- func=func
+ func=func,
+ clientside_function=clientside_function
)
if (length(map) >= 2) {
ids <- lapply(names(map), function(x) dash:::getIdProps(x)$ids)
props <- lapply(names(map), function(x) dash:::getIdProps(x)$props)
outputs_as_list <- mapply(paste, ids, props, sep=".", SIMPLIFY = FALSE)
-
+
if (length(Reduce(intersect, outputs_as_list))) {
- stop(sprintf("One or more outputs are duplicated across callbacks. Please ensure that all ID and property combinations are unique."), call. = FALSE)
+ stop(sprintf("One or more outputs are duplicated across callbacks. Please ensure that all ID and property combinations are unique."), call. = FALSE)
}
}
return(map)
@@ -378,12 +379,12 @@ assert_valid_callbacks <- function(output, params, func) {
invalid_params <- vapply(params, function(x) {
!any(c('input', 'state') %in% attr(x, "class"))
}, FUN.VALUE=logical(1))
-
+
# Verify that no outputs are duplicated
if (length(output) != length(unique(output))) {
- stop(sprintf("One or more callback outputs have been duplicated; please confirm that all outputs are unique."), call. = FALSE)
+ stop(sprintf("One or more callback outputs have been duplicated; please confirm that all outputs are unique."), call. = FALSE)
}
-
+
# Verify that params contains no elements that are not either members of 'input' or 'state' classes
if (any(invalid_params)) {
stop(sprintf("Callback parameters must be inputs or states. Please verify formatting of callback parameters."), call. = FALSE)
@@ -393,44 +394,46 @@ assert_valid_callbacks <- function(output, params, func) {
if (!(valid_seq(params))) {
stop(sprintf("Strict ordering of callback handler parameters is required. Please ensure that input parameters precede all state parameters."), call. = FALSE)
}
-
+
# Assert that the component ID as passed is a string.
# This function inspects the output object to see if its ID
# is a valid string.
validateOutput <- function(string) {
return((is.character(string[["id"]]) & !grepl("^\\s*$", string[["id"]]) & !grepl("\\.", string[["id"]])))
}
-
+
# Check if the callback uses multiple outputs
if (any(sapply(output, is.list))) {
invalid_callback_ID <- (!all(vapply(output, validateOutput, logical(1))))
} else {
invalid_callback_ID <- (!validateOutput(output))
- }
+ }
if (invalid_callback_ID) {
stop(sprintf("Callback IDs must be (non-empty) character strings that do not contain one or more dots/periods. Please verify that the component ID is valid."), call. = FALSE)
}
# Assert that user_function is a valid function
if(!(is.function(func))) {
- stop(sprintf("The callback method's 'func' parameter requires a function as its argument. Please verify that 'func' is a valid, executable R function."), call. = FALSE)
+ if (!(all(names(func) == c("namespace", "function_name")))) {
+ stop(sprintf("The callback method's 'func' parameter requires an R function or clientsideFunction call as its argument. Please verify that 'func' is either a valid R function or clientsideFunction."), call. = FALSE)
+ }
}
-
+
# Check if inputs are a nested list
if(!(any(sapply(inputs, is.list)))) {
stop(sprintf("Callback inputs should be a nested list, in which each element of the sublist represents a component ID and its properties."), call. = FALSE)
}
-
+
# Check if state is a nested list, if the list is not empty
if(!(length(state) == 0) & !(any(sapply(state, is.list)))) {
stop(sprintf("Callback states should be a nested list, in which each element of the sublist represents a component ID and its properties."), call. = FALSE)
}
-
+
# Check that input is not NULL
if(is.null(inputs)) {
stop(sprintf("The callback method requires that one or more properly formatted inputs are passed."), call. = FALSE)
}
-
+
# Check that outputs are not inputs
# https://github.com/plotly/dash/issues/323
@@ -443,17 +446,17 @@ assert_valid_callbacks <- function(output, params, func) {
x
}
}
-
+
# determine whether any input matches the output, or outputs, if
# multiple callback scenario
inputs_vs_outputs <- mapply(function(inputObject, outputObject) {
identical(outputObject[["id"]], inputObject[["id"]]) & identical(outputObject[["property"]], inputObject[["property"]])
}, inputs, listWrap(output))
-
+
if(TRUE %in% inputs_vs_outputs) {
stop(sprintf("Circular input and output arguments were found. Please verify that callback outputs are not also input arguments."), call. = FALSE)
}
-
+
# TO DO: check that components contain props
TRUE
}
@@ -464,9 +467,9 @@ valid_seq <- function(params) {
class_attr <- vapply(params, function(x) {
attr(x, "class")[attr(x, "class") %in% c('input', 'state')]
}, FUN.VALUE=character(1))
-
+
rle_result <- rle(class_attr)$values
-
+
if (identical(rle_result, 'input')) {
return(TRUE)
} else if (identical(rle_result, c('input', 'state'))) {
@@ -479,7 +482,7 @@ valid_seq <- function(params) {
resolve_prefix <- function(prefix, environment_var) {
if (!(is.null(prefix))) {
assertthat::assert_that(is.character(prefix))
-
+
return(prefix)
} else {
prefix_env <- Sys.getenv(environment_var)
@@ -497,7 +500,7 @@ resolve_prefix <- function(prefix, environment_var) {
# optionally returns an R package name (if the file is contained
# inside an R package), or NULL if the dependency is not found,
# and a (local) path to the dependency.
-#
+#
# script_name is e.g. "dash_core_components.min.js"
# url_package is e.g. "dash_core_components"
# dependencies = list of htmlDependency objects
@@ -511,14 +514,14 @@ get_package_mapping <- function(script_name, url_package, dependencies) {
if (x$name %in% c('react', 'react-dom', 'prop-types')) {
x$name <- 'dash-renderer'
}
-
+
if (!is.null(x$script))
dep_path <- file.path(x$src$file, x$script)
else if (!is.null(x$stylesheet))
dep_path <- file.path(x$src$file, x$stylesheet)
-
+
# remove n>1 slashes and replace with / if present;
- # htmltools seems to permit // in pathnames, but
+ # htmltools seems to permit // in pathnames, but
# this complicates string matching unless they're
# removed from the pathname
result <- c(pkg_name=ifelse("package" %in% names(x), x$package, NULL),
@@ -526,26 +529,26 @@ get_package_mapping <- function(script_name, url_package, dependencies) {
dep_path=gsub("//+", replacement = "/", dep_path)
)
}, FUN.VALUE = character(3))
-
+
package_map <- t(package_map)
-
+
# pos_match is a vector of logical() values -- this allows filtering
# of the package_map entries based on name, path, and matching of
# URL package name against R package names. when all conditions are
# satisfied, pos_match will return TRUE
pos_match <- grepl(paste0(script_name, "$"), package_map[, "dep_path"]) &
grepl(url_package, package_map[,"dep_name"])
-
+
rpkg_name <- package_map[,"pkg_name"][pos_match]
rpkg_path <- package_map[,"dep_path"][pos_match]
-
+
return(list(rpkg_name=rpkg_name, rpkg_path=rpkg_path))
}
get_mimetype <- function(filename) {
# the tools package is available to all
filename_ext <- file_ext(filename)
-
+
if (filename_ext == 'js')
return('application/JavaScript')
else if (filename_ext == 'css')
@@ -556,14 +559,14 @@ get_mimetype <- function(filename) {
return(NULL)
}
-generate_css_dist_html <- function(href,
- local = FALSE,
+generate_css_dist_html <- function(href,
+ local = FALSE,
local_path = NULL,
prefix = NULL,
as_is = FALSE) {
if (!(local)) {
- if (grepl("^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$",
- href,
+ if (grepl("^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$",
+ href,
perl=TRUE) || as_is) {
sprintf("", href)
}
@@ -573,21 +576,21 @@ generate_css_dist_html <- function(href,
# strip leading slash from href if present
href <- sub("^/", "", href)
modified <- as.integer(file.mtime(local_path))
- sprintf("",
- prefix,
- href,
+ sprintf("",
+ prefix,
+ href,
modified)
}
-}
+}
-generate_js_dist_html <- function(href,
+generate_js_dist_html <- function(href,
local = FALSE,
local_path = NULL,
prefix = NULL,
as_is = FALSE) {
if (!(local)) {
- if (grepl("^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$",
- href,
+ if (grepl("^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$",
+ href,
perl=TRUE) || as_is) {
sprintf("", href)
}
@@ -598,11 +601,11 @@ generate_js_dist_html <- function(href,
href <- sub("^/", "", href)
modified <- as.integer(file.mtime(local_path))
sprintf("",
- prefix,
- href,
+ prefix,
+ href,
modified)
}
-}
+}
# This function takes the list object containing asset paths
# for all stylesheets and scripts, as well as the URL path
@@ -615,7 +618,7 @@ generate_js_dist_html <- function(href,
# assets pathname (i.e. "assets/stylesheet.css"), and
# $scripts, a list of character strings formatted
# identically to $css, also named with subpaths.
-#
+#
get_asset_path <- function(assets_map, asset_path) {
unlist(setNames(assets_map, NULL))[asset_path]
}
@@ -647,7 +650,7 @@ get_asset_url <- function(asset_path, prefix = "/") {
# prepend the asset name with the route prefix
return(paste(prefix, asset, sep="/"))
}
-
+
encode_plotly <- function(layout_objs) {
if (is.list(layout_objs)) {
if ("plotly" %in% class(layout_objs) &&
@@ -655,11 +658,11 @@ encode_plotly <- function(layout_objs) {
any(c("visdat", "data") %in% names(layout_objs$x))) {
# check to determine whether the current element is an
# object output from the plot_ly or ggplotly function;
- # if it is, we can safely assume that it contains no
- # other plot_ly or ggplotly objects and return the updated
+ # if it is, we can safely assume that it contains no
+ # other plot_ly or ggplotly objects and return the updated
# element as a mutated plotly figure argument that contains
- # only data and layout attributes. we suppress messages
- # since the plotly_build function will supply them, as it's
+ # only data and layout attributes. we suppress messages
+ # since the plotly_build function will supply them, as it's
# typically run interactively.
obj <- suppressMessages(plotly::plotly_build(layout_objs)$x)
layout_objs <- obj[c("data", "layout")]
@@ -698,14 +701,14 @@ printCallStack <- function(call_stack, header=TRUE) {
)
}
-stackTraceToHTML <- function(call_stack,
- throwing_call,
+stackTraceToHTML <- function(call_stack,
+ throwing_call,
error_message) {
if(is.null(call_stack)) {
return(NULL)
}
header <- " ### DashR Traceback (most recent/innermost call last) ###"
-
+
formattedStack <- c(paste0(
" ",
seq_along(
@@ -715,7 +718,7 @@ stackTraceToHTML <- function(call_stack,
call_stack,
collapse="
"
)
- )
+ )
template <- "
" response <- sprintf(template, @@ -745,7 +748,7 @@ getStackTrace <- function(expr, debug = FALSE, prune_errors = TRUE) { calls <- sys.calls() reverseStack <- rev(calls) attr(e, "stack.trace") <- calls - + if (!is.null(e$call[[1]])) errorCall <- e$call[[1]] else { @@ -756,21 +759,21 @@ getStackTrace <- function(expr, debug = FALSE, prune_errors = TRUE) { # getStackTrace, so we select the second match instead errorCall <- reverseStack[grepl(x=reverseStack, "simpleError|simpleWarning")][[2]] } - + functionsAsList <- lapply(calls, function(completeCall) { currentCall <- completeCall[[1]] - + if (is.function(currentCall) & !is.primitive(currentCall)) { - constructedCall <- paste0("%s
Error: %s: %s
%s