Repository: bokeh/bokeh

Issue:
Implement --develop mode
Also document what `--develop` does.

`tornado` does not seem to do a good job in tracking dependencies all the way to the bokeh app, and does not restart when the bokeh app source changes.

PS Why was `tornado` chosen as a web server in the first place? E.g. `flask` does a brilliant job at serving simple WSGI apps, doesn't to require the use of non-blocking event-loop paradigms, whereas `tornado` can be shown to becomes unstable when these paradigms are violated.



Discussion:
`--develop` _is_ the option for auto reload. It has not been implemented yet, and it's inclusion in the server CLI was an accidental oversight. I actually can't find an issue for it now though, so I am renaming this one.


Resolving patch:
diff --git a/bokeh/command/subcommands/serve.py b/bokeh/command/subcommands/serve.py
--- a/bokeh/command/subcommands/serve.py
+++ b/bokeh/command/subcommands/serve.py
@@ -309,7 +309,9 @@
 from bokeh.server.tornado import DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES
 from bokeh.settings import settings
 
-from os import getpid
+from tornado.autoreload import watch
+import os
+from fnmatch import fnmatch
 
 from ..subcommand import Subcommand
 from ..util import build_single_handler_applications, die, report_server_init_errors
@@ -482,6 +484,19 @@ class Serve(Subcommand):
             type=int,
         )),
 
+        ('--dev', dict(
+            metavar ='FILES-TO-WATCH',
+            action  ='store',
+            default = None,
+            type    = str,
+            nargs   = '*',
+            help    =   "Enable live reloading during app development."
+                        "By default it watches all *.py *.html *.css *.yaml files"
+                        "in the app directory tree. Additional files can be passed"
+                        "as arguments."
+                        "NOTE: This setting only works with a single app."
+                        "It also restricts the number of processes to 1.",
+        )),
     )
 
 
@@ -558,6 +573,39 @@ def invoke(self, args):
 
         server_kwargs['use_index'] = not args.disable_index
         server_kwargs['redirect_root'] = not args.disable_index_redirect
+        server_kwargs['autoreload'] = args.dev is not None
+
+        def find_autoreload_targets(app_path):
+            path = os.path.abspath(app_path)
+            if not os.path.isdir(path):
+                return
+
+            for path, subdirs, files in os.walk(path):
+                for name in files:
+                    if (fnmatch(name, '*.html') or
+                        fnmatch(name, '*.css') or
+                        fnmatch(name, '*.yaml')):
+                        log.info("Watching: " + os.path.join(path, name))
+                        watch(os.path.join(path, name))
+
+        def add_optional_autoreload_files(file_list):
+            for filen in file_list:
+                if os.path.isdir(filen):
+                    log.warn("Cannot watch directory " + filen)
+                    continue
+                log.info("Watching: " + filen)
+                watch(filen)
+
+        if server_kwargs['autoreload']:
+            if len(applications.keys()) != 1:
+                die("--dev can only support a single app.")
+            if server_kwargs['num_procs'] != 1:
+                log.info("Running in --dev mode. --num-procs is limited to 1.")
+                server_kwargs['num_procs'] = 1
+
+            find_autoreload_targets(args.files[0])
+            add_optional_autoreload_files(args.dev)
+
 
         with report_server_init_errors(**server_kwargs):
             server = Server(applications, **server_kwargs)
@@ -577,5 +625,5 @@ def show_callback():
                 url = "http://%s:%d%s%s" % (address_string, server.port, server.prefix, route)
                 log.info("Bokeh app running at: %s" % url)
 
-            log.info("Starting Bokeh server with process id: %d" % getpid())
+            log.info("Starting Bokeh server with process id: %d" % os.getpid())
             server.run_until_shutdown()


Test patch:
diff --git a/bokeh/command/subcommands/tests/test_serve.py b/bokeh/command/subcommands/tests/test_serve.py
--- a/bokeh/command/subcommands/tests/test_serve.py
+++ b/bokeh/command/subcommands/tests/test_serve.py
@@ -181,6 +181,20 @@ def test_args():
             default=20*1024*1024,
             type=int,
         )),
+
+        ('--dev', dict(
+            metavar ='FILES-TO-WATCH',
+            action  ='store',
+            default = None,
+            type    = str,
+            nargs   = '*',
+            help    =   "Enable live reloading during app development."
+                        "By default it watches all *.py *.html *.css *.yaml files"
+                        "in the app directory tree. Additional files can be passed"
+                        "as arguments."
+                        "NOTE: This setting only works with a single app."
+                        "It also restricts the number of processes to 1.",
+        )),
     )