From 2f25a81bf0c7571daa4d0c2c03966927b08248cc Mon Sep 17 00:00:00 2001 From: MarcSkovMadsen Date: Wed, 17 Apr 2024 15:30:24 +0000 Subject: [PATCH 1/2] enable syncing dataframe parameters --- panel/io/location.py | 21 ++++++++++++++++++++- panel/tests/io/test_location.py | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/panel/io/location.py b/panel/io/location.py index 7afade0c9d..221e157c87 100644 --- a/panel/io/location.py +++ b/panel/io/location.py @@ -24,6 +24,24 @@ from bokeh.server.contexts import BokehSessionContext from pyviz_comms import Comm +def _default_is_equal(value, other): + return value==other + +def _dataframe_is_equal(value, other): + if hasattr(value, "equals"): + return value.equals(other) + if value is None and other is None: + return True + return False + +_PARAMETER_IS_EQUAL_MAP = { + param.DataFrame: _dataframe_is_equal +} + +def _is_equal(value, other, parameter_type): + is_equal = _PARAMETER_IS_EQUAL_MAP.get(parameter_type, _default_is_equal) + return is_equal(value, other) + class Location(Syncable): """ @@ -164,9 +182,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), type(p.param[pname])) except Exception: equal = False + if not equal: mapped[pname] = v try: 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) From bb3c56adfaae80339b9f9adb113f6f67bc2cf1d2 Mon Sep 17 00:00:00 2001 From: MarcSkovMadsen Date: Wed, 17 Apr 2024 18:05:49 +0000 Subject: [PATCH 2/2] review feedback --- panel/io/cache.py | 11 +++++++++++ panel/io/location.py | 22 ++-------------------- panel/tests/io/test_cache.py | 21 ++++++++++++++++++++- 3 files changed, 33 insertions(+), 21 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 221e157c87..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,25 +25,6 @@ from bokeh.server.contexts import BokehSessionContext from pyviz_comms import Comm -def _default_is_equal(value, other): - return value==other - -def _dataframe_is_equal(value, other): - if hasattr(value, "equals"): - return value.equals(other) - if value is None and other is None: - return True - return False - -_PARAMETER_IS_EQUAL_MAP = { - param.DataFrame: _dataframe_is_equal -} - -def _is_equal(value, other, parameter_type): - is_equal = _PARAMETER_IS_EQUAL_MAP.get(parameter_type, _default_is_equal) - return is_equal(value, other) - - class Location(Syncable): """ The Location component can be made available in a server context @@ -182,7 +164,7 @@ def _update_synced(self, event: param.parameterized.Event = None) -> None: except Exception: pass try: - equal = _is_equal(v, getattr(p, pname), type(p.param[pname])) + equal = is_equal(v, getattr(p, pname)) except Exception: equal = False 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