Repository: plotly/dash

Issue:
[Feature Request] Support the Flask Extension API
## Motivation
As described [here](http://flask.pocoo.org/docs/1.0/extensions/#using-extensions), the average flask extension is an object that can be instantiated *without* access to the flask app, e.g. `foo = Foo()`, and then later is given access to the app using `foo.init_app(app)`. This is required in situations where you are using the [app factory pattern](http://flask.pocoo.org/docs/1.0/patterns/appfactories/), where you only have access to the app inside the factory function, but you need to be able to define your extension outside this scope.

## Proposed Changes
Fortunately, Dash *almost* meets those requirements. We just need:
* To be able to instantiate an incomplete Dash app without passing it a Flask app. Currently if we try to do so, Dash will generate its own Flask app for us, which is not what we want in this situation. Thus I'm suggesting some syntax like `dash = dash.Dash(defer=True)`, which would postpone most of the init logic until it has access to a Flask app
* To be able to provide the flask app at a later time. For this, I propose the standard Flask extension syntax: `dash.init_app(app)`, which gives dash a copy of the app, and lets it run any deferred init logic that it wasn't able to complete at creation time.

## Relation to other Issues
This relates to #38, except that I'm trying to provide a pragmatic solution that would let Dash function as a flask extension, without requiring broad API changes.


Discussion:

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- 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,


Test patch: