diff --git a/dash/CHANGELOG.md b/dash/CHANGELOG.md index 0b20648f55..ad5d1a350c 100644 --- a/dash/CHANGELOG.md +++ b/dash/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased ### Changed +- [#739](https://github.com/plotly/dash/pull/739) Allow the Flask app to be provided to Dash after object initialization. This allows users to define Dash layouts etc when using the app factory pattern, or any other pattern that inhibits access to the app object. This broadly complies with the flask extension API, allowing Dash to be considered as a Flask extension where it needs to be. + - [#722](https://github.com/plotly/dash/pull/722) Assets are served locally by default. Both JS scripts and CSS files are affected. This improves robustness and flexibility in numerous situations, but in certain cases initial loading could be slowed. To restore the previous CDN serving, set `app.scripts.config.serve_locally = False` (and similarly with `app.css`, but this is generally less important). - Undo/redo toolbar is removed by default, you can enable it with `app=Dash(show_undo_redo=true)`. The CSS hack `._dash-undo-redo:{display:none;}` is no longer needed [#724](https://github.com/plotly/dash/pull/724) diff --git a/dash/dash.py b/dash/dash.py index eab4fd7284..1e31a74ffd 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -89,7 +89,7 @@ class Dash(object): def __init__( self, name='__main__', - server=None, + server=True, static_folder='static', assets_folder='assets', assets_url_path='/assets', @@ -110,6 +110,10 @@ def __init__( plugins=None, **kwargs): + # Store some flask-related parameters for use in init_app() + self.compress = compress + self.name = name + # pylint-disable: too-many-instance-attributes if 'csrf_protect' in kwargs: warnings.warn(''' @@ -125,8 +129,17 @@ def __init__( ) self._assets_url_path = assets_url_path - # allow users to supply their own flask server - self.server = server or Flask(name, static_folder=static_folder) + # We have 3 cases: server is either True (we create the server), False + # (defer server creation) or a Flask app instance (we use their server) + if isinstance(server, bool): + if server: + self.server = Flask(name, static_folder=static_folder) + else: + self.server = None + elif isinstance(server, Flask): + self.server = server + else: + raise ValueError('server must be a Flask app, or a boolean') url_base_pathname, routes_pathname_prefix, requests_pathname_prefix = \ pathname_configs( @@ -154,22 +167,6 @@ def __init__( 'show_undo_redo': show_undo_redo }) - assets_blueprint_name = '{}{}'.format( - self.config.routes_pathname_prefix.replace('/', '_'), - 'dash_assets' - ) - - self.server.register_blueprint( - flask.Blueprint( - assets_blueprint_name, name, - static_folder=self._assets_folder, - static_url_path='{}{}'.format( - self.config.routes_pathname_prefix, - assets_url_path.lstrip('/') - ) - ) - ) - # list of dependencies self.callback_map = {} @@ -181,15 +178,6 @@ def __init__( # default renderer string self.renderer = 'var renderer = new DashRenderer();' - if compress: - # gzip - Compress(self.server) - - @self.server.errorhandler(exceptions.PreventUpdate) - def _handle_error(_): - """Handle a halted callback and return an empty 204 response""" - return '', 204 - # static files from the packages self.css = Css() self.scripts = Scripts() @@ -204,8 +192,79 @@ def _handle_error(_): # urls self.routes = [] + self._layout = None + self._cached_layout = None + self._dev_tools = _AttributeDict({ + 'serve_dev_bundles': False, + 'hot_reload': False, + 'hot_reload_interval': 3000, + 'hot_reload_watch_interval': 0.5, + 'hot_reload_max_retry': 8, + 'ui': False, + 'props_check': False, + }) + + self._assets_files = [] + + # hot reload + self._reload_hash = None + self._hard_reload = False + self._lock = threading.RLock() + self._watch_thread = None + self._changed_assets = [] + + self.logger = logging.getLogger(name) + self.logger.addHandler(logging.StreamHandler(stream=sys.stdout)) + + if isinstance(plugins, _patch_collections_abc('Iterable')): + for plugin in plugins: + plugin.plug(self) + + if self.server is not None: + self.init_app() + + def init_app(self, app=None): + """ + Initialize the parts of Dash that require a flask app + """ + + if app is not None: + self.server = app + + assets_blueprint_name = '{}{}'.format( + self.config.routes_pathname_prefix.replace('/', '_'), + 'dash_assets' + ) + + self.server.register_blueprint( + flask.Blueprint( + assets_blueprint_name, + self.name, + static_folder=self._assets_folder, + static_url_path='{}{}'.format( + self.config.routes_pathname_prefix, + self._assets_url_path.lstrip('/') + ) + ) + ) + + if self.compress: + # gzip + Compress(self.server) + + @self.server.errorhandler(exceptions.PreventUpdate) + def _handle_error(_): + """Handle a halted callback and return an empty 204 response""" + return '', 204 + prefix = self.config['routes_pathname_prefix'] + self.server.before_first_request(self._setup_server) + + # add a handler for components suites errors to return 404 + self.server.errorhandler(exceptions.InvalidResourceError)( + self._invalid_resources_handler) + self._add_url('{}_dash-layout'.format(prefix), self.serve_layout) self._add_url('{}_dash-dependencies'.format(prefix), self.dependencies) @@ -236,40 +295,6 @@ def _handle_error(_): '{}_favicon.ico'.format(prefix), self._serve_default_favicon) - self.server.before_first_request(self._setup_server) - - self._layout = None - self._cached_layout = None - self._dev_tools = _AttributeDict({ - 'serve_dev_bundles': False, - 'hot_reload': False, - 'hot_reload_interval': 3000, - 'hot_reload_watch_interval': 0.5, - 'hot_reload_max_retry': 8, - 'ui': False, - 'props_check': False, - }) - - # add a handler for components suites errors to return 404 - self.server.errorhandler(exceptions.InvalidResourceError)( - self._invalid_resources_handler) - - self._assets_files = [] - - # hot reload - self._reload_hash = None - self._hard_reload = False - self._lock = threading.RLock() - self._watch_thread = None - self._changed_assets = [] - - self.logger = logging.getLogger(name) - self.logger.addHandler(logging.StreamHandler(stream=sys.stdout)) - - if isinstance(plugins, _patch_collections_abc('Iterable')): - for plugin in plugins: - plugin.plug(self) - def _add_url(self, name, view_func, methods=('GET',)): self.server.add_url_rule( name,