From 370c79e3e598d789173074b09a3c8bf8ba341c13 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 12 Aug 2019 00:53:07 -0400 Subject: [PATCH 1/3] rename pruned_errors to prune_errors --- R/dash.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/R/dash.R b/R/dash.R index e073b982..a27f2fed 100644 --- a/R/dash.R +++ b/R/dash.R @@ -286,7 +286,7 @@ Dash <- R6::R6Class( output_value <- getStackTrace(do.call(callback, callback_args), debug = private$debug, - pruned_errors = private$pruned_errors) + prune_errors = private$prune_errors) # reset callback context private$callback_context_ <- NULL @@ -525,7 +525,7 @@ Dash <- R6::R6Class( port = Sys.getenv('DASH_PORT', 8050), block = TRUE, showcase = FALSE, - pruned_errors = TRUE, + dev_tools_prune_errors = TRUE, debug = FALSE, dev_tools_ui = NULL, dev_tools_props_check = NULL, @@ -545,7 +545,7 @@ Dash <- R6::R6Class( self$config$props_check <- FALSE } - private$pruned_errors <- pruned_errors + private$prune_errors <- dev_tools_prune_errors private$debug <- debug self$server$ignite(block = block, showcase = showcase, ...) @@ -569,7 +569,7 @@ Dash <- R6::R6Class( # initialize flags for debug mode and stack pruning, debug = NULL, - pruned_errors = NULL, + prune_errors = NULL, stack_message = NULL, # callback context From f7adc8385e89ad336658f09a6457e13155693ef9 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 12 Aug 2019 00:54:06 -0400 Subject: [PATCH 2/3] rename pruned to prune --- R/utils.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/utils.R b/R/utils.R index d8d67664..9c50dcfa 100644 --- a/R/utils.R +++ b/R/utils.R @@ -685,7 +685,7 @@ stackTraceToHTML <- function(call_stack, # and capture the call stack. By default, the call # stack will be "pruned" of error handling functions # for greater readability. -getStackTrace <- function(expr, debug = FALSE, pruned_errors = TRUE) { +getStackTrace <- function(expr, debug = FALSE, prune_errors = TRUE) { if(debug) { tryCatch(withCallingHandlers( expr, @@ -711,7 +711,7 @@ getStackTrace <- function(expr, debug = FALSE, pruned_errors = TRUE) { reverseStack <- rev(calls) - if (pruned_errors) { + if (prune_errors) { # this line should match the last occurrence of the function # which raised the error within the call stack; prune here indexFromLast <- match(TRUE, lapply(reverseStack, function(currentCall) { From a44050a27ba55056afed8ebac81e852ddf852e1f Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 12 Aug 2019 23:20:18 -0400 Subject: [PATCH 3/3] Provide support for no_update in Dash for R (#111) --- NAMESPACE | 1 + R/dash.R | 10 +- R/dependencies.R | 12 +- man/dependencies.Rd | 7 ++ tests/integration/callbacks/test_no_update.py | 111 ++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 tests/integration/callbacks/test_no_update.py diff --git a/NAMESPACE b/NAMESPACE index c18633f2..e7d4dc17 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,7 @@ S3method(print,dash_component) export(Dash) +export(dashNoUpdate) export(input) export(output) export(state) diff --git a/R/dash.R b/R/dash.R index e073b982..f5750f22 100644 --- a/R/dash.R +++ b/R/dash.R @@ -287,11 +287,17 @@ Dash <- R6::R6Class( output_value <- getStackTrace(do.call(callback, callback_args), debug = private$debug, pruned_errors = private$pruned_errors) - + # reset callback context private$callback_context_ <- NULL - if (is.null(private$stack_message)) { + # 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") { + response$body <- character(1) # return empty string + response$status <- 204L + } + 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 # run plotly_build from the plotly package diff --git a/R/dependencies.R b/R/dependencies.R index f063c5d1..77ec8b77 100644 --- a/R/dependencies.R +++ b/R/dependencies.R @@ -5,11 +5,13 @@ #' Use in conjunction with the `callback()` method from the [dash::Dash] class #' to define the update logic in your application. #' +#' The `dashNoUpdate()` function permits application developers to prevent a +#' single output from updating the layout. It has no formal arguments. +#' #' @name dependencies #' @param id a component id #' @param property the component property to use - #' @rdname dependencies #' @export output <- function(id, property) { @@ -44,3 +46,11 @@ dependency <- function(id = NULL, property = NULL) { property = property ) } + +#' @rdname dependencies +#' @export +dashNoUpdate <- function() { + x <- list(NULL) + class(x) <- "no_update" + return(x) +} diff --git a/man/dependencies.Rd b/man/dependencies.Rd index b7dea530..c303bc1f 100644 --- a/man/dependencies.Rd +++ b/man/dependencies.Rd @@ -5,6 +5,7 @@ \alias{output} \alias{input} \alias{state} +\alias{dashNoUpdate} \title{Input/Output/State definitions} \usage{ output(id, property) @@ -12,6 +13,8 @@ output(id, property) input(id, property) state(id, property) + +dashNoUpdate() } \arguments{ \item{id}{a component id} @@ -22,3 +25,7 @@ state(id, property) Use in conjunction with the \code{callback()} method from the \link[dash:Dash]{dash::Dash} class to define the update logic in your application. } +\details{ +The \code{dashNoUpdate()} function permits application developers to prevent a +single output from updating the layout. It has no formal arguments. +} diff --git a/tests/integration/callbacks/test_no_update.py b/tests/integration/callbacks/test_no_update.py new file mode 100644 index 00000000..f8de0fe6 --- /dev/null +++ b/tests/integration/callbacks/test_no_update.py @@ -0,0 +1,111 @@ +from selenium.webdriver.support.select import Select +import time + +app = """ +library(dash) +library(dashHtmlComponents) +library(dashCoreComponents) + +app <- Dash$new() + +app$layout( + htmlDiv(list( + dccDropdown(options = list( + list(label = "Red", value = "#FF0000"), + list(label = "Green", value = "#00FF00"), + list(label = "Blue", value = "#0000FF"), + list(label = "Do nothing", value = "nothing") + ), + id = "color-selector"), + htmlButton(children = "Select all the colors!", + id = "multi-selector" + ), + htmlDiv(id='message-box', + children='Please select a color choice from the dropdown menu.'), + htmlDiv(id='message-box2', + children=' ') + ) + ) +) + +app$callback(output=list(id='message-box2', property='children'), + params=list( + input(id='multi-selector', property='n_clicks')), + function(n_clicks) + { + # if button has been clicked, n_clicks is numeric() + # on first launch of callback at layout initialization, + # value of n_clicks will be list(NULL), which is not + # comparable using >, < or =; hence the is.numeric() + # check + if (is.numeric(n_clicks) && n_clicks >= 1) + { + # return a vector to ensure that the check for + # class(x) == "no_update" isn't made for objects + # where length(x) > 1 + return(c("Multiple color values: ", + "#FF0000, ", + "#00FF00, ", + "#0000FF ", + "returned!") + ) + } + } +) + +app$callback(output=list(id='message-box', property='children'), + params=list( + input(id='color-selector', property='value')), + function(color) + { + if (color %in% c("#FF0000", "#00FF00", "#0000FF")) { + msg <- sprintf("The hexadecimal representation of your last chosen color is %s", + color) + return(msg) + } else { + return(dashNoUpdate()) + } + } +) + +app$run_server() +""" + + +def test_rsnu001_no_update(dashr): + dashr.start_server(app) + dashr.find_element("#color-selector").click() + dashr.find_elements("div.VirtualizedSelectOption")[0].click() + dashr.wait_for_text_to_equal( + "#message-box", + "The hexadecimal representation of your last chosen color is #FF0000" + ) + dashr.find_element("#color-selector").click() + dashr.find_elements("div.VirtualizedSelectOption")[3].click() + time.sleep(1) + assert dashr.find_element("#message-box").text == "The hexadecimal representation of your last chosen color is #FF0000" + dashr.find_element("#color-selector").click() + dashr.find_elements("div.VirtualizedSelectOption")[1].click() + dashr.wait_for_text_to_equal( + "#message-box", + "The hexadecimal representation of your last chosen color is #00FF00" + ) + dashr.find_element("#color-selector").click() + dashr.find_elements("div.VirtualizedSelectOption")[3].click() + time.sleep(1) + assert dashr.find_element("#message-box").text == "The hexadecimal representation of your last chosen color is #00FF00" + dashr.find_element("#color-selector").click() + dashr.find_elements("div.VirtualizedSelectOption")[2].click() + dashr.wait_for_text_to_equal( + "#message-box", + "The hexadecimal representation of your last chosen color is #0000FF" + ) + dashr.find_element("#color-selector").click() + dashr.find_elements("div.VirtualizedSelectOption")[3].click() + time.sleep(1) + assert dashr.find_element("#message-box").text == "The hexadecimal representation of your last chosen color is #0000FF" + dashr.find_element("#multi-selector").click() + dashr.wait_for_text_to_equal( + "#message-box2", + "Multiple color values: #FF0000, #00FF00, #0000FF returned!" + )