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

[WIP] Inspect environment variables and set routing prefixes, add catchall endpoint #53

Merged
merged 43 commits into from
Mar 9, 2019

Conversation

rpkyle
Copy link
Contributor

@rpkyle rpkyle commented Feb 21, 2019

This PR will introduce a 'catchall' endpoint /*, which will be added as the final handler in the routr stack.

Additionally, DashR does not inspect local environment variables when initializing an app; routes_pathname_prefix and requests_pathname_prefix must be set manually. For greater parity with Dash for Python, the following route-related variables should be checked before setting route prefixes to NULL:

  • DASH_URL_BASE_PATHNAME
  • DASH_ROUTES_PATHNAME_PREFIX
  • DASH_REQUESTS_PATHNAME_PREFIX

Two additional helper functions are proposed in this PR:

  • get_package_mapping: given path to dependency, package string from request URL, and list of dependencies, attempts to build a package_map. The mapping is then used to resolve local paths to dependencies within the R packages that provide them. A list is returned with the R package name providing the dependency, as well as a local path to the file.
  • get_mimetype: resolve dependency MIME type based on file extension, currently supports application/JavaScript, application/json, text/css.

Lastly, previous versions of DashR did not include a _dash-component-suites handler. This PR also

  • eliminates static handling of routes for Dash components served upon index loading using ressource_route (which resolves a duplicate route error for the React JS upon relaunching an existing DashR app in the current workspace)
  • related to item above, 🔪 dashR::resolve_dependencies and register_dependencies
  • constructs script tags manually, rather than with htmltools
  • provides a dash_suite handler to serve dependency files, following the Dash convention
  • provides support for inserting the DashR app name into dependency pathnames
  • provides support for including a Cache-Control header, and setting the cache lifetime via components_cache_max_age
  • more careful CSS handling
  • treats dependencies bundled into DashR (i.e. dash_renderer, react, react-dom) as being part of the dash_renderer "package", to mirror behaviour in Dash for Python
  • some basic sanity checking for dependencies: removing multiple slashes in pathnames, ensuring that source maps aren't being served until user interacts with debugger, checking for duplicate dependencies

This PR is a work in progress, aiming to implement similar behaviour as found in _configs.py:

https://github.com/plotly/dash/blob/f590166be2ca476c8970512fd9b23aaf26541ae8/dash/_configs.py#L8

...and in dash.py:

https://github.com/plotly/dash/blob/1249ffbd051bfb5fdbe439612cbec7fa8fff5ab5/dash/dash.py#L488

Closes #52.

@rpkyle rpkyle changed the title [WIP] Inspect environmental variables and set routing prefixes as needed [WIP] Inspect environment variables and set routing prefixes as needed Feb 21, 2019
@rpkyle rpkyle changed the title [WIP] Inspect environment variables and set routing prefixes as needed [WIP] Inspect environment variables and set routing prefixes, add catchall endpoint Feb 21, 2019
@rpkyle rpkyle self-assigned this Feb 21, 2019
@rpkyle rpkyle added this to the DashR Launch milestone Feb 21, 2019
@chriddyp
Copy link
Member

I think let's 🔪 DASH_URL_BASE_PATHNAME - We've only included it for backwards compatible reasons in dash.py. Instead, we can force users to be more explicit by setting both requests and routes prefixes

R/dash.R Outdated
@@ -268,7 +268,14 @@ Dash <- R6::R6Class(
TRUE
})


route$add_handler('get', '/*', function(request, response, keys, ...) {
Copy link
Member

Choose a reason for hiding this comment

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

Did you have to move this around so that it gets declared after the other routes or does it work like this?

Copy link
Member

Choose a reason for hiding this comment

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

Oh nvm, the original route wasn't removed: https://github.com/plotly/dashR/pull/53/files#diff-1881af3720f8f49fd3a44e51c9fb5a0dR180

Let's 🔪 the original route since /* should catch /.

Also, this route needs to be prefixed by routes_pathname_prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I noticed that when I glanced at the Python code but forgot to insert here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in 62d7dd9

R/dash.R Outdated
@@ -370,8 +377,32 @@ Dash <- R6::R6Class(
# private fields defined on initiation
name = NULL,
serve_locally = NULL,
routes_pathname_prefix = NULL,
requests_pathname_prefix = NULL,
routes_pathname_prefix = {
Copy link
Member

Choose a reason for hiding this comment

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

This syntax is interesting - is this like a function that gets called whenever this variable is accessed? Or is it just a function shorthand?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I realized this isn't going to work the way we want, so I've 🔨 a bit, hopefully the refactored implementation of the routes/requests pathname prefixes is closer to the Python logic.

R/dash.R Outdated
routes_pathname_prefix = NULL,
requests_pathname_prefix = NULL,
routes_pathname_prefix = {
routes_prefix <- Sys.getenv("DASH_ROUTES_PATHNAME_PREFIX")
Copy link
Member

Choose a reason for hiding this comment

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

These variables are also defined in the constructor:

routes_pathname_prefix = '/',
requests_pathname_prefix = '/',

The order of preference should be:

  1. Did the user supply it in the constructor? If so, then use that.
  2. If not, then check the environment
  3. If not in the environment, then use the default.

In Python, for (1), the only way to check if a user supplied it while keeping a default argument was to set that default argument to None. That way, if it's not None, then we can assume that the user set it. (If the default was /, then we wouldn't know whether the user explicitly supplied that or if its just the default).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, perfect. This makes sense. I’ll just use NULL instead of None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in 5fba10f

@rpkyle
Copy link
Contributor Author

rpkyle commented Feb 21, 2019

I think let's 🔪 DASH_URL_BASE_PATHNAME - We've only included it for backwards compatible reasons in dash.py. Instead, we can force users to be more explicit by setting both requests and routes prefixes

fixed in 5fba10f

@rpkyle
Copy link
Contributor Author

rpkyle commented Mar 7, 2019

Previously, we encountered an error: subscript out of bounds message in the R console when attempting to pass a NULL value to the callback handler. This caused issues with _dash-update-component.

This is possible when using a component like coreLocation, in which an ID may be supplied without a default value. The original code:

callback_args <- lapply(c(request$body$inputs, request$body$state), `[[`, 3)

...has been replaced with:

dashR/R/dash.R

Lines 244 to 260 in 7fa2581

callback_args <- list()
for (input_element in request$body$inputs) {
if(is.null(input_element$value))
callback_args <- c(callback_args, list(list(NULL)))
else
callback_args <- c(callback_args, input_element$value)
}
if (length(request$body$state)) {
for (state_element in request$body$state) {
if(is.null(state_element$value))
callback_args <- c(callback_args, list(list(NULL)))
else
callback_args <- c(callback_args, state_element$value)
}
}

The new code handles inputs which may contain NULL values, which we wish to retain. In R, assigning NULL into a list element removes the element from a list, while appending NULL inserts into an existing list.

https://cran.r-project.org/doc/FAQ/R-FAQ.html#Others:

@rpkyle rpkyle requested a review from chriddyp March 7, 2019 22:38
@rpkyle
Copy link
Contributor Author

rpkyle commented Mar 7, 2019

I've tried to address the most recent round of PR comments, and I think I've licked the _dash-update-component error Nadia mentioned. If it's possible to review and consider 💃 this one, I'm ready to tackle other issues 🙂

@chriddyp @alexcjohnson @NadiaTahiri @nicolaskruchten

version = "16.2.0",
src = list(href = "https://unpkg.com/react@16.2.0",
file = "lib/react@16.2.0"),
meta = NULL,
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this is just white space removal, but a few things strike me as odd about this code:

  1. "react-prod" is react-16. This is different than the python side - we allow users to switch to react 16 optionally but it's not a "prod" vs "dev" distinction.
  2. On the python side, we handle this in dash-renderer using the same structure as the rest of the component repos (https://github.com/plotly/dash-renderer/blob/1c7c75d3127a742093f59bd34ea23b9a84be1ac9/dash_renderer/__init__.py). I would be worried about DashR handling these versions independently of dash-renderer as we don't want DashPy to be distributed with a different version than DashR

This is non-blocking and not a high priority, but something to maybe break out into its own issue and consider down the line.

Copy link
Contributor Author

@rpkyle rpkyle Mar 8, 2019

Choose a reason for hiding this comment

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

I should add a debug=TRUE option to DashR, so that component developers can optionally request specific (development) versions of dependencies, replicating functionality currently available in Dash for Python.

This item is on the roadmap for DashR, but I'll open an issue more clearly outlining what's required, and link here for future reference.

R/utils.R Outdated
"/",
paste(dep$src$file,
dep$script,
sep = "/")
Copy link
Member

Choose a reason for hiding this comment

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

If this is only used as a file path, could we use file.path? I recognize that it's being used below, but just to get into the habit of never constructing file paths by hand.

(unless this variable is also used as a script tag / url pathname? By reading the code below it didn't seem like it but I might've missed something)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in 3a61d02

R/utils.R Outdated
modified <- as.integer(Sys.time())
}

# we don't want to serve the JavaScript source maps here
Copy link
Member

Choose a reason for hiding this comment

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

maybe add a comment like "until we have full support for a debug=TRUE mode"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. I could open an issue for this as well, I'll have a look at what's needed to approach parity based on what's currently available in Dash for Python.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in b6af99b

R/utils.R Outdated
sep = "/")

# remove n>1 slashes and replace with / if present
Copy link
Member

Choose a reason for hiding this comment

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

could you prefix this comment with something like "htmltools sometimes includes multiple // - replace these with /".
That way, the reader knows that // is coming from htmltools and not necessarily the dash-renderer API. In the future if you refactor out htmltools, then the reader will know that this // won't need to be handled because htmltools was the source of the issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've started the process of 🔪 htmltools, managed to remove nearly all references to this package, but not quite there yet. I opened an issue earlier this week which attempts to enumerate all the spots in the code and comments that are involved:

#54

Copy link
Member

Choose a reason for hiding this comment

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

Yeah - My suggestion here is mostly just to get in the habit of writing these types of commits - if there is something in the code that looks odd, it's good to get in the habit of explaining why we have to do stuff like this and where the problem originated.

In this case, everyone involved in this PR is now aware of the issue and you will be the one refactoring it shortly. However, imagine the case where this refactor might happen 6 months from now by someone else. If they didn't know that this // was originating from htmltools, then they might not feel comfortable removing this block of code. Perhaps they might think that dash-renderer (a 3rd party piece of code) sometimes introduces a // in certain scenarios, scenarios that might not necessarily be tested in integration tests.

So, it's not strictly necessary in this scenario where we plan on fixing this up in a couple of weeks, but it's a good habit to get into and a healthy way to start thinking about comments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For sure, decisions like these seem apparent now that I'm staring at the same code over and over again, but for anyone else (or me even a year from now), statements like this are hard to comprehend in total isolation.

I'll be a bit more diligent about making these sorts of commits, it benefits everyone from the author, to the reviewer, and even savvy users who decide to take a look under the hood.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in b77c94d

Copy link
Member

@chriddyp chriddyp left a comment

Choose a reason for hiding this comment

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

Thanks for working through all of these suggestions!

💃 Once you've considered a few small suggestions concerning comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

need a catchall endpoint
3 participants