From 95b8e12e669a97da61faf3d1d2713d07a093a25d Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 27 May 2024 14:54:05 -0400 Subject: [PATCH 1/2] feat: Use hash of reactive value object to determine if it has changed --- setup.cfg | 1 + shiny/reactive/_reactives.py | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index d63ae762a..00e1cc660 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,7 @@ install_requires = # starlette. For more information, see: # https://github.com/posit-dev/py-shiny/issues/1114#issuecomment-1942757757 python-multipart>=0.0.7;platform_system!="Emscripten" + xxhash>=3.4.1 tests_require = pytest>=3 zip_safe = False diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index c8bf7e920..d7ffd26ff 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -17,6 +17,7 @@ import asyncio import functools +import pickle import traceback import warnings from typing import ( @@ -30,11 +31,13 @@ overload, ) +from xxhash import xxh32_hexdigest + from .. import _utils from .._docstring import add_example from .._utils import is_async_callable, run_coro_sync from .._validation import req -from ..types import MISSING, MISSING_TYPE, ActionButtonValue, SilentException +from ..types import MISSING, MISSING_TYPE, ActionButtonValue, Any, SilentException from ._core import Context, Dependents, ReactiveWarning, isolate if TYPE_CHECKING: @@ -43,6 +46,14 @@ T = TypeVar("T") +def is_immutable(x: Any) -> bool: + return isinstance(x, (int, float, str, tuple, frozenset, bool)) + + +def hash_digest(x: Any) -> str: + return xxh32_hexdigest(pickle.dumps(x, protocol=pickle.HIGHEST_PROTOCOL)) + + # ============================================================================== # Value # ============================================================================== @@ -116,6 +127,7 @@ def __init__( self._read_only: bool = read_only self._value_dependents: Dependents = Dependents() self._is_set_dependents: Dependents = Dependents() + self._hash: str | None = hash_digest(value) if not is_immutable(value) else None def __call__(self) -> T: return self.get() @@ -172,14 +184,22 @@ def set(self, value: T) -> bool: # The ._set() method allows setting read-only Value objects. This is used when the # Value is part of a session.Inputs object, and the session wants to set it. def _set(self, value: T) -> bool: - if self._value is value: + value_hash = None + if is_immutable(value) and value == self._value: + return False + elif isinstance(self._value, MISSING_TYPE) and isinstance(value, MISSING_TYPE): return False + else: + value_hash = hash_digest(value) + if value_hash == self._hash: + return False if isinstance(self._value, MISSING_TYPE) != isinstance(value, MISSING_TYPE): self._is_set_dependents.invalidate() self._value = value self._value_dependents.invalidate() + self._hash = value_hash return True def unset(self) -> None: From 3e519eaf25453aea491e71c0f8f827030916e419 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 27 May 2024 15:08:38 -0400 Subject: [PATCH 2/2] consider missing value in constructor --- shiny/reactive/_reactives.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index d7ffd26ff..b61f1b78b 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -127,7 +127,9 @@ def __init__( self._read_only: bool = read_only self._value_dependents: Dependents = Dependents() self._is_set_dependents: Dependents = Dependents() - self._hash: str | None = hash_digest(value) if not is_immutable(value) else None + self._hash: str | None = None + if not (is_immutable(value) or isinstance(value, MISSING_TYPE)): + self._hash = hash_digest(value) def __call__(self) -> T: return self.get()