From b8dfc38f6f755209c251565f28fdd4c9845aab5f Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Thu, 23 May 2019 19:58:44 +1000 Subject: [PATCH 1/5] Add separate init_app function for providing the flask app --- dash/dash.py | 135 +++++++++++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index eab4fd7284..36d372d54d 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -108,8 +108,13 @@ def __init__( components_cache_max_age=None, show_undo_redo=False, plugins=None, + defer_app=False, **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(''' @@ -154,22 +159,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 +170,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 +184,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 not defer_app: + 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 +287,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, From 644a67eec5c19a5fc82677822b72a94a5ab84191 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Thu, 23 May 2019 20:02:21 +1000 Subject: [PATCH 2/5] Update changelog --- dash/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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) From 17ea03ef64073fab5c6e37cfae0bfb5ae19ba72a Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Mon, 27 May 2019 18:04:29 +1000 Subject: [PATCH 3/5] Remove defer_app kwarg, and overload server instead --- dash/dash.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 36d372d54d..ad5cf354f2 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -108,7 +108,6 @@ def __init__( components_cache_max_age=None, show_undo_redo=False, plugins=None, - defer_app=False, **kwargs): # Store some flask-related parameters for use in init_app() @@ -131,7 +130,13 @@ 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) + if server: + if isinstance(server, Flask): + self.server = server + else: + self.server = Flask(name, static_folder=static_folder) + else: + self.server = None url_base_pathname, routes_pathname_prefix, requests_pathname_prefix = \ pathname_configs( @@ -212,7 +217,7 @@ def __init__( for plugin in plugins: plugin.plug(self) - if not defer_app: + if self.server is not None: self.init_app() def init_app(self, app=None): From f61767ba7d5703b01342479bab8ad520a1060ae3 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Tue, 28 May 2019 02:34:15 +1000 Subject: [PATCH 4/5] Server can be True, False, or Flask() --- dash/dash.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index ad5cf354f2..53267f8e25 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', @@ -129,14 +129,17 @@ def __init__( ) self._assets_url_path = assets_url_path - # allow users to supply their own flask server - if server: - if isinstance(server, Flask): - self.server = server - else: + # 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: - self.server = None + raise ValueError('server must be a Flask app, or a boolean') url_base_pathname, routes_pathname_prefix, requests_pathname_prefix = \ pathname_configs( From e6853e7f5074402874d0758145a84fc2c45ac613 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Tue, 28 May 2019 02:42:41 +1000 Subject: [PATCH 5/5] The most grevious sin --- dash/dash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 53267f8e25..1e31a74ffd 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -129,8 +129,8 @@ def __init__( ) self._assets_url_path = assets_url_path - # 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) + # 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)