Repository: bokeh/bokeh

Issue:
Warn about Python callbacks when they won't run
Document should track whether there are any on_change callbacks on the doc or on any model in the doc. If there are and you publish to a static format, we should issue some type of warning and perhaps suggest using JavaScript callbacks.

Warn about Python callbacks when they won't run
Document should track whether there are any on_change callbacks on the doc or on any model in the doc. If there are and you publish to a static format, we should issue some type of warning and perhaps suggest using JavaScript callbacks.



Discussion:

Resolving patch:
diff --git a/bokeh/embed/util.py b/bokeh/embed/util.py
--- a/bokeh/embed/util.py
+++ b/bokeh/embed/util.py
@@ -31,7 +31,7 @@
 from ..core.json_encoder import serialize_json
 from ..core.templates import DOC_JS, FILE, PLOT_DIV, SCRIPT_TAG
 from ..document.document import DEFAULT_TITLE, Document
-from ..model import Model
+from ..model import Model, collect_models
 from ..settings import settings
 from ..util.compiler import bundle_all_models
 from ..util.serialization import make_id
@@ -95,6 +95,18 @@ def check_one_model_or_doc(model):
         raise ValueError("Input must be exactly one Model or Document")
     return models[0]
 
+def submodel_has_python_callbacks(models):
+    ''' Traverses submodels to check for Python (event) callbacks
+
+    '''
+    has_python_callback = False
+    for model in collect_models(models):
+        if len(model._callbacks) > 0 or len(model._event_callbacks) > 0:
+            has_python_callback = True
+            break
+
+    return has_python_callback
+
 def div_for_render_item(item):
     '''
 
@@ -186,6 +198,19 @@ def standalone_docs_json_and_render_items(models):
 
     '''
     models = check_models_or_docs(models)
+    if submodel_has_python_callbacks(models):
+        msg = ('You are generating standalone HTML/JS output, but trying to '
+               'use real Python callbacks (i.e. with on_change or on_event). '
+               'This cannot work. Only JavaScript callbacks may be used '
+               'with standalone output. For more information on JavaScript '
+               'callbacks, see\n\n'
+               'http://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.html#userguide-interaction-jscallbacks\n\n'
+                'Alternatively, to use real Python callbacks, a Bokeh server '
+               'application may be used. For more information on building and '
+               'running Bokeh applications, see\n\n'
+               ' http://bokeh.pydata.org/en/latest/docs/user_guide/server.html')
+        log.warn(msg)
+
 
     render_items = []
     docs_by_id = {}


Test patch:
diff --git a/bokeh/embed/tests/test_util.py b/bokeh/embed/tests/test_util.py
--- a/bokeh/embed/tests/test_util.py
+++ b/bokeh/embed/tests/test_util.py
@@ -18,11 +18,16 @@
 #-----------------------------------------------------------------------------
 
 # Standard library imports
+import logging
 
 # External imports
 
 # Bokeh imports
 from bokeh.model import Model
+from bokeh.core.properties import Int, String, List
+from bokeh.document.document import Document
+from bokeh.util.logconfig import basicConfig
+from bokeh.events import Tap
 
 # Module under test
 import bokeh.embed.util as beu
@@ -30,6 +35,52 @@
 #-----------------------------------------------------------------------------
 # Setup
 #-----------------------------------------------------------------------------
+# needed for caplog tests to function
+basicConfig()
+
+# Taken from test_callback_manager.py
+class _GoodPropertyCallback(object):
+
+    def __init__(self):
+        self.last_name = None
+        self.last_old = None
+        self.last_new = None
+
+    def __call__(self, name, old, new):
+        self.method(name, old, new)
+
+    def method(self, name, old, new):
+        self.last_name = name
+        self.last_old = old
+        self.last_new = new
+
+    def partially_good(self, name, old, new, newer):
+        pass
+
+    def just_fine(self, name, old, new, extra='default'):
+        pass
+
+class _GoodEventCallback(object):
+
+    def __init__(self):
+        self.last_name = None
+        self.last_old = None
+        self.last_new = None
+
+    def __call__(self, event):
+        self.method(event)
+
+    def method(self, event):
+        self.event = event
+
+    def partially_good(self, arg, event):
+        pass
+
+# Taken from test_model
+class EmbedTestUtilModel(Model):
+    a = Int(12)
+    b = String("hello")
+    c = List(Int, [1, 2, 3])
 
 #-----------------------------------------------------------------------------
 # General API
@@ -81,7 +132,34 @@ class Test_script_for_render_items(object):
     pass
 
 class Test_standalone_docs_json_and_render_items(object):
-    pass
+
+    def test_log_warning_if_python_property_callback(self, caplog):
+        d = Document()
+        m1 = EmbedTestUtilModel()
+        c1 = _GoodPropertyCallback()
+        d.add_root(m1)
+
+        m1.on_change('name', c1)
+        assert len(m1._callbacks) != 0
+
+        with caplog.at_level(logging.WARN):
+            beu.standalone_docs_json_and_render_items(m1)
+            assert len(caplog.records) == 1
+            assert caplog.text != ''
+
+    def test_log_warning_if_python_event_callback(self, caplog):
+        d = Document()
+        m1 = EmbedTestUtilModel()
+        c1 = _GoodEventCallback()
+        d.add_root(m1)
+
+        m1.on_event(Tap, c1)
+        assert len(m1._event_callbacks) != 0
+
+        with caplog.at_level(logging.WARN):
+            beu.standalone_docs_json_and_render_items(m1)
+            assert len(caplog.records) == 1
+            assert caplog.text != ''
 
 class Test_wrap_in_onload(object):