Repository: bokeh/bokeh

Issue:
Limit imports of client/server code to only when necessary
Hi,

I try to import a module Bar module using

```
from bokeh.charts import Bar
```

then I got SyntaxError

```
Traceback (most recent call last):
  File "bokeh_func.py", line 2, in <module>
    from bokeh.charts import Bar 
  File "/usr/local/lib/python2.7/dist-packages/bokeh/charts/__init__.py", line 10, in <module>
    from ..plotting.helpers import DEFAULT_PALETTE
  File "/usr/local/lib/python2.7/dist-packages/bokeh/plotting/__init__.py", line 7, in <module>
    from ..io import curdoc; curdoc
  File "/usr/local/lib/python2.7/dist-packages/bokeh/io.py", line 29, in <module>
    from .core.state import State
  File "/usr/local/lib/python2.7/dist-packages/bokeh/core/state.py", line 46, in <module>
    from ..client import DEFAULT_SESSION_ID
  File "/usr/local/lib/python2.7/dist-packages/bokeh/client/__init__.py", line 6, in <module>
    from .session import ClientSession, pull_session, push_session, show_session, DEFAULT_SESSION_ID
  File "/usr/local/lib/python2.7/dist-packages/bokeh/client/session.py", line 10, in <module>
    from ._connection import ClientConnection
  File "/usr/local/lib/python2.7/dist-packages/bokeh/client/_connection.py", line 11, in <module>
    from tornado import gen, locks
  File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 1243, in <module>
    import tornado.platform.asyncio
  File "/usr/local/lib/python2.7/dist-packages/tornado/platform/asyncio.py", line 33, in <module>
    import asyncio # type: ignore
  File "/usr/local/lib/python2.7/dist-packages/asyncio/__init__.py", line 9, in <module>
    from . import selectors
  File "/usr/local/lib/python2.7/dist-packages/asyncio/selectors.py", line 39
    "{!r}".format(fileobj)) from None
```

There is asyncio installed on my system. When I uninstalled asyncio, there is no error.

Bokeh info

```
Python version      :  2.7.6 (default, Jun 22 2015, 17:58:13) 
IPython version     :  1.2.1
Bokeh version       :  0.12.1
BokehJS static path :  /usr/local/lib/python2.7/dist-packages/bokeh/server/static
```



Discussion:
@budiaji, how did you end up with asyncio package on Python 2.7? asyncio is a Python 3.3 package, so a syntax error like the reported one is not unexpected. Apparently pip can install asyncio without a hickup on 2.7. For 2.7 you may want to check out https://pypi.python.org/pypi/trollius (if that's necessary at all).

Right, this is an upstream issue. The error is coming from within tornado, not Bokeh. It's seems that it is looking for any system `asyncio` and aliasing it as `tornado.platform.asyncio`, with the results you are seeing. I echo @mattpap's question: what is this `asyncio` that you are installing on python 2.7?

About the only thing I think we could do is move the `bokeh.client` imports from the top-level to inside the functions where they are actually used. That's only an "accidental" fix for your situation, and also only "works" if you don't also use Bokeh server. But it seems like a good idea to try and avoid bringing in tornado and everything unless its actually needed, so I'd be +1 for moving those imports in any case. @bokeh/dev ?

OK there was no response from the OP, but I think the idea of structuring to limit imports of heavy server related things to only when they are actually used is a good one, so I have updated this issue. 

Resolving patch:
diff --git a/bokeh/core/state.py b/bokeh/core/state.py
--- a/bokeh/core/state.py
+++ b/bokeh/core/state.py
@@ -33,7 +33,6 @@
 
 from ..document import Document
 from ..resources import Resources, _SessionCoordinates
-from ..client import DEFAULT_SESSION_ID
 
 class State(object):
     ''' Manage state related to controlling Bokeh output.
@@ -221,7 +220,7 @@ def output_notebook(self):
         '''
         self._notebook = True
 
-    def output_server(self, session_id=DEFAULT_SESSION_ID, url="default", app_path='/'):
+    def output_server(self, session_id=None, url="default", app_path='/'):
         ''' Store Bokeh plots and documents on a Bokeh server.
 
         .. warning::
@@ -244,6 +243,12 @@ def output_server(self, session_id=DEFAULT_SESSION_ID, url="default", app_path='
             None
 
         '''
+
+        # limit heavyweight import to only when needed
+        from ..client import DEFAULT_SESSION_ID
+        if session_id is None:
+            session_id = DEFAULT_SESSION_ID
+
         self._session_coords = _SessionCoordinates(dict(session_id=session_id,
                                                         url=url,
                                                         app_path=app_path))
diff --git a/bokeh/io.py b/bokeh/io.py
--- a/bokeh/io.py
+++ b/bokeh/io.py
@@ -32,12 +32,11 @@
 from .models.layouts import LayoutDOM, Row, Column, VBoxForm
 from .layouts import gridplot, GridSpec ; gridplot, GridSpec
 from .model import _ModelInDocument
+import bokeh.util.browser as browserlib  # full import needed for test mocking to work
 from .util.deprecation import deprecated
 from .util.notebook import load_notebook, publish_display_data, get_comms
 from .util.string import decode_utf8
 from .util.serialization import make_id
-import bokeh.util.browser as browserlib  # full import needed for test mocking to work
-from .client import DEFAULT_SESSION_ID, push_session, show_session
 
 #-----------------------------------------------------------------------------
 # Globals and constants
@@ -173,7 +172,7 @@ def output_notebook(resources=None, verbose=False, hide_banner=False, load_timeo
 # usually we default session_id to "generate a random one" but
 # here we default to a hardcoded one. This is to support local
 # usage e.g. with a notebook.
-def output_server(session_id=DEFAULT_SESSION_ID, url="default", app_path="/"):
+def output_server(session_id=None, url="default", app_path="/"):
     """ Configure the default output state to push its document to a
     session on a Bokeh server.
 
@@ -215,6 +214,10 @@ def output_server(session_id=DEFAULT_SESSION_ID, url="default", app_path="/"):
     bokeh.client sessions as described at http://bokeh.pydata.org/en/latest/docs/user_guide/server.html#connecting-with-bokeh-client"
     """)
 
+    # limit heavyweight import to only when needed
+    from .client import DEFAULT_SESSION_ID
+    if session_id is None:
+        session_id = DEFAULT_SESSION_ID
     _state.output_server(session_id=session_id, url=url, app_path=app_path)
 
 def set_curdoc(doc):
@@ -338,6 +341,9 @@ def _show_notebook_with_state(obj, state, notebook_handle):
             return handle
 
 def _show_server_with_state(obj, state, new, controller):
+    # limit heavyweight import to only when needed
+    from .client import show_session
+
     push(state=state)
     show_session(session_id=state.session_id_allowing_none, url=state.url, app_path=state.app_path,
                  new=new, controller=controller)
@@ -451,6 +457,8 @@ def _save_helper(obj, filename, resources, title, validate):
 
 # this function exists mostly to be mocked in tests
 def _push_to_server(session_id, url, app_path, document, io_loop):
+    # limit heavyweight import to only when needed
+    from .client import push_session
     session = push_session(document, session_id=session_id, url=url, app_path=app_path, io_loop=io_loop)
     session.close()
     session.loop_until_closed()


Test patch:
diff --git a/tests/test_no_client_server_common.py b/tests/test_no_client_server_common.py
new file mode 100644
--- /dev/null
+++ b/tests/test_no_client_server_common.py
@@ -0,0 +1,19 @@
+from __future__ import print_function
+
+import subprocess
+
+basic_imports = [
+    "import bokeh.charts",
+    "import bokeh.embed",
+    "import bokeh.io",
+    "import bokeh.models",
+    "import bokeh.plotting",
+]
+
+def test_no_tornado_common():
+    proc = subprocess.Popen([
+        "python", "-c", "import sys; %s; sys.exit(1 if any(('bokeh.client' in x or 'bokeh.server' in x) for x in sys.modules.keys()) else 0)" % ";".join(basic_imports)
+    ],stdout=subprocess.PIPE)
+    proc.wait()
+    if proc.returncode != 0:
+        assert False
diff --git a/tests/test_no_tornado_common.py b/tests/test_no_tornado_common.py
--- a/tests/test_no_tornado_common.py
+++ b/tests/test_no_tornado_common.py
@@ -1,6 +1,5 @@
 from __future__ import print_function
 
-import pytest
 import subprocess
 
 basic_imports = [
@@ -17,6 +16,6 @@ def test_no_tornado_common():
         "python", "-c", "import sys; %s; sys.exit(1 if any('tornado' in x for x in sys.modules.keys()) else 0)" % ";".join(basic_imports)
     ],stdout=subprocess.PIPE)
     out, errs = proc.communicate()
-    msg = out.decode('utf-8', errors='ignore')
+    proc.wait()
     if proc.returncode != 0:
         assert False