Repository: bokeh/bokeh

Issue:
DirectoryHandler does not handle ipynb files correctly
The documentation says that the ``DirectoryHandler`` allows serving main.ipynb notebook files inside a directory and the code does have codepaths that mention notebooks. However the implementation tries to load notebook files as if they were normal scripts, leading to immediate errors because notebook files are actually JSON. If the DirectoryHandler finds an ipynb file it should apply the same nbconvert transform used by the NotebookHandler.


Discussion:

Resolving patch:
diff --git a/bokeh/application/handlers/directory.py b/bokeh/application/handlers/directory.py
--- a/bokeh/application/handlers/directory.py
+++ b/bokeh/application/handlers/directory.py
@@ -55,6 +55,7 @@
 
 # Bokeh imports
 from .handler import Handler
+from .notebook import NotebookHandler
 from .script import ScriptHandler
 from .server_lifecycle import ServerLifecycleHandler
 
@@ -106,7 +107,9 @@ def __init__(self, *args, **kwargs):
             raise ValueError("No 'main.py' or 'main.ipynb' in %s" % (src_path))
         self._path = src_path
         self._main = main
-        self._main_handler = ScriptHandler(filename=self._main, argv=argv)
+
+        handler = NotebookHandler if main.endswith('.ipynb') else ScriptHandler
+        self._main_handler = handler(filename=self._main, argv=argv)
 
         lifecycle = join(src_path, 'server_lifecycle.py')
         if exists(lifecycle):


Test patch:
diff --git a/bokeh/application/handlers/tests/test_directory.py b/bokeh/application/handlers/tests/test_directory.py
--- a/bokeh/application/handlers/tests/test_directory.py
+++ b/bokeh/application/handlers/tests/test_directory.py
@@ -105,6 +105,49 @@ def load(filename):
 
         assert len(doc.roots) == 2
 
+    def test_directory_empty_mainipynb(self):
+        import nbformat
+
+        doc = Document()
+        source = nbformat.v4.new_notebook()
+        result = {}
+        def load(filename):
+            handler = bahd.DirectoryHandler(filename=filename)
+            handler.modify_document(doc)
+            result['handler'] = handler
+            result['filename'] = filename
+            if handler.failed:
+                raise RuntimeError(handler.error)
+
+        with_directory_contents({
+            'main.ipynb': nbformat.writes(source)
+        }, load)
+
+        assert not doc.roots
+
+    def test_directory_mainipynb_adds_roots(self):
+        import nbformat
+
+        doc = Document()
+        source = nbformat.v4.new_notebook()
+        code = script_adds_two_roots('SomeModelInNbTestDirectory',
+                                     'AnotherModelInNbTestDirectory')
+        source.cells.append(nbformat.v4.new_code_cell(code))
+        result = {}
+        def load(filename):
+            handler = bahd.DirectoryHandler(filename=filename)
+            handler.modify_document(doc)
+            result['handler'] = handler
+            result['filename'] = filename
+            if handler.failed:
+                raise RuntimeError(handler.error)
+
+        with_directory_contents({
+            'main.ipynb': nbformat.writes(source)
+        }, load)
+
+        assert len(doc.roots) == 2
+
     def test_directory_has_theme_file(self):
         doc = Document()
         def load(filename):