Repository: holoviz/panel

Issue:
ValueError on one-element dataframes: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
panel 1.0.3:

The following minimal example does not work - the second panel does not update when the select buttom is changed. May be related to #4950 

```
import panel as pn
import xarray as xr
import pandas as pd

da = xr.DataArray(np.arange(56).reshape((7, 8)), dims=["x", "y"],name='test')

minimal = pn.widgets.Select(name='test', options=[1,2,3],value=1)
@pn.depends(minimal)
def show_value_works(value):
    return pn.pane.DataFrame(da.to_dataframe())

@pn.depends(minimal)
def show_value_fails(value):
    return pn.pane.DataFrame(da.sel(x=[value],y=[value]).to_dataframe())

layout = pn.Row(minimal,show_value_works,show_value_fails)
layout.servable()
```

error output:
```

2023-05-26 17:34:21,364 Exception in callback functools.partial(<bound method IOLoop._discard_future_result of <tornado.platform.asyncio.AsyncIOMainLoop object at 0x7f4c34cd8190>>, <Task finished name='Task-14221' coro=<ServerSession.with_document_locked() done, defined at /home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/bokeh/server/session.py:77> exception=ValueError('The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().')>)
Traceback (most recent call last):
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/tornado/ioloop.py", line 738, in _run_callback
    ret = callback()
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/tornado/ioloop.py", line 762, in _discard_future_result
    future.result()
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/bokeh/server/session.py", line 98, in _needs_document_lock_wrapper
    result = await result
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/reactive.py", line 424, in _change_coroutine
    state._handle_exception(e)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/io/state.py", line 432, in _handle_exception
    raise exception
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/reactive.py", line 422, in _change_coroutine
    self._change_event(doc)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/reactive.py", line 440, in _change_event
    self._process_events(events)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/reactive.py", line 379, in _process_events
    self.param.update(**self_events)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/param/parameterized.py", line 1902, in update
    self_._batch_call_watchers()
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/param/parameterized.py", line 2063, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/param/parameterized.py", line 2025, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/param.py", line 827, in _replace_pane
    self._update_inner(new_object)
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/pane/base.py", line 700, in _update_inner
    new_pane, internal = self._update_from_object(
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/panel/pane/base.py", line 666, in _update_from_object
    if not equal:
  File "/home/it-499/mambaforge/envs/pavics/lib/python3.8/site-packages/pandas/core/generic.py", line 1537, in __nonzero__
    raise ValueError(
ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

```




Discussion:

Resolving patch:
diff --git a/panel/pane/base.py b/panel/pane/base.py
--- a/panel/pane/base.py
+++ b/panel/pane/base.py
@@ -10,6 +10,7 @@
     Tuple, Type, TypeVar,
 )
 
+import numpy as np
 import param
 
 from bokeh.models import ImportedStyleSheet
@@ -22,11 +23,14 @@
 from ..io.document import create_doc_if_none_exists, unlocked
 from ..io.notebook import push
 from ..io.state import state
-from ..layout.base import NamedListPanel, Panel, Row
+from ..layout.base import (
+    ListPanel, NamedListPanel, Panel, Row,
+)
 from ..links import Link
 from ..models import ReactiveHTML as _BkReactiveHTML
 from ..reactive import Reactive
 from ..util import param_reprs
+from ..util.checks import is_dataframe, is_series
 from ..viewable import (
     Layoutable, ServableMixin, Viewable, Viewer,
 )
@@ -582,7 +586,7 @@ def _update_inner_layout(self, *events):
         self._pane.param.update({event.name: event.new for event in events})
 
     @classmethod
-    def _recursive_update(cls, layout: Panel, index: int, old: Reactive, new: Reactive):
+    def _recursive_update(cls, old: Reactive, new: Reactive):
         """
         Recursively descends through Panel layouts and diffs their
         contents updating only changed parameters ensuring we don't
@@ -590,10 +594,6 @@ def _recursive_update(cls, layout: Panel, index: int, old: Reactive, new: Reacti
 
         Arguments
         ---------
-        layout: Panel
-          The layout the items are being updated or replaced on.
-        index: int
-          The index of the item being replaced.
         old: Reactive
           The Reactive component being updated or replaced.
         new: Reactive
@@ -601,32 +601,43 @@ def _recursive_update(cls, layout: Panel, index: int, old: Reactive, new: Reacti
           or replaced with.
         """
         ignored = ('name',)
-        if type(old) is not type(new):
-            layout[index] = new
-            return
-        if isinstance(new, Panel):
+        if isinstance(new, ListPanel):
             if len(old) == len(new):
                 for i, (sub_old, sub_new) in enumerate(zip(old, new)):
+                    if type(sub_old) is not type(sub_new):
+                        old[i] = new
+                        continue
                     if isinstance(new, NamedListPanel):
                         old._names[i] = new._names[i]
-                    cls._recursive_update(new, i, sub_old, sub_new)
+                    cls._recursive_update(sub_old, sub_new)
                 ignored += ('objects',)
         pvals = dict(old.param.values())
         new_params = {}
-        for k, new_p in new.param.values().items():
-            old_p = pvals[k]
-            if k in ignored or new_p is old_p:
+        for p, p_new in new.param.values().items():
+            p_old = pvals[p]
+            if p in ignored or p_new is p_old:
                 continue
             try:
-                equal = _generate_hash(new_p) == _generate_hash(old_p)
+                equal = p_new == p_old
+                if is_dataframe(equal) or is_series(equal) or isinstance(equal, np.ndarray):
+                    equal = equal.all()
+                equal = bool(equal)
             except Exception:
                 try:
-                    equal = bool(new_p == old_p)
+                    equal = _generate_hash(p_new) == _generate_hash(p_old)
                 except Exception:
                     equal = False
             if not equal:
-                new_params[k] = new_p
-        old.param.update(**new_params)
+                new_params[p] = p_new
+        if isinstance(old, PaneBase):
+            changing = any(p in old._rerender_params for p in new_params)
+            old._object_changing = changing
+            try:
+                old.param.update(**new_params)
+            finally:
+                old._object_changing = False
+        else:
+            old.param.update(**new_params)
 
     @classmethod
     def _update_from_object(cls, object: Any, old_object: Any, was_internal: bool, inplace: bool=False, **kwargs):
@@ -652,28 +663,12 @@ def _update_from_object(cls, object: Any, old_object: Any, was_internal: bool, i
         if type(old_object) is pane_type and ((not links and not custom_watchers and was_internal) or inplace):
             if isinstance(object, Panel) and len(old_object) == len(object):
                 for i, (old, new) in enumerate(zip(old_object, object)):
-                    cls._recursive_update(old_object, i, old, new)
-            elif isinstance(object, Reactive):
-                pvals = dict(old_object.param.values())
-                new_params = {}
-                for k, v in object.param.values().items():
-                    if k == 'name' or v is pvals[k]:
+                    if type(old) is not type(new):
+                        old_object[i] = new
                         continue
-                    try:
-                        equal = (v == pvals[k])
-                    except Exception:
-                        equal = False
-                    if not equal:
-                        new_params[k] = v
-                if isinstance(object, PaneBase):
-                    changing = any(p in old_object._rerender_params for p in new_params)
-                    old_object._object_changing = changing
-                    try:
-                        old_object.param.update(**new_params)
-                    finally:
-                        old_object._object_changing = False
-                else:
-                    old_object.param.update(**new_params)
+                    cls._recursive_update(old, new)
+            elif isinstance(object, Reactive):
+                cls._recursive_update(old_object, object)
             else:
                 old_object.object = object
         else:


Test patch:
diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py
--- a/panel/tests/test_param.py
+++ b/panel/tests/test_param.py
@@ -1,5 +1,6 @@
 import os
 
+import pandas as pd
 import param
 import pytest
 
@@ -1532,21 +1533,75 @@ class Test(param.Parameterized):
     assert w.value == 1
 
 
+def test_param_function_inplace_dataframe_update(document, comm):
+    number = NumberInput(value=0)
+
+    def layout(value):
+        return pd.DataFrame({
+            'x': [0, 1, 2],
+            'y': [0, 1, value]
+        })
+
+    pane = ParamFunction(bind(layout, number), inplace=True)
+
+    root = pane.get_root(document, comm)
+
+    model = root.children[0]
+
+    html_table = model.text
+    assert html_table.startswith("&lt;table class=&quot;dataframe panel-df&quot;&gt;\n  &lt;thead&gt;\n")
+
+    number.value = 314
+
+    assert model is root.children[0]
+    assert model.text != html_table
+    assert '314' in model.text
+
+    html_table = model.text
+    number.param.trigger('value')
+    assert model.text is html_table
+
+
 def test_param_function_recursive_update(document, comm):
     checkbox = Checkbox(value=False)
 
     def layout(value):
         return Row(Markdown(f"{value}"))
 
+    pane = ParamFunction(bind(layout, checkbox), inplace=True)
+
+    root = pane.get_root(document, comm)
+
+    layout = root.children[0]
+
+    assert layout.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
+
+    checkbox.value = True
+
+    assert layout is root.children[0]
+    assert layout.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
+
+
+def test_param_function_recursive_update_multiple(document, comm):
+    checkbox = Checkbox(value=False)
+
+    def layout(value):
+        return Row(Markdown(f"{value}"), Markdown(f"{not value}"))
+
     layout = ParamFunction(bind(layout, checkbox), inplace=True)
 
     root = layout.get_root(document, comm)
 
-    assert root.children[0].children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
+    layout = root.children[0]
+
+    assert layout.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
+    assert layout.children[1].text == '&lt;p&gt;True&lt;/p&gt;\n'
 
     checkbox.value = True
 
-    assert root.children[0].children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
+    assert layout is root.children[0]
+    assert layout.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
+    assert layout.children[1].text == '&lt;p&gt;False&lt;/p&gt;\n'
 
 
 def test_param_editablerangeslider_with_bounds():