-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Assets files & index customizations #286
Changes from 23 commits
53ec2ef
a14709a
69b8adc
7cafb47
7ebab39
fc26544
d2cce95
1212ee5
846c8fc
4777731
23195f0
0c96dc7
f943471
cd0c15c
0d9151c
ff1cff3
eaf91a1
2197e38
ea0d2ca
4cf9e5c
f6e9922
8cc781b
0046402
6af0381
2c104a5
1171f0a
cd4cfa9
495762e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,11 +1,14 @@ | ||||||||||||||
from __future__ import print_function | ||||||||||||||
|
||||||||||||||
import os | ||||||||||||||
import sys | ||||||||||||||
import collections | ||||||||||||||
import importlib | ||||||||||||||
import json | ||||||||||||||
import pkgutil | ||||||||||||||
import warnings | ||||||||||||||
import re | ||||||||||||||
|
||||||||||||||
from functools import wraps | ||||||||||||||
|
||||||||||||||
import plotly | ||||||||||||||
|
@@ -19,6 +22,42 @@ | |||||||||||||
from .development.base_component import Component | ||||||||||||||
from . import exceptions | ||||||||||||||
from ._utils import AttributeDict as _AttributeDict | ||||||||||||||
from ._utils import interpolate_str as _interpolate | ||||||||||||||
|
||||||||||||||
_default_index = ''' | ||||||||||||||
<!DOCTYPE html> | ||||||||||||||
<html> | ||||||||||||||
<head> | ||||||||||||||
{%metas%} | ||||||||||||||
<title>{%title%}</title> | ||||||||||||||
{%favicon%} | ||||||||||||||
{%css%} | ||||||||||||||
</head> | ||||||||||||||
<body> | ||||||||||||||
{%app_entry%} | ||||||||||||||
<footer> | ||||||||||||||
{%config%} | ||||||||||||||
{%scripts%} | ||||||||||||||
</footer> | ||||||||||||||
</body> | ||||||||||||||
</html> | ||||||||||||||
''' | ||||||||||||||
|
||||||||||||||
_app_entry = ''' | ||||||||||||||
<div id="react-entry-point"> | ||||||||||||||
<div class="_dash-loading"> | ||||||||||||||
Loading... | ||||||||||||||
</div> | ||||||||||||||
</div> | ||||||||||||||
''' | ||||||||||||||
|
||||||||||||||
_re_index_entry = re.compile(r'{%app_entry%}') | ||||||||||||||
_re_index_config = re.compile(r'{%config%}') | ||||||||||||||
_re_index_scripts = re.compile(r'{%scripts%}') | ||||||||||||||
|
||||||||||||||
_re_index_entry_id = re.compile(r'id="react-entry-point"') | ||||||||||||||
_re_index_config_id = re.compile(r'id="_dash-config"') | ||||||||||||||
_re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"') | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
# pylint: disable=too-many-instance-attributes | ||||||||||||||
|
@@ -29,8 +68,13 @@ def __init__( | |||||||||||||
name='__main__', | ||||||||||||||
server=None, | ||||||||||||||
static_folder='static', | ||||||||||||||
assets_folder=None, | ||||||||||||||
assets_url_path='/assets', | ||||||||||||||
include_assets_files=True, | ||||||||||||||
url_base_pathname='/', | ||||||||||||||
compress=True, | ||||||||||||||
meta_tags=None, | ||||||||||||||
index_string=_default_index, | ||||||||||||||
**kwargs): | ||||||||||||||
|
||||||||||||||
# pylint-disable: too-many-instance-attributes | ||||||||||||||
|
@@ -42,20 +86,35 @@ def __init__( | |||||||||||||
See https://github.com/plotly/dash/issues/141 for details. | ||||||||||||||
''', DeprecationWarning) | ||||||||||||||
|
||||||||||||||
name = name or 'dash' | ||||||||||||||
self._assets_folder = assets_folder or os.path.join( | ||||||||||||||
flask.helpers.get_root_path(name), 'assets' | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
# allow users to supply their own flask server | ||||||||||||||
self.server = server or Flask(name, static_folder=static_folder) | ||||||||||||||
|
||||||||||||||
self.server.register_blueprint( | ||||||||||||||
flask.Blueprint('assets', 'assets', | ||||||||||||||
static_folder=self._assets_folder, | ||||||||||||||
static_url_path=assets_url_path)) | ||||||||||||||
|
||||||||||||||
self.url_base_pathname = url_base_pathname | ||||||||||||||
self.config = _AttributeDict({ | ||||||||||||||
'suppress_callback_exceptions': False, | ||||||||||||||
'routes_pathname_prefix': url_base_pathname, | ||||||||||||||
'requests_pathname_prefix': url_base_pathname | ||||||||||||||
'requests_pathname_prefix': url_base_pathname, | ||||||||||||||
'include_assets_files': include_assets_files, | ||||||||||||||
'assets_external_path': '', | ||||||||||||||
}) | ||||||||||||||
|
||||||||||||||
# list of dependencies | ||||||||||||||
self.callback_map = {} | ||||||||||||||
|
||||||||||||||
self._index_string = '' | ||||||||||||||
self.index_string = index_string | ||||||||||||||
self._meta_tags = meta_tags or [] | ||||||||||||||
self._favicon = None | ||||||||||||||
|
||||||||||||||
if compress: | ||||||||||||||
# gzip | ||||||||||||||
Compress(self.server) | ||||||||||||||
|
@@ -149,12 +208,26 @@ def layout(self, value): | |||||||||||||
# pylint: disable=protected-access | ||||||||||||||
self.css._update_layout(layout_value) | ||||||||||||||
self.scripts._update_layout(layout_value) | ||||||||||||||
self._collect_and_register_resources( | ||||||||||||||
self.scripts.get_all_scripts() | ||||||||||||||
) | ||||||||||||||
self._collect_and_register_resources( | ||||||||||||||
self.css.get_all_css() | ||||||||||||||
|
||||||||||||||
@property | ||||||||||||||
def index_string(self): | ||||||||||||||
return self._index_string | ||||||||||||||
|
||||||||||||||
@index_string.setter | ||||||||||||||
def index_string(self, value): | ||||||||||||||
checks = ( | ||||||||||||||
(_re_index_entry.search(value), 'app_entry'), | ||||||||||||||
(_re_index_config.search(value), 'config',), | ||||||||||||||
(_re_index_scripts.search(value), 'scripts'), | ||||||||||||||
) | ||||||||||||||
missing = [missing for check, missing in checks if not check] | ||||||||||||||
if missing: | ||||||||||||||
raise Exception( | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a Dash-specific exception from the |
||||||||||||||
'Did you forget to include {} in your index string ?'.format( | ||||||||||||||
', '.join('{%' + x + '%}' for x in missing) | ||||||||||||||
) | ||||||||||||||
) | ||||||||||||||
self._index_string = value | ||||||||||||||
|
||||||||||||||
def serve_layout(self): | ||||||||||||||
layout = self._layout_value() | ||||||||||||||
|
@@ -180,6 +253,7 @@ def serve_routes(self): | |||||||||||||
) | ||||||||||||||
|
||||||||||||||
def _collect_and_register_resources(self, resources): | ||||||||||||||
# now needs the app context. | ||||||||||||||
# template in the necessary component suite JS bundles | ||||||||||||||
# add the version number of the package as a query parameter | ||||||||||||||
# for cache busting | ||||||||||||||
|
@@ -217,8 +291,12 @@ def _relative_url_path(relative_package_path='', namespace=''): | |||||||||||||
srcs.append(url) | ||||||||||||||
elif 'absolute_path' in resource: | ||||||||||||||
raise Exception( | ||||||||||||||
'Serving files form absolute_path isn\'t supported yet' | ||||||||||||||
'Serving files from absolute_path isn\'t supported yet' | ||||||||||||||
) | ||||||||||||||
elif 'asset_path' in resource: | ||||||||||||||
static_url = flask.url_for('assets.static', | ||||||||||||||
filename=resource['asset_path']) | ||||||||||||||
srcs.append(static_url) | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should wire in cache-busting URLs while we're here. It's a really common issue in the community forum (e.g. https://community.plot.ly/t/reloading-css-automatically/11065). I originally thought that we could just do this with a query string with the last modified timestamp but it seems like that's not recommended (https://css-tricks.com/strategies-for-cache-busting-css/#article-header-id-2). Instead, it seems like we'd need to somehow encode it into the resource name. Do you have a sense of how hard that might be? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I'm being pretty hypocritical here, looks like I did cache busting with the component libraries with query strings: Lines 194 to 199 in 3dfa941
🙄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did cache busting before with webpack and an extension. Was basically a template render that replaced the hash part of a filename with a new one. In dash, I think it would be kinda hard to do that, but we could have a watcher on the asset folder, copy those assets with a filename including the hash to a temp static folder, keep the hash in a dict with the path as key and serve the file with the hash formatted in. When a file change, copy it to the temp folder with a new hash and put the new hash as the value for the path. Could also tell the browser to reload while we're at it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that's a good idea. I'm a little worried about introducing a temp folder, I feel like users won't know why it's there or if they should commit it, etc. Instead of having a watcher, could we just call this function on every page load (while in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The temp folder would be created with the But yea, just checking the timestamps of the files before index and appending that in a query string would be a quick fix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, sounds like there are a couple of options. So, let's create a new GitHub issue about this and tackle it in a subsequent PR |
||||||||||||||
return srcs | ||||||||||||||
|
||||||||||||||
def _generate_css_dist_html(self): | ||||||||||||||
|
@@ -260,6 +338,20 @@ def _generate_config_html(self): | |||||||||||||
'</script>' | ||||||||||||||
).format(json.dumps(self._config())) | ||||||||||||||
|
||||||||||||||
def _generate_meta_html(self): | ||||||||||||||
has_charset = any('charset' in x for x in self._meta_tags) | ||||||||||||||
|
||||||||||||||
tags = [] | ||||||||||||||
if not has_charset: | ||||||||||||||
tags.append('<meta charset="UTF-8"/>') | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is very thoughtful 👍 |
||||||||||||||
for meta in self._meta_tags: | ||||||||||||||
attributes = [] | ||||||||||||||
for k, v in meta.items(): | ||||||||||||||
attributes.append('{}="{}"'.format(k, v)) | ||||||||||||||
tags.append('<meta {} />'.format(' '.join(attributes))) | ||||||||||||||
|
||||||||||||||
return '\n '.join(tags) | ||||||||||||||
|
||||||||||||||
# Serve the JS bundles for each package | ||||||||||||||
def serve_component_suites(self, package_name, path_in_package_dist): | ||||||||||||||
if package_name not in self.registered_paths: | ||||||||||||||
|
@@ -294,28 +386,47 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument | |||||||||||||
scripts = self._generate_scripts_html() | ||||||||||||||
css = self._generate_css_dist_html() | ||||||||||||||
config = self._generate_config_html() | ||||||||||||||
metas = self._generate_meta_html() | ||||||||||||||
title = getattr(self, 'title', 'Dash') | ||||||||||||||
return ''' | ||||||||||||||
<!DOCTYPE html> | ||||||||||||||
<html> | ||||||||||||||
<head> | ||||||||||||||
<meta charset="UTF-8"> | ||||||||||||||
<title>{}</title> | ||||||||||||||
{} | ||||||||||||||
</head> | ||||||||||||||
<body> | ||||||||||||||
<div id="react-entry-point"> | ||||||||||||||
<div class="_dash-loading"> | ||||||||||||||
Loading... | ||||||||||||||
</div> | ||||||||||||||
</div> | ||||||||||||||
<footer> | ||||||||||||||
{} | ||||||||||||||
{} | ||||||||||||||
</footer> | ||||||||||||||
</body> | ||||||||||||||
</html> | ||||||||||||||
'''.format(title, css, config, scripts) | ||||||||||||||
if self._favicon: | ||||||||||||||
favicon = '<link rel="icon" type="image/x-icon" href="{}">'.format( | ||||||||||||||
flask.url_for('assets.static', filename=self._favicon)) | ||||||||||||||
else: | ||||||||||||||
favicon = '' | ||||||||||||||
|
||||||||||||||
index = self.interpolate_index( | ||||||||||||||
metas=metas, title=title, css=css, config=config, | ||||||||||||||
scripts=scripts, app_entry=_app_entry, favicon=favicon) | ||||||||||||||
|
||||||||||||||
checks = ( | ||||||||||||||
(_re_index_entry_id.search(index), '#react-entry-point'), | ||||||||||||||
(_re_index_config_id.search(index), '#_dash-configs'), | ||||||||||||||
(_re_index_scripts_id.search(index), 'dash-renderer'), | ||||||||||||||
) | ||||||||||||||
missing = [missing for check, missing in checks if not check] | ||||||||||||||
|
||||||||||||||
if missing: | ||||||||||||||
plural = 's' if len(missing) > 1 else '' | ||||||||||||||
raise Exception( | ||||||||||||||
'Missing element{pl} {ids} in index.'.format( | ||||||||||||||
ids=', '.join(missing), | ||||||||||||||
pl=plural | ||||||||||||||
) | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
return index | ||||||||||||||
|
||||||||||||||
def interpolate_index(self, | ||||||||||||||
metas='', title='', css='', config='', | ||||||||||||||
scripts='', app_entry='', favicon=''): | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's include a quick docstring for our users. Something like:
(but with the example filled out) |
||||||||||||||
return _interpolate(self.index_string, | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's probably not enough error-checking here... Shouldn't we raise a very helpful and clear message if they forget to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two options I thought of:
Which would be best ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would actually do both :) That way it fails fast for those using |
||||||||||||||
metas=metas, | ||||||||||||||
title=title, | ||||||||||||||
css=css, | ||||||||||||||
config=config, | ||||||||||||||
scripts=scripts, | ||||||||||||||
favicon=favicon, | ||||||||||||||
app_entry=app_entry) | ||||||||||||||
|
||||||||||||||
def dependencies(self): | ||||||||||||||
return flask.jsonify([ | ||||||||||||||
|
@@ -558,9 +669,47 @@ def dispatch(self): | |||||||||||||
return self.callback_map[target_id]['callback'](*args) | ||||||||||||||
|
||||||||||||||
def _setup_server(self): | ||||||||||||||
if self.config.include_assets_files: | ||||||||||||||
self._walk_assets_directory() | ||||||||||||||
|
||||||||||||||
self._generate_scripts_html() | ||||||||||||||
self._generate_css_dist_html() | ||||||||||||||
|
||||||||||||||
def _walk_assets_directory(self): | ||||||||||||||
walk_dir = self._assets_folder | ||||||||||||||
slash_splitter = re.compile(r'[\\/]+') | ||||||||||||||
|
||||||||||||||
def add_resource(p): | ||||||||||||||
res = {'asset_path': p} | ||||||||||||||
if self.config.assets_external_path: | ||||||||||||||
res['external_url'] = '{}{}'.format( | ||||||||||||||
self.config.assets_external_path, path) | ||||||||||||||
return res | ||||||||||||||
|
||||||||||||||
for current, _, files in os.walk(walk_dir): | ||||||||||||||
if current == walk_dir: | ||||||||||||||
base = '' | ||||||||||||||
else: | ||||||||||||||
s = current.replace(walk_dir, '').lstrip('\\').lstrip('/') | ||||||||||||||
splitted = slash_splitter.split(s) | ||||||||||||||
if len(splitted) > 1: | ||||||||||||||
base = '/'.join(slash_splitter.split(s)) | ||||||||||||||
else: | ||||||||||||||
base = splitted[0] | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NVM, I get it now, we have to replace |
||||||||||||||
|
||||||||||||||
for f in files: | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I had a hunch the test case was just getting lucky so I added more files and got this ordering: This can be fixed by just adding
This sorts the files as: I personally would prefer if 'load_after10' and 'load_after11' came last, which can be done by changing The former is what is expected, but the latter is what a programmer usually wants when sorting files. Either way I like the way everything else looks and am 💃 once the sorting works. |
||||||||||||||
if base: | ||||||||||||||
path = '/'.join([base, f]) | ||||||||||||||
else: | ||||||||||||||
path = f | ||||||||||||||
|
||||||||||||||
if f.endswith('js'): | ||||||||||||||
self.scripts.append_script(add_resource(path)) | ||||||||||||||
elif f.endswith('css'): | ||||||||||||||
self.css.append_css(add_resource(path)) | ||||||||||||||
elif f == 'favicon.ico': | ||||||||||||||
self._favicon = path | ||||||||||||||
|
||||||||||||||
def run_server(self, | ||||||||||||||
port=8050, | ||||||||||||||
debug=False, | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,4 @@ six | |
plotly>=2.0.8 | ||
requests[security] | ||
flake8 | ||
pylint | ||
pylint==1.9.2 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
window.tested = ['load_first']; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#content { | ||
padding: 8px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
window.tested.push('load_after'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
window.tested.push('load_after1'); | ||
document.getElementById('tested').innerHTML = JSON.stringify(window.tested); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
body {margin: 0;} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also create an issue about adding a
css_urls
andjs_script_urls
arguments to this init argument as an alternative to ourapp.css.append_css
andapp.scripts.append_script
:(from comment #286)