From 5f7136da2fb1b55a62a22e11adbf92d836f80cc5 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Wed, 17 Apr 2024 21:14:01 +0200 Subject: [PATCH] Enable syncing dataframe parameters (#6745) --- panel/io/cache.py | 11 +++++++++++ panel/io/location.py | 5 +++-- panel/tests/io/test_cache.py | 21 ++++++++++++++++++++- panel/tests/io/test_location.py | 24 ++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/panel/io/cache.py b/panel/io/cache.py index 5a3016dbeb..1bc657e7cc 100644 --- a/panel/io/cache.py +++ b/panel/io/cache.py @@ -112,6 +112,10 @@ def _pandas_hash(obj): if len(obj) >= _PANDAS_ROWS_LARGE: obj = obj.sample(n=_PANDAS_SAMPLE_SIZE, random_state=0) try: + if isinstance(obj, pd.DataFrame): + return ((b"%s" % pd.util.hash_pandas_object(obj).sum()) + + (b"%s" % pd.util.hash_pandas_object(obj.columns).sum()) + ) return b"%s" % pd.util.hash_pandas_object(obj).sum() except TypeError: # Use pickle if pandas cannot hash the object for example if @@ -463,3 +467,10 @@ def server_clear(session_context): pass return wrapped_func + +def is_equal(value, other)->bool: + """Returns True if value and other are equal + + Supports complex values like DataFrames + """ + return value is other or _generate_hash(value)==_generate_hash(other) diff --git a/panel/io/location.py b/panel/io/location.py index 7afade0c9d..989c8a88ce 100644 --- a/panel/io/location.py +++ b/panel/io/location.py @@ -15,6 +15,7 @@ from ..models.location import Location as _BkLocation from ..reactive import Syncable from ..util import edit_readonly, parse_query +from .cache import is_equal from .document import create_doc_if_none_exists from .state import state @@ -24,7 +25,6 @@ from bokeh.server.contexts import BokehSessionContext from pyviz_comms import Comm - class Location(Syncable): """ The Location component can be made available in a server context @@ -164,9 +164,10 @@ def _update_synced(self, event: param.parameterized.Event = None) -> None: except Exception: pass try: - equal = v == getattr(p, pname) + equal = is_equal(v, getattr(p, pname)) except Exception: equal = False + if not equal: mapped[pname] = v try: diff --git a/panel/tests/io/test_cache.py b/panel/tests/io/test_cache.py index 78211a1e18..4e00b49c14 100644 --- a/panel/tests/io/test_cache.py +++ b/panel/tests/io/test_cache.py @@ -17,7 +17,9 @@ diskcache = None diskcache_available = pytest.mark.skipif(diskcache is None, reason="requires diskcache") -from panel.io.cache import _find_hash_func, cache +from panel.io.cache import ( + _find_hash_func, _generate_hash, cache, is_equal, +) from panel.io.state import set_curdoc, state from panel.tests.util import serve_and_wait @@ -339,3 +341,20 @@ def expensive_calculation(self, value): assert model.expensive_calculation(2) == 4 assert model.executions == 2 + +DF1 = pd.DataFrame({"x": [1]}) +DF2 = pd.DataFrame({"y": [1]}) + +def test_hash_on_simple_dataframes(): + assert _generate_hash(DF1)!=_generate_hash(DF2) + +@pytest.mark.parametrize(["value", "other", "expected"], [ + (None, None, True), + (True, False, False), (False, True, False), (False, False, True), (True, True, True), + (None, 1, False), (1, None, False), (1, 1, True), (1,2,False), + (None, "a", False), ("a", None, False), ("a", "a", True), ("a","b",False), + (1,"1", False), + (None, DF1, False), (DF1, None, False), (DF1, DF1, True), (DF1, DF1.copy(), True), (DF1,DF2,False), +]) +def test_is_equal(value, other, expected): + assert is_equal(value, other)==expected diff --git a/panel/tests/io/test_location.py b/panel/tests/io/test_location.py index 014616719c..c7106a726e 100644 --- a/panel/tests/io/test_location.py +++ b/panel/tests/io/test_location.py @@ -1,3 +1,4 @@ +import pandas as pd import param import pytest @@ -26,6 +27,8 @@ class SyncParameterized(param.Parameterized): string = param.String(default=None) + dataframe = param.DataFrame(default=None) + def test_location_update_query(location): location.update_query(a=1) @@ -164,3 +167,24 @@ def app(): def test_iframe_srcdoc_location(): Location(pathname="srcdoc") + +@pytest.fixture +def dataframe(): + return pd.DataFrame({"x": [1]}) + +def test_location_sync_from_dataframe(location, dataframe): + p = SyncParameterized(dataframe=dataframe) + location.sync(p) + assert location.search == "?dataframe=%5B%7B%22x%22%3A+1%7D%5D" + +def test_location_sync_to_dataframe(location, dataframe): + p = SyncParameterized() + location.search = "?dataframe=%5B%7B%22x%22%3A+1%7D%5D" + location.sync(p) + pd.testing.assert_frame_equal(p.dataframe, dataframe) + +def test_location_sync_to_dataframe_with_initial_value(location, dataframe): + p = SyncParameterized(dataframe=pd.DataFrame({"y": [2]})) + location.search = "?dataframe=%5B%7B%22x%22%3A+1%7D%5D" + location.sync(p) + pd.testing.assert_frame_equal(p.dataframe, dataframe)