Repository: bokeh/bokeh

Issue:
Modifying datasources broken
In bokeh master and presumably any rc/dev releases cut in the last few days, modifying a CDS is broken completely. This is because https://github.com/bokeh/bokeh/pull/6374 introduced a change in the way bokeh properties check whether an old and new value has changed. The approach there guards against checks on arrays, but since the ``.data`` property on a CDS is not itself an array it ends up doing a simple equality between the dictionary of arrays, which raises this error:

>ValueError�: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

This occurs for any change to a ``ColumnDataSource.data``, which means that any server or notebook examples that modify a CDS in python are currently broken.

Discussion:
bryevdv: @philippjfr I have updated the test I think it should be better now
philippjfr: Looks good and works for me.

Patch:
diff --git a/bokeh/core/property/bases.py b/bokeh/core/property/bases.py
index c5d7c92c9..4aace587a 100644
--- a/bokeh/core/property/bases.py
+++ b/bokeh/core/property/bases.py
@@ -178,7 +178,19 @@ class Property(PropertyDescriptorFactory):
         if isinstance(new, np.ndarray) or isinstance(old, np.ndarray):
             return np.array_equal(new, old)
 
-        return new == old
+        # this handles the special but common case where there is a dict with numpy
+        # arrays as values (e.g. the .data property of a ColumnDataSource)
+        if isinstance(new, dict) and isinstance(old, dict):
+            if set(new.keys()) != set(old.keys()):
+                return False
+            return all(self.matches(new[k], old[k]) for k in new)
+
+        try:
+            return new == old
+
+        # if the comparison fails for some reason, just punt and return no-match
+        except ValueError:
+            return False
 
     def from_json(self, json, models=None):
         ''' Convert from JSON-compatible values into a value for this property.


Test patch:
diff --git a/bokeh/core/property/tests/test_bases.py b/bokeh/core/property/tests/test_bases.py
index 14a1234bc..d7a0309ab 100644
--- a/bokeh/core/property/tests/test_bases.py
+++ b/bokeh/core/property/tests/test_bases.py
@@ -45,6 +45,14 @@ def test_property_assert_msg_funcs():
         p.prepare_value(hp, "foo", 10)
         assert str(e) == "bad True name, 10"
 
+def test_property_matches_basic_types(capsys):
+    p = pb.Property()
+    for x in [1, 1.2, "a", np.arange(4), None, False, True, {}, []]:
+        assert p.matches(x, x) is True
+        assert p.matches(x, "junk") is False
+    out, err = capsys.readouterr()
+    assert err == ""
+
 def test_property_matches_compatible_arrays(capsys):
     p = pb.Property()
     a = np.arange(5)
@@ -65,3 +73,35 @@ def test_property_matches_incompatible_arrays(capsys):
     out, err = capsys.readouterr()
     # no way to suppress FutureWarning in this case
     # assert err == ""
+
+def test_property_matches_dicts_with_array_values(capsys):
+    p = pb.Property()
+    d1 = dict(foo=np.arange(10))
+    d2 = dict(foo=np.arange(10))
+
+    assert p.matches(d1, d1) is True
+    assert p.matches(d1, d2) is True
+
+    # XXX not sure if this is preferable to have match, or not
+    assert p.matches(d1, dict(foo=list(range(10)))) is True
+
+    assert p.matches(d1, dict(foo=np.arange(11))) is False
+    assert p.matches(d1, dict(bar=np.arange(10))) is False
+    assert p.matches(d1, dict(bar=10)) is False
+    out, err = capsys.readouterr()
+    assert err == ""
+
+def test_property_matches_non_dict_containers_with_array_false(capsys):
+    p = pb.Property()
+    d1 = [np.arange(10)]
+    d2 = [np.arange(10)]
+    assert p.matches(d1, d1) is True  # because object identity
+    assert p.matches(d1, d2) is False
+
+    t1 = (np.arange(10),)
+    t2 = (np.arange(10),)
+    assert p.matches(t1, t1) is True  # because object identity
+    assert p.matches(t1, t2) is False
+
+    out, err = capsys.readouterr()
+    assert err == ""