Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-param for openSUSE:Factory checked in at 2026-04-02 17:41:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-param (Old) and /work/SRC/openSUSE:Factory/.python-param.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-param" Thu Apr 2 17:41:22 2026 rev:36 rq:1344237 version:2.3.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-param/python-param.changes 2026-03-13 21:22:00.358216241 +0100 +++ /work/SRC/openSUSE:Factory/.python-param.new.21863/python-param.changes 2026-04-02 17:42:35.518889777 +0200 @@ -1,0 +2,6 @@ +Wed Apr 1 23:45:38 UTC 2026 - Dirk Müller <[email protected]> + +- update to 2.3.3: + * Ensure shared rx inputs are reused (#1117) + +------------------------------------------------------------------- Old: ---- param-2.3.2.tar.gz New: ---- param-2.3.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-param.spec ++++++ --- /var/tmp/diff_new_pack.I4Qxu8/_old 2026-04-02 17:42:36.190917532 +0200 +++ /var/tmp/diff_new_pack.I4Qxu8/_new 2026-04-02 17:42:36.194917697 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-param -Version: 2.3.2 +Version: 2.3.3 Release: 0 Summary: Declarative Python programming using Parameters License: BSD-3-Clause ++++++ param-2.3.2.tar.gz -> param-2.3.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/param-2.3.2/PKG-INFO new/param-2.3.3/PKG-INFO --- old/param-2.3.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/param-2.3.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: param -Version: 2.3.2 +Version: 2.3.3 Summary: Declarative parameters for robust Python classes and a rich API for reactive programming Project-URL: Homepage, https://param.holoviz.org/ Project-URL: Tracker, https://github.com/holoviz/param/issues diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/param-2.3.2/param/_version.py new/param-2.3.3/param/_version.py --- old/param-2.3.2/param/_version.py 2020-02-02 01:00:00.000000000 +0100 +++ new/param-2.3.3/param/_version.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,5 +1,6 @@ -# file generated by setuptools-scm +# file generated by vcs-versioning # don't change, don't track in version control +from __future__ import annotations __all__ = [ "__version__", @@ -10,25 +11,14 @@ "commit_id", ] -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] - COMMIT_ID = Union[str, None] -else: - VERSION_TUPLE = object - COMMIT_ID = object - version: str __version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE -commit_id: COMMIT_ID -__commit_id__: COMMIT_ID +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None -__version__ = version = '2.3.2' -__version_tuple__ = version_tuple = (2, 3, 2) +__version__ = version = '2.3.3' +__version_tuple__ = version_tuple = (2, 3, 3) __commit_id__ = commit_id = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/param-2.3.2/param/reactive.py new/param-2.3.3/param/reactive.py --- old/param-2.3.2/param/reactive.py 2020-02-02 01:00:00.000000000 +0100 +++ new/param-2.3.3/param/reactive.py 2020-02-02 01:00:00.000000000 +0100 @@ -1436,7 +1436,7 @@ def __init__( self, obj=None, operation=None, fn=None, depth=0, method=None, prev=None, - _shared_obj=None, _current=None, _wrapper=None, **kwargs + _shared_obj=None, _current=None, _wrapper=None, _shared=None, **kwargs ): # _init is used to prevent to __getattribute__ to execute its # specialized code. @@ -1458,6 +1458,11 @@ self._current_task = None self._error_state = None self._current_ = _current + # _shared is used for branching rx pipelines where we clone the input. + # Here we store the original shared input, which makes it possible to + # cache the input value as long as the shared instance does not store + # a diverging _method accessor. + self._shared = _shared if isinstance(obj, rx) and not prev: self._prev = obj else: @@ -1529,6 +1534,17 @@ root._dirty_obj = False return self._shared_obj[0] + @property + def _is_async(self) -> bool: + if not self._operation: + return False + fn = self._operation["fn"] + return ( + inspect.iscoroutinefunction(fn) or + inspect.isasyncgenfunction(fn) or + inspect.isgeneratorfunction(fn) + ) + @_obj.setter def _obj(self, obj): if self._shared_obj is None: @@ -1632,10 +1648,15 @@ self._root._dirty_obj = True self._error_state = None - async def _resolve_async(self, obj): + async def _resolve_async(self, obj = None): import asyncio self._current_task = task = asyncio.current_task() - if inspect.isasyncgen(obj): + if obj is None: + if self._shared._current_task: + await self._shared._current_task + self._current_ = self._shared.rx.value + self._trigger.param.trigger('value') + elif inspect.isasyncgen(obj): async for val in obj: if self._current_task is not task: break @@ -1647,7 +1668,7 @@ self._current_ = value self._trigger.param.trigger('value') - def _lazy_resolve(self, obj): + def _lazy_resolve(self, obj = None): from .parameterized import async_executor if inspect.isgenerator(obj): obj = _to_async_gen(obj) @@ -1662,10 +1683,24 @@ if obj is Skip or obj is Undefined: self._current_ = Undefined raise Skip + elif ( + self._shared is not None and + self._method is None and + self._shared._method is None + ): + # If this rx is cloned from an shared input then we make use + # of the shared.rx.value to ensure branching pipelines do + # not have to recompute the inputs multiple times. + if self._is_async: + self._shared.rx.value # trigger async resolve + self._lazy_resolve() + else: + self._current_ = self._shared.rx.value + raise Skip operation = self._operation if operation: obj = self._eval_operation(obj, operation) - if inspect.isasyncgen(obj) or inspect.iscoroutine(obj) or inspect.isgenerator(obj): + if self._is_async: self._lazy_resolve(obj) obj = Skip if obj is Skip: @@ -1723,7 +1758,7 @@ if copy: kwargs = dict( self._kwargs, _current=self._current, method=self._method, - prev=self._prev, **kwargs + prev=self._prev, _shared=self, **kwargs ) else: kwargs = dict(prev=self, **dict(self._kwargs, **kwargs)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/param-2.3.2/tests/testreactive.py new/param-2.3.3/tests/testreactive.py --- old/param-2.3.2/tests/testreactive.py 2020-02-02 01:00:00.000000000 +0100 +++ new/param-2.3.3/tests/testreactive.py 2020-02-02 01:00:00.000000000 +0100 @@ -775,3 +775,69 @@ x = rx(1) with pytest.raises(AttributeError, match="'rx' has no attribute 'value'"): x.value = 1 + +def test_shared_rx_only_triggers_once(): + call_count = 0 + + class Model(param.Parameterized): + a = param.Number(1.0) + + model = Model() + + def expensive_compute(a): + nonlocal call_count + call_count += 1 + return {"x": a + 1, "y": a * 2} + + shared = rx(expensive_compute)(model.param.a) + + x_rx = shared.rx.pipe(lambda d: d["x"]) + y_rx = shared.rx.pipe(lambda d: d["y"]) + + x_rx.rx.value + y_rx.rx.value + + assert call_count == 1 + + model.a = 2.0 + + x_rx.rx.value + y_rx.rx.value + + assert call_count == 2 + + +async def test_async_shared_rx_only_triggers_once(): + call_count = 0 + + class Model(param.Parameterized): + a = param.Number(1.0) + + model = Model() + + async def expensive_compute(a): + nonlocal call_count + call_count += 1 + return {"x": a + 1, "y": a * 2} + + shared = model.param.a.rx.pipe(expensive_compute) + + x_rx = shared.rx.pipe(lambda d: d["x"]) + y_rx = shared.rx.pipe(lambda d: d["y"]) + + x_rx.rx.value + y_rx.rx.value + + await async_wait_until(lambda: call_count == 1) + + model.a = 2.0 + + x_rx.rx.value + y_rx.rx.value + + await async_wait_until(lambda: call_count == 2) + + assert x_rx.rx.value == 3 + assert y_rx.rx.value == 4 + + assert call_count == 2
