Repository: holoviz/panel

Issue:
widget callback issue
```
print( pn.__name__, pn.__version__)   # prints:    panel 0.2.0a3.post4+g82e3f43
w = pn.widgets.TextInput(name='A widget', value='A string')
w.param.watch(print, 'value')
w
```
I can't come up with a reliable sequence, but the problem appears frequently:
alternate between
* setting a string in the textinput widget
* setting  w.value to a different string

eventually, the textinput widget is not updated, although its value is indeed changed.
It may take setting w.value to a different string more than once before the widget text
updates again as before

One sequence that does seem to 'work':
* set up w
* change the widget text to '12'
* set w.value='foo'
* change the widget text back to '12'
* set w.value='foo'
at this point the widget does not update the display


Discussion:

Resolving patch:
diff --git a/panel/util.py b/panel/util.py
--- a/panel/util.py
+++ b/panel/util.py
@@ -10,6 +10,7 @@
 
 from collections import defaultdict, MutableSequence, MutableMapping, OrderedDict
 from datetime import datetime
+from contextlib import contextmanager
 
 import param
 import bokeh
@@ -29,8 +30,10 @@
 except:
     basestring = unicode = str
 
-
+# Global variables
 CUSTOM_MODELS = {}
+BLOCKED = False
+
 
 def load_compiled_models(custom_model, implementation):
     """
@@ -195,13 +198,24 @@ def diff(doc, binary=True, events=None):
     the latest plot data.
     """
     events = list(doc._held_events) if events is None else events
-    if not events:
+    if not events or BLOCKED:
         return None
     msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary)
     doc._held_events = [e for e in doc._held_events if e not in events]
     return msg
 
 
+@contextmanager
+def block_comm():
+    """
+    Context manager to temporarily block comm push
+    """
+    global BLOCKED
+    BLOCKED = True
+    yield
+    BLOCKED = False
+
+
 def push(doc, comm, binary=True):
     """
     Pushes events stored on the document across the provided comm.
diff --git a/panel/viewable.py b/panel/viewable.py
--- a/panel/viewable.py
+++ b/panel/viewable.py
@@ -15,7 +15,8 @@
 
 from bokeh.application.handlers import FunctionHandler
 from bokeh.application import Application
-from bokeh.document import Document
+from bokeh.document.document import Document, _combine_document_events
+from bokeh.document.events import ModelChangedEvent
 from bokeh.io import curdoc, show
 from bokeh.models import CustomJS
 from bokeh.server.server import Server
@@ -352,8 +353,21 @@ def param_change(*events):
             if not msgs: return
 
             def update_model():
-                for msg in msgs:
-                    model.update(**msg)
+                update = {k: v for msg in msgs for k, v in msg.items()}
+                if comm:
+                    for attr, new in update.items():
+                        setattr(model, attr, new)
+                        event = doc._held_events[-1] if doc._held_events else None
+                        if (event and event.model is model and event.attr == attr and
+                            event.new is new):
+                            continue
+                        # If change did not trigger event trigger it manually
+                        old = getattr(model, attr)
+                        serializable_new = model.lookup(attr).serializable_value(model)
+                        event = ModelChangedEvent(doc, model, attr, old, new, serializable_new)
+                        _combine_document_events(event, doc._held_events)
+                else:
+                    model.update(**update)
 
             if comm:
                 self._expecting += [m for msg in msgs for m in msg]
@@ -362,7 +376,7 @@ def update_model():
                     push(doc, comm)
                 except:
                     raise
-                finally:
+                else:
                     self._expecting = self._expecting[:-len(msg)]
             else:
                 doc.add_next_tick_callback(update_model)


Test patch:
diff --git a/panel/tests/test_widgets.py b/panel/tests/test_widgets.py
--- a/panel/tests/test_widgets.py
+++ b/panel/tests/test_widgets.py
@@ -5,6 +5,7 @@
 
 from bokeh.layouts import WidgetBox
 from bokeh.models import Div as BkDiv, Slider as BkSlider
+from panel.util import block_comm
 from panel.widgets import (
     TextInput, StaticText, FloatSlider, IntSlider, RangeSlider,
     LiteralInput, Checkbox, Select, MultiSelect, Button, Toggle,
@@ -34,6 +35,32 @@ def test_text_input(document, comm):
     assert repr(text) == "TextInput(name='Text:', value='A')"
 
 
+def test_widget_triggers_events(document, comm):
+    """
+    Ensure widget events don't get swallowed in comm mode
+    """
+    text = TextInput(value='ABC', name='Text:')
+
+    box = text._get_root(document, comm=comm)
+    widget = box.children[0]
+    document.add_root(box)
+    document.hold()
+
+    # Simulate client side change
+    widget.value = '123'
+    document._held_events = document._held_events[:-1]
+
+    # Set new value
+    with block_comm():
+        text.value = '123'
+
+    assert len(document._held_events) == 1
+    event = document._held_events[0]
+    assert event.model is widget
+    assert event.attr == 'value'
+    assert event.new == '123'
+
+
 def test_static_text(document, comm):
 
     text = StaticText(value='ABC', name='Text:')