Repository: bokeh/bokeh

Issue:
Add option to extend sys.path with app directory
(I thought there was already an issue for this, but I could not find it)

After some investigation I do not think it's possible (or even possibly desirable) to have things like 

```
from .foo import bar
```

work from the app directory. AFIAK Jupyter notebooks do not try to create a package, they just add the notebook directory to `sys.path` and then 

```
from foo import bar
```

is possible. I propose to do the same:

```
bokeh serve --modify-sys-path=yes
```

UPDATE: 

After a little more research, I believe the most proper thing to do is to prepend `''` into `sys.path` and set the CWD to the app location before `exec`, then undo those things. That seems to be exactly consistent with standard python behavior as described here: https://docs.python.org/2/library/sys.html#sys.path



Discussion:

Resolving patch:
diff --git a/bokeh/application/handlers/code_runner.py b/bokeh/application/handlers/code_runner.py
--- a/bokeh/application/handlers/code_runner.py
+++ b/bokeh/application/handlers/code_runner.py
@@ -1,6 +1,6 @@
 from __future__ import absolute_import, print_function
 
-from os.path import abspath
+from os.path import abspath, dirname
 from types import ModuleType
 import os
 import sys
@@ -68,8 +68,17 @@ def new_module(self):
 
     def run(self, module, post_check):
         try:
+            # Simulate the sys.path behaviour decribed here:
+            #
+            # https://docs.python.org/2/library/sys.html#sys.path
+            _cwd = os.getcwd()
+            _sys_path = list(sys.path)
+            os.chdir(dirname(self._path))
+            sys.path.insert(0, '')
+
             exec(self._code, module.__dict__)
             post_check()
+
         except Exception as e:
             self._failed = True
             self._error_detail = traceback.format_exc()
@@ -78,3 +87,8 @@ def run(self, module, post_check):
             filename, line_number, func, txt = traceback.extract_tb(exc_traceback)[-1]
 
             self._error = "%s\nFile \"%s\", line %d, in %s:\n%s" % (str(e), os.path.basename(filename), line_number, func, txt)
+
+        finally:
+            # undo sys.path, CWD fixups
+            os.chdir(_cwd)
+            sys.path = _sys_path
diff --git a/examples/app/crossfilter/main.py b/examples/app/crossfilter/main.py
--- a/examples/app/crossfilter/main.py
+++ b/examples/app/crossfilter/main.py
@@ -10,11 +10,6 @@
 from bokeh.plotting import Figure
 from bokeh.sampledata.autompg import autompg
 
-# this is to be able to import modules from the app directory
-import sys
-from os.path import dirname
-sys.path.append(dirname(__file__))
-
 from models import StyleableBox, StatsBox
 from models.helpers import load_component
 
diff --git a/examples/app/crossfilter/models/models.py b/examples/app/crossfilter/models/models.py
--- a/examples/app/crossfilter/models/models.py
+++ b/examples/app/crossfilter/models/models.py
@@ -3,11 +3,7 @@
 from bokeh.core import validation
 from bokeh.core.validation.warnings import EMPTY_LAYOUT
 
-import sys
-from os.path import dirname
-sys.path.append(dirname(__file__))
-
-from helpers import load_component
+from .helpers import load_component
 
 class StyleableBox(BaseBox):
     '''
@@ -21,7 +17,7 @@ class StatsBox(BaseBox):
     __implementation__ = load_component('./stats_box.coffee')
     styles = String(default=None)
     display_items = Dict(String, Any, default=None)
-    
+
     @validation.warning(EMPTY_LAYOUT)
     def _check_empty_layout(self):
         pass


Test patch:
diff --git a/bokeh/application/handlers/tests/test_code.py b/bokeh/application/handlers/tests/test_code.py
--- a/bokeh/application/handlers/tests/test_code.py
+++ b/bokeh/application/handlers/tests/test_code.py
@@ -40,7 +40,7 @@ class TestCodeHandler(unittest.TestCase):
 
     def test_empty_script(self):
         doc = Document()
-        handler = CodeHandler("# This script does nothing", "test_filename")
+        handler = CodeHandler("# This script does nothing", "/test_filename")
         handler.modify_document(doc)
         if handler.failed:
             raise RuntimeError(handler.error)
@@ -49,7 +49,7 @@ def test_empty_script(self):
 
     def test_script_adds_roots(self):
         doc = Document()
-        handler = CodeHandler(script_adds_two_roots, "test_filename")
+        handler = CodeHandler(script_adds_two_roots, "/test_filename")
         handler.modify_document(doc)
         if handler.failed:
             raise RuntimeError(handler.error)
@@ -58,7 +58,7 @@ def test_script_adds_roots(self):
 
     def test_script_bad_syntax(self):
         doc = Document()
-        handler = CodeHandler("This is a syntax error", "test_filename")
+        handler = CodeHandler("This is a syntax error", "/test_filename")
         handler.modify_document(doc)
 
         assert handler.error is not None
@@ -66,8 +66,24 @@ def test_script_bad_syntax(self):
 
     def test_script_runtime_error(self):
         doc = Document()
-        handler = CodeHandler("raise RuntimeError('nope')", "test_filename")
+        handler = CodeHandler("raise RuntimeError('nope')", "/test_filename")
         handler.modify_document(doc)
 
         assert handler.error is not None
         assert 'nope' in handler.error
+
+    def test_script_sys_path(self):
+        doc = Document()
+        handler = CodeHandler("""import sys; raise RuntimeError("path: '%s'" % sys.path[0])""", "/test_filename")
+        handler.modify_document(doc)
+
+        assert handler.error is not None
+        assert "path: ''" in handler.error
+
+    def test_script_cwd(self):
+        doc = Document()
+        handler = CodeHandler("""import os; raise RuntimeError("cwd: '%s'" % os.getcwd())""", "/test_filename")
+        handler.modify_document(doc)
+
+        assert handler.error is not None
+        assert "cwd: '/'" in handler.error