Repository: bokeh/bokeh

Issue:
allow push_notebook() to run when no change has occurred
As first mentioned in #7268, and discussed on gitter with @bryevdv:

A typical usage of an animated bokeh figure inside a jupyter notebook, when using the native ipywidgets - think `interact()` - would be to:

* first create a figure with dummy/default settings using `output_notebook()` to create a handle
* then call `interact()` to let the user change these settings, associated with a function that does `push_notebook(handle)` 

In many cases this implies that the very first call made by interact() will come with no change from the initialized figure; and even if not, nothing prevents the user from tweaking with her settings but releasing the mouse while no actual change was made; `interact()` would not - and should not - filter it out.

I hope this is a convincing argument to allow for `push_notebook()` to be called when no change has occurred since last time the figure was refreshed. 

To repeat some of the material from #7268:
Right now, with bokeh 0.12.13 and several ancestors that I tried as well, I get the following message upon the very first call to `push_notebook()`

    ValueError: PATCH-DOC message requires at least one event




Discussion:

Resolving patch:
diff --git a/bokeh/io/notebook.py b/bokeh/io/notebook.py
--- a/bokeh/io/notebook.py
+++ b/bokeh/io/notebook.py
@@ -250,6 +250,13 @@ def push_notebook(document=None, state=None, handle=None):
         return
 
     events = list(handle.doc._held_events)
+
+    # This is to avoid having an exception raised for attempting to create a
+    # PATCH-DOC with no events. In the notebook, we just want to silenty
+    # ignore calls to push_notebook when there are no new events
+    if len(events) == 0:
+        return
+
     handle.doc._held_events = []
     msg = Protocol("1.0").create("PATCH-DOC", events)
 


Test patch:
diff --git a/bokeh/io/tests/test_notebook.py b/bokeh/io/tests/test_notebook.py
--- a/bokeh/io/tests/test_notebook.py
+++ b/bokeh/io/tests/test_notebook.py
@@ -21,7 +21,8 @@
 #-----------------------------------------------------------------------------
 
 # Standard library imports
-from mock import patch
+import json
+from mock import MagicMock, patch, PropertyMock
 
 # External imports
 
@@ -108,6 +109,35 @@ class Obj(object):
     assert mock__publish_display_data.call_args[0] == expected_args
     assert mock__publish_display_data.call_args[1] == expected_kwargs
 
+class Test_push_notebook(object):
+
+    @patch('bokeh.io.notebook.CommsHandle.comms', new_callable=PropertyMock)
+    def test_no_events(self, mock_comms):
+        mock_comms.return_value = MagicMock()
+
+        d = Document()
+
+        handle = binb.CommsHandle("comms", d)
+        binb.push_notebook(d, None, handle)
+        assert mock_comms.call_count == 0
+
+    @patch('bokeh.io.notebook.CommsHandle.comms', new_callable=PropertyMock)
+    def test_with_events(self, mock_comms):
+        mock_comm = MagicMock()
+        mock_send = MagicMock(return_value="junk")
+        mock_comm.send = mock_send
+        mock_comms.return_value = mock_comm
+
+        d = Document()
+
+        handle = binb.CommsHandle("comms", d)
+        d.title = "foo"
+        binb.push_notebook(d, None, handle)
+        assert mock_comms.call_count > 0
+        assert mock_send.call_count == 3 # sends header, metadata, then content
+        assert json.loads(mock_send.call_args[0][0]) == {u"events": [{u"kind": u"TitleChanged", u"title": u"foo"}], u"references": []}
+        assert mock_send.call_args[1] == {}
+
 #-----------------------------------------------------------------------------
 # Dev API
 #-----------------------------------------------------------------------------