param 2.1.0a2__tar.gz → 2.1.1a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {param-2.1.0a2 → param-2.1.1a0}/PKG-INFO +45 -10
  2. {param-2.1.0a2 → param-2.1.1a0}/param/_utils.py +1 -1
  3. {param-2.1.0a2 → param-2.1.1a0}/param/_version.py +2 -2
  4. {param-2.1.0a2 → param-2.1.1a0}/param/parameterized.py +13 -9
  5. {param-2.1.0a2 → param-2.1.1a0}/param/reactive.py +135 -10
  6. {param-2.1.0a2 → param-2.1.1a0}/tests/testdeprecations.py +3 -3
  7. {param-2.1.0a2 → param-2.1.1a0}/tests/testreactive.py +254 -21
  8. {param-2.1.0a2 → param-2.1.1a0}/tests/testsignatures.py +3 -3
  9. {param-2.1.0a2 → param-2.1.1a0}/tests/testwatch.py +2 -2
  10. {param-2.1.0a2 → param-2.1.1a0}/tests/utils.py +63 -0
  11. {param-2.1.0a2 → param-2.1.1a0}/.gitignore +0 -0
  12. {param-2.1.0a2 → param-2.1.1a0}/LICENSE.txt +0 -0
  13. {param-2.1.0a2 → param-2.1.1a0}/README.md +0 -0
  14. {param-2.1.0a2 → param-2.1.1a0}/numbergen/__init__.py +0 -0
  15. {param-2.1.0a2 → param-2.1.1a0}/param/__init__.py +0 -0
  16. {param-2.1.0a2 → param-2.1.1a0}/param/depends.py +0 -0
  17. {param-2.1.0a2 → param-2.1.1a0}/param/display.py +0 -0
  18. {param-2.1.0a2 → param-2.1.1a0}/param/ipython.py +0 -0
  19. {param-2.1.0a2 → param-2.1.1a0}/param/parameters.py +0 -0
  20. {param-2.1.0a2 → param-2.1.1a0}/param/serializer.py +0 -0
  21. {param-2.1.0a2 → param-2.1.1a0}/param/version.py +0 -0
  22. {param-2.1.0a2 → param-2.1.1a0}/pyproject.toml +0 -0
  23. {param-2.1.0a2 → param-2.1.1a0}/tests/__init__.py +0 -0
  24. {param-2.1.0a2 → param-2.1.1a0}/tests/conftest.py +0 -0
  25. {param-2.1.0a2 → param-2.1.1a0}/tests/testaddparameter.py +0 -0
  26. {param-2.1.0a2 → param-2.1.1a0}/tests/testbind.py +0 -0
  27. {param-2.1.0a2 → param-2.1.1a0}/tests/testbooleanparam.py +0 -0
  28. {param-2.1.0a2 → param-2.1.1a0}/tests/testbytesparam.py +0 -0
  29. {param-2.1.0a2 → param-2.1.1a0}/tests/testcalendardateparam.py +0 -0
  30. {param-2.1.0a2 → param-2.1.1a0}/tests/testcalendardaterangeparam.py +0 -0
  31. {param-2.1.0a2 → param-2.1.1a0}/tests/testcallable.py +0 -0
  32. {param-2.1.0a2 → param-2.1.1a0}/tests/testclassselector.py +0 -0
  33. {param-2.1.0a2 → param-2.1.1a0}/tests/testcolorparameter.py +0 -0
  34. {param-2.1.0a2 → param-2.1.1a0}/tests/testcomparator.py +0 -0
  35. {param-2.1.0a2 → param-2.1.1a0}/tests/testcompositeparams.py +0 -0
  36. {param-2.1.0a2 → param-2.1.1a0}/tests/testcustomparam.py +0 -0
  37. {param-2.1.0a2 → param-2.1.1a0}/tests/testdateparam.py +0 -0
  38. {param-2.1.0a2 → param-2.1.1a0}/tests/testdaterangeparam.py +0 -0
  39. {param-2.1.0a2 → param-2.1.1a0}/tests/testdefaults.py +0 -0
  40. {param-2.1.0a2 → param-2.1.1a0}/tests/testdynamicparams.py +0 -0
  41. {param-2.1.0a2 → param-2.1.1a0}/tests/testfiledeserialization.py +0 -0
  42. {param-2.1.0a2 → param-2.1.1a0}/tests/testfileselector.py +0 -0
  43. {param-2.1.0a2 → param-2.1.1a0}/tests/testipythonmagic.py +0 -0
  44. {param-2.1.0a2 → param-2.1.1a0}/tests/testjsonserialization.py +0 -0
  45. {param-2.1.0a2 → param-2.1.1a0}/tests/testlist.py +0 -0
  46. {param-2.1.0a2 → param-2.1.1a0}/tests/testlistselector.py +0 -0
  47. {param-2.1.0a2 → param-2.1.1a0}/tests/testmultifileselector.py +0 -0
  48. {param-2.1.0a2 → param-2.1.1a0}/tests/testnumbergen.py +0 -0
  49. {param-2.1.0a2 → param-2.1.1a0}/tests/testnumberparameter.py +0 -0
  50. {param-2.1.0a2 → param-2.1.1a0}/tests/testnumpy.py +0 -0
  51. {param-2.1.0a2 → param-2.1.1a0}/tests/testobjectselector.py +0 -0
  52. {param-2.1.0a2 → param-2.1.1a0}/tests/testpandas.py +0 -0
  53. {param-2.1.0a2 → param-2.1.1a0}/tests/testparamdepends.py +0 -0
  54. {param-2.1.0a2 → param-2.1.1a0}/tests/testparameterizedobject.py +0 -0
  55. {param-2.1.0a2 → param-2.1.1a0}/tests/testparameterizedrepr.py +0 -0
  56. {param-2.1.0a2 → param-2.1.1a0}/tests/testparamoutput.py +0 -0
  57. {param-2.1.0a2 → param-2.1.1a0}/tests/testparamunion.py +0 -0
  58. {param-2.1.0a2 → param-2.1.1a0}/tests/testpathparam.py +0 -0
  59. {param-2.1.0a2 → param-2.1.1a0}/tests/testpickle.py +0 -0
  60. {param-2.1.0a2 → param-2.1.1a0}/tests/testrangeparameter.py +0 -0
  61. {param-2.1.0a2 → param-2.1.1a0}/tests/testrefs.py +0 -0
  62. {param-2.1.0a2 → param-2.1.1a0}/tests/testreprhtml.py +0 -0
  63. {param-2.1.0a2 → param-2.1.1a0}/tests/testselector.py +0 -0
  64. {param-2.1.0a2 → param-2.1.1a0}/tests/teststringparam.py +0 -0
  65. {param-2.1.0a2 → param-2.1.1a0}/tests/testtimedependent.py +0 -0
  66. {param-2.1.0a2 → param-2.1.1a0}/tests/testtupleparam.py +0 -0
  67. {param-2.1.0a2 → param-2.1.1a0}/tests/testutils.py +0 -0
  68. {param-2.1.0a2 → param-2.1.1a0}/tests/testversion.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: param
3
- Version: 2.1.0a2
3
+ Version: 2.1.1a0
4
4
  Summary: Make your Python code clearer and more reliable by declaring Parameters.
5
5
  Project-URL: Homepage, https://param.holoviz.org/
6
6
  Project-URL: Tracker, https://github.com/holoviz/param/issues
@@ -27,12 +27,35 @@ Classifier: Topic :: Scientific/Engineering
27
27
  Classifier: Topic :: Software Development :: Libraries
28
28
  Requires-Python: >=3.8
29
29
  Provides-Extra: all
30
- Requires-Dist: param[doc]; extra == 'all'
31
- Requires-Dist: param[lint]; extra == 'all'
32
- Requires-Dist: param[tests-full]; extra == 'all'
30
+ Requires-Dist: aiohttp; extra == 'all'
31
+ Requires-Dist: cloudpickle; extra == 'all'
32
+ Requires-Dist: coverage[toml]; extra == 'all'
33
+ Requires-Dist: flake8; extra == 'all'
34
+ Requires-Dist: gmpy; extra == 'all'
35
+ Requires-Dist: ipython; extra == 'all'
36
+ Requires-Dist: jsonschema; extra == 'all'
37
+ Requires-Dist: nbsite==0.8.4; extra == 'all'
38
+ Requires-Dist: nbval; extra == 'all'
39
+ Requires-Dist: nest-asyncio; extra == 'all'
40
+ Requires-Dist: numpy; extra == 'all'
41
+ Requires-Dist: odfpy; extra == 'all'
42
+ Requires-Dist: openpyxl; extra == 'all'
43
+ Requires-Dist: pandas; extra == 'all'
44
+ Requires-Dist: panel; extra == 'all'
45
+ Requires-Dist: pre-commit; extra == 'all'
46
+ Requires-Dist: pyarrow; extra == 'all'
47
+ Requires-Dist: pytest; extra == 'all'
48
+ Requires-Dist: pytest-asyncio; extra == 'all'
49
+ Requires-Dist: pytest-xdist; extra == 'all'
50
+ Requires-Dist: pytest<8.1; extra == 'all'
51
+ Requires-Dist: sphinx-remove-toctrees; extra == 'all'
52
+ Requires-Dist: tables; extra == 'all'
53
+ Requires-Dist: xlrd; extra == 'all'
33
54
  Provides-Extra: doc
55
+ Requires-Dist: aiohttp; extra == 'doc'
34
56
  Requires-Dist: nbsite==0.8.4; extra == 'doc'
35
- Requires-Dist: param[examples]; extra == 'doc'
57
+ Requires-Dist: pandas; extra == 'doc'
58
+ Requires-Dist: panel; extra == 'doc'
36
59
  Requires-Dist: sphinx-remove-toctrees; extra == 'doc'
37
60
  Provides-Extra: examples
38
61
  Requires-Dist: aiohttp; extra == 'examples'
@@ -52,22 +75,34 @@ Requires-Dist: pyarrow; extra == 'tests-deser'
52
75
  Requires-Dist: tables; extra == 'tests-deser'
53
76
  Requires-Dist: xlrd; extra == 'tests-deser'
54
77
  Provides-Extra: tests-examples
78
+ Requires-Dist: aiohttp; extra == 'tests-examples'
55
79
  Requires-Dist: nbval; extra == 'tests-examples'
56
- Requires-Dist: param[examples]; extra == 'tests-examples'
80
+ Requires-Dist: pandas; extra == 'tests-examples'
81
+ Requires-Dist: panel; extra == 'tests-examples'
57
82
  Requires-Dist: pytest-asyncio; extra == 'tests-examples'
58
83
  Requires-Dist: pytest-xdist; extra == 'tests-examples'
59
84
  Requires-Dist: pytest<8.1; extra == 'tests-examples'
60
85
  Provides-Extra: tests-full
86
+ Requires-Dist: aiohttp; extra == 'tests-full'
61
87
  Requires-Dist: cloudpickle; extra == 'tests-full'
88
+ Requires-Dist: coverage[toml]; extra == 'tests-full'
62
89
  Requires-Dist: gmpy; extra == 'tests-full'
63
90
  Requires-Dist: ipython; extra == 'tests-full'
64
91
  Requires-Dist: jsonschema; extra == 'tests-full'
92
+ Requires-Dist: nbval; extra == 'tests-full'
65
93
  Requires-Dist: nest-asyncio; extra == 'tests-full'
66
94
  Requires-Dist: numpy; extra == 'tests-full'
95
+ Requires-Dist: odfpy; extra == 'tests-full'
96
+ Requires-Dist: openpyxl; extra == 'tests-full'
67
97
  Requires-Dist: pandas; extra == 'tests-full'
68
- Requires-Dist: param[tests-deser]; extra == 'tests-full'
69
- Requires-Dist: param[tests-examples]; extra == 'tests-full'
70
- Requires-Dist: param[tests]; extra == 'tests-full'
98
+ Requires-Dist: panel; extra == 'tests-full'
99
+ Requires-Dist: pyarrow; extra == 'tests-full'
100
+ Requires-Dist: pytest; extra == 'tests-full'
101
+ Requires-Dist: pytest-asyncio; extra == 'tests-full'
102
+ Requires-Dist: pytest-xdist; extra == 'tests-full'
103
+ Requires-Dist: pytest<8.1; extra == 'tests-full'
104
+ Requires-Dist: tables; extra == 'tests-full'
105
+ Requires-Dist: xlrd; extra == 'tests-full'
71
106
  Description-Content-Type: text/markdown
72
107
 
73
108
  <img src="https://raw.githubusercontent.com/holoviz/param/main/doc/_static/logo_horizontal.png" width=250>
@@ -98,7 +98,7 @@ def _deprecate_positional_args(func):
98
98
  f"Passing '{extra_args}' as positional argument(s) to 'param.{name}' "
99
99
  "has been deprecated since Param 2.0.0 and will raise an error in a future version, "
100
100
  "please pass them as keyword arguments.",
101
- ParamPendingDeprecationWarning,
101
+ ParamDeprecationWarning,
102
102
  stacklevel=2,
103
103
  )
104
104
 
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '2.1.0a2'
16
- __version_tuple__ = version_tuple = (2, 1, 0)
15
+ __version__ = version = '2.1.1a0'
16
+ __version_tuple__ = version_tuple = (2, 1, 1)
@@ -164,11 +164,13 @@ def eval_function_with_deps(function):
164
164
  kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw_deps.items()}
165
165
  return function(*args, **kwargs)
166
166
 
167
- def resolve_value(value):
167
+ def resolve_value(value, recursive=True):
168
168
  """
169
169
  Resolves the current value of a dynamic reference.
170
170
  """
171
- if isinstance(value, (list, tuple)):
171
+ if not recursive:
172
+ pass
173
+ elif isinstance(value, (list, tuple)):
172
174
  return type(value)(resolve_value(v) for v in value)
173
175
  elif isinstance(value, dict):
174
176
  return type(value)((resolve_value(k), resolve_value(v)) for k, v in value.items())
@@ -1469,12 +1471,13 @@ class Parameter(_ParameterBase):
1469
1471
  item in a list).
1470
1472
  """
1471
1473
  name = self.name
1472
- if obj is not None and self.allow_refs and obj._param__private.initialized and name not in obj._param__private.syncing:
1474
+ if obj is not None and self.allow_refs and obj._param__private.initialized:
1475
+ syncing = name in obj._param__private.syncing
1473
1476
  ref, deps, val, is_async = obj.param._resolve_ref(self, val)
1474
1477
  refs = obj._param__private.refs
1475
1478
  if ref is not None:
1476
1479
  self.owner.param._update_ref(name, ref)
1477
- elif name in refs:
1480
+ elif name in refs and not syncing:
1478
1481
  del refs[name]
1479
1482
  if name in obj._param__private.async_refs:
1480
1483
  obj._param__private.async_refs.pop(name).cancel()
@@ -2007,14 +2010,15 @@ class Parameters:
2007
2010
  updates = {}
2008
2011
  for pname, ref in self_.self._param__private.refs.items():
2009
2012
  # Skip updating value if dependency has not changed
2010
- deps = resolve_ref(ref, self_[pname].nested_refs)
2013
+ recursive = self_[pname].nested_refs
2014
+ deps = resolve_ref(ref, recursive)
2011
2015
  is_gen = inspect.isgeneratorfunction(ref)
2012
2016
  is_async = iscoroutinefunction(ref) or is_gen
2013
2017
  if not any((dep.owner is e.obj and dep.name == e.name) for dep in deps for e in events) and not is_async:
2014
2018
  continue
2015
2019
 
2016
2020
  try:
2017
- new_val = resolve_value(ref)
2021
+ new_val = resolve_value(ref, recursive)
2018
2022
  except Skip:
2019
2023
  new_val = Undefined
2020
2024
  if new_val is Skip or new_val is Undefined:
@@ -2037,7 +2041,7 @@ class Parameters:
2037
2041
  return None, None, value, False
2038
2042
  ref = value
2039
2043
  try:
2040
- value = resolve_value(value)
2044
+ value = resolve_value(value, recursive=pobj.nested_refs)
2041
2045
  except Skip:
2042
2046
  value = Undefined
2043
2047
  if is_async:
@@ -4196,13 +4200,13 @@ class Parameterized(metaclass=ParameterizedMetaclass):
4196
4200
 
4197
4201
  #PARAM3_DEPRECATION
4198
4202
  @property
4199
- @_deprecated(extra_msg="Use `inst.param.watchers` instead.", warning_cat=FutureWarning)
4203
+ @_deprecated(extra_msg="Use `inst.param.watchers` instead.", warning_cat=_ParamFutureWarning)
4200
4204
  def _param_watchers(self):
4201
4205
  return self._param__private.watchers
4202
4206
 
4203
4207
  #PARAM3_DEPRECATION
4204
4208
  @_param_watchers.setter
4205
- @_deprecated(extra_msg="Use `inst.param.watchers = ...` instead.", warning_cat=FutureWarning)
4209
+ @_deprecated(extra_msg="Use `inst.param.watchers = ...` instead.", warning_cat=_ParamFutureWarning)
4206
4210
  def _param_watchers(self, value):
4207
4211
  self._param__private.watchers = value
4208
4212
 
@@ -81,6 +81,7 @@ display its repr.
81
81
  """
82
82
  from __future__ import annotations
83
83
 
84
+ import asyncio
84
85
  import inspect
85
86
  import math
86
87
  import operator
@@ -90,19 +91,19 @@ from functools import partial
90
91
  from types import FunctionType, MethodType
91
92
  from typing import Any, Callable, Optional
92
93
 
93
- from . import Event
94
94
  from .depends import depends
95
95
  from .display import _display_accessors, _reactive_display_objs
96
96
  from .parameterized import (
97
97
  Parameter, Parameterized, Skip, Undefined, eval_function_with_deps, get_method_owner,
98
98
  register_reference_transform, resolve_ref, resolve_value, transform_reference
99
99
  )
100
- from ._utils import iscoroutinefunction, full_groupby
100
+ from .parameters import Boolean, Event
101
+ from ._utils import _to_async_gen, iscoroutinefunction, full_groupby
101
102
 
102
103
 
103
104
  class Wrapper(Parameterized):
104
105
  """
105
- Simple wrapper to allow updating literal values easily.
106
+ Helper class to allow updating literal values easily.
106
107
  """
107
108
 
108
109
  object = Parameter(allow_refs=False)
@@ -110,20 +111,73 @@ class Wrapper(Parameterized):
110
111
 
111
112
  class GenWrapper(Parameterized):
112
113
  """
113
- Wrapper to allow streaming from generator functions.
114
+ Helper class to allow streaming from generator functions.
114
115
  """
115
116
 
116
117
  object = Parameter(allow_refs=True)
117
118
 
118
119
 
119
120
  class Trigger(Parameterized):
121
+ """
122
+ Helper class to allow triggering an event under some condition.
123
+ """
120
124
 
121
125
  value = Event()
122
126
 
123
- def __init__(self, parameters, **params):
127
+ def __init__(self, parameters=None, internal=False, **params):
124
128
  super().__init__(**params)
129
+ self.internal = internal
125
130
  self.parameters = parameters
126
131
 
132
+ class Resolver(Parameterized):
133
+ """
134
+ Helper class to allow (recursively) resolving references.
135
+ """
136
+
137
+ object = Parameter(allow_refs=True)
138
+
139
+ recursive = Boolean(default=False)
140
+
141
+ value = Parameter()
142
+
143
+ def __init__(self, **params):
144
+ self._watchers = []
145
+ super().__init__(**params)
146
+
147
+ def _resolve_value(self, *events):
148
+ nested = self.param.object.nested_refs
149
+ refs = resolve_ref(self.object, nested)
150
+ value = resolve_value(self.object, nested)
151
+ if self.recursive:
152
+ new_refs = [r for r in resolve_ref(value, nested) if r not in refs]
153
+ while new_refs:
154
+ refs += new_refs
155
+ value = resolve_value(value, nested)
156
+ new_refs = [r for r in resolve_ref(value, nested) if r not in refs]
157
+ if events:
158
+ self._update_refs(refs)
159
+ self.value = value
160
+ return refs
161
+
162
+ @depends('object', watch=True, on_init=True)
163
+ def _resolve_object(self):
164
+ refs = self._resolve_value()
165
+ self._update_refs(refs)
166
+
167
+ def _update_refs(self, refs):
168
+ for w in self._watchers:
169
+ (w.inst or w.cls).param.unwatch(w)
170
+ self._watchers = []
171
+ for _, params in full_groupby(refs, lambda x: id(x.owner)):
172
+ self._watchers.append(
173
+ params[0].owner.param.watch(self._resolve_value, [p.name for p in params])
174
+ )
175
+
176
+
177
+ class NestedResolver(Resolver):
178
+
179
+ object = Parameter(allow_refs=True, nested_refs=True)
180
+
127
181
 
128
182
  class reactive_ops:
129
183
  """
@@ -204,8 +258,17 @@ class reactive_ops:
204
258
  kwargs: mapping, optional
205
259
  A dictionary of keywords to pass to `func`.
206
260
  """
207
- def apply(vs, *args, **kwargs):
208
- return [func(v, *args, **kwargs) for v in vs]
261
+ if inspect.isasyncgenfunction(func) or inspect.isgeneratorfunction(func):
262
+ raise TypeError(
263
+ "Cannot map a generator function. Only regular function "
264
+ "or coroutine functions are permitted."
265
+ )
266
+ if inspect.iscoroutinefunction(func):
267
+ async def apply(vs, *args, **kwargs):
268
+ return list(await asyncio.gather(*(func(v, *args, **kwargs) for v in vs)))
269
+ else:
270
+ def apply(vs, *args, **kwargs):
271
+ return [func(v, *args, **kwargs) for v in vs]
209
272
  return self._as_rx()._apply_operator(apply, *args, **kwargs)
210
273
 
211
274
  def not_(self):
@@ -235,6 +298,27 @@ class reactive_ops:
235
298
  """
236
299
  return self._as_rx()._apply_operator(func, *args, **kwargs)
237
300
 
301
+ def resolve(self, nested=True, recursive=False):
302
+ """
303
+ Resolves references held by the expression.
304
+
305
+ As an example if the expression returns a list of parameters
306
+ this operation will return a list of the parameter values.
307
+
308
+ Arguments
309
+ ---------
310
+ nested: bool
311
+ Whether to resolve references contained within nested objects,
312
+ i.e. tuples, lists, sets and dictionaries.
313
+ recursive: bool
314
+ Whether to recursively resolve references, i.e. if a reference
315
+ itself returns a reference we recurse into it until no more
316
+ references can be resolved.
317
+ """
318
+ resolver_type = NestedResolver if nested else Resolver
319
+ resolver = resolver_type(object=self._reactive, recursive=recursive)
320
+ return resolver.param.value.rx()
321
+
238
322
  def updating(self):
239
323
  """
240
324
  Returns a new expression that is True while the expression is updating.
@@ -646,19 +730,27 @@ class rx:
646
730
  self._depth = depth
647
731
  self._dirty = _current is None
648
732
  self._dirty_obj = False
733
+ self._current_task = None
649
734
  self._error_state = None
650
735
  self._current_ = _current
651
736
  if isinstance(obj, rx) and not prev:
652
737
  self._prev = obj
653
738
  else:
654
739
  self._prev = prev
740
+
741
+ # Define special trigger parameter if operation has to be lazily evaluated
742
+ if operation and (iscoroutinefunction(operation['fn']) or inspect.isgeneratorfunction(operation['fn'])):
743
+ self._trigger = Trigger(internal=True)
744
+ self._current_ = Undefined
745
+ else:
746
+ self._trigger = None
655
747
  self._root = self._compute_root()
656
748
  self._fn_params = self._compute_fn_params()
657
749
  self._internal_params = self._compute_params()
658
750
  # Filter params that external objects depend on, ensuring
659
751
  # that Trigger parameters do not cause double execution
660
752
  self._params = [
661
- p for p in self._internal_params if not isinstance(p.owner, Trigger)
753
+ p for p in self._internal_params if (not isinstance(p.owner, Trigger) or p.owner.internal)
662
754
  or any (p not in self._internal_params for p in p.owner.parameters)
663
755
  ]
664
756
  self._setup_invalidations(depth)
@@ -721,7 +813,9 @@ class rx:
721
813
  return args + kwargs
722
814
 
723
815
  def _compute_params(self) -> list[Parameter]:
724
- ps = self._fn_params
816
+ ps = list(self._fn_params)
817
+ if self._trigger:
818
+ ps.append(self._trigger.param.value)
725
819
 
726
820
  # Collect parameters on previous objects in chain
727
821
  prev = self._prev
@@ -735,6 +829,9 @@ class rx:
735
829
  return ps
736
830
 
737
831
  # Accumulate dependencies in args and/or kwargs
832
+ for ref in resolve_ref(self._operation['fn']):
833
+ if ref not in ps:
834
+ ps.append(ref)
738
835
  for arg in list(self._operation['args'])+list(self._operation['kwargs'].values()):
739
836
  for ref in resolve_ref(arg):
740
837
  if ref not in ps:
@@ -763,11 +860,15 @@ class rx:
763
860
  """
764
861
  if self._fn is not None:
765
862
  for _, params in full_groupby(self._fn_params, lambda x: id(x.owner)):
766
- params[0].owner.param._watch(self._invalidate_obj, [p.name for p in params], precedence=-1)
863
+ fps = [p.name for p in params if p in self._root._fn_params]
864
+ if fps:
865
+ params[0].owner.param._watch(self._invalidate_obj, fps, precedence=-1)
767
866
  for _, params in full_groupby(self._internal_params, lambda x: id(x.owner)):
768
867
  params[0].owner.param._watch(self._invalidate_current, [p.name for p in params], precedence=-1)
769
868
 
770
869
  def _invalidate_current(self, *events):
870
+ if all(event.obj is self._trigger for event in events):
871
+ return
771
872
  self._dirty = True
772
873
  self._error_state = None
773
874
 
@@ -775,6 +876,26 @@ class rx:
775
876
  self._root._dirty_obj = True
776
877
  self._error_state = None
777
878
 
879
+ async def _resolve_async(self, obj):
880
+ self._current_task = task = asyncio.current_task()
881
+ if inspect.isasyncgen(obj):
882
+ async for val in obj:
883
+ if self._current_task is not task:
884
+ break
885
+ self._current_ = val
886
+ self._trigger.param.trigger('value')
887
+ else:
888
+ value = await obj
889
+ if self._current_task is task:
890
+ self._current_ = value
891
+ self._trigger.param.trigger('value')
892
+
893
+ def _lazy_resolve(self, obj):
894
+ from .parameterized import async_executor
895
+ if inspect.isgenerator(obj):
896
+ obj = _to_async_gen(obj)
897
+ async_executor(partial(self._resolve_async, obj))
898
+
778
899
  def _resolve(self):
779
900
  if self._error_state:
780
901
  raise self._error_state
@@ -787,9 +908,13 @@ class rx:
787
908
  operation = self._operation
788
909
  if operation:
789
910
  obj = self._eval_operation(obj, operation)
911
+ if inspect.isasyncgen(obj) or inspect.iscoroutine(obj) or inspect.isgenerator(obj):
912
+ self._lazy_resolve(obj)
913
+ obj = Skip
790
914
  if obj is Skip:
791
915
  raise Skip
792
916
  except Skip:
917
+ self._dirty = False
793
918
  return self._current_
794
919
  except Exception as e:
795
920
  self._error_state = e
@@ -22,7 +22,7 @@ def specific_filter():
22
22
  class TestDeprecateParameter:
23
23
 
24
24
  def test_deprecate_posargs_Parameter(self):
25
- with pytest.raises(param._utils.ParamPendingDeprecationWarning):
25
+ with pytest.raises(param._utils.ParamDeprecationWarning):
26
26
  param.Parameter(1, 'doc')
27
27
 
28
28
  def test_deprecate_List_class_(self):
@@ -103,11 +103,11 @@ class TestDeprecateParameterizedModule:
103
103
  param.parameterized.all_equal(1, 1)
104
104
 
105
105
  def test_deprecate_param_watchers(self):
106
- with pytest.raises(FutureWarning):
106
+ with pytest.raises(param._utils.ParamFutureWarning):
107
107
  param.parameterized.Parameterized()._param_watchers
108
108
 
109
109
  def test_deprecate_param_watchers_setter(self):
110
- with pytest.raises(FutureWarning):
110
+ with pytest.raises(param._utils.ParamFutureWarning):
111
111
  param.parameterized.Parameterized()._param_watchers = {}
112
112
 
113
113
  def test_param_error_unsafe_ops_before_initialized(self):
@@ -27,6 +27,8 @@ import pytest
27
27
  from param.parameterized import Skip
28
28
  from param.reactive import bind, rx
29
29
 
30
+ from .utils import async_wait_until
31
+
30
32
  NUMERIC_BINARY_OPERATORS = (
31
33
  operator.add, divmod, operator.floordiv, operator.mod, operator.mul,
32
34
  operator.pow, operator.sub, operator.truediv,
@@ -67,6 +69,8 @@ class Parameters(param.Parameterized):
67
69
 
68
70
  boolean = param.Boolean(default=False)
69
71
 
72
+ parameter = param.Parameter(allow_refs=False)
73
+
70
74
  event = param.Event()
71
75
 
72
76
  @param.depends('integer')
@@ -305,6 +309,19 @@ def test_reactive_map():
305
309
  i.rx.value = range(1, 4)
306
310
  assert b.rx.value == [2, 4, 6]
307
311
 
312
+ async def test_reactive_async_map():
313
+ i = rx(range(3))
314
+ async def mul(x):
315
+ await asyncio.sleep(0.05)
316
+ return x*2
317
+ b = i.rx.map(mul)
318
+ assert b.rx.value is param.Undefined
319
+ await async_wait_until(lambda: b.rx.value == [0, 2, 4])
320
+ i.rx.value = range(1, 4)
321
+ assert b.rx.value == [0, 2, 4]
322
+ await async_wait_until(lambda: b.rx.value == [2, 4, 6])
323
+ assert b.rx.value == [2, 4, 6]
324
+
308
325
  def test_reactive_map_args():
309
326
  i = rx(range(3))
310
327
  j = rx(2)
@@ -393,11 +410,7 @@ async def test_reactive_watch_async_on_event():
393
410
  items = []
394
411
  event.rx.watch(items.append)
395
412
  p.param.trigger('event')
396
- for _ in range(3):
397
- await asyncio.sleep(0.05)
398
- if items == [True]:
399
- break
400
- assert items == [True]
413
+ await async_wait_until(lambda: items == [True])
401
414
 
402
415
  def test_reactive_set_value_non_root_raises():
403
416
  rx_val = rx(1) + 1
@@ -433,6 +446,96 @@ def test_reactive_when_initial():
433
446
  p.param.trigger('event')
434
447
  assert integer.rx.value == 4
435
448
 
449
+ def test_reactive_resolve():
450
+ p = Parameters(integer=3)
451
+ p2 = Parameters(parameter=p.param.integer)
452
+
453
+ prx = p2.param.parameter.rx()
454
+ assert prx.rx.value is p.param.integer
455
+
456
+ resolved_prx = prx.rx.resolve()
457
+ assert resolved_prx.rx.value == 3
458
+
459
+ changes = []
460
+ resolved_prx.rx.watch(changes.append)
461
+
462
+ # Test changing referenced value
463
+ p.integer = 4
464
+ assert resolved_prx.rx.value == 4
465
+ assert changes == [4]
466
+
467
+ # Test changing reference itself
468
+ p2.parameter = p.param.number
469
+ assert resolved_prx.rx.value == 3.14
470
+ assert changes == [4, 3.14]
471
+
472
+ # Ensure no updates triggered when old reference is updated
473
+ p.integer = 5
474
+ assert resolved_prx.rx.value == 3.14
475
+ assert changes == [4, 3.14]
476
+
477
+ def test_reactive_resolve_nested():
478
+ p = Parameters(integer=3)
479
+ p2 = Parameters(parameter=[p.param.integer])
480
+
481
+ prx = p2.param.parameter.rx()
482
+ assert prx.rx.value == [p.param.integer]
483
+
484
+ resolved_prx = prx.rx.resolve(nested=True)
485
+ assert resolved_prx.rx.value == [3]
486
+
487
+ changes = []
488
+ resolved_prx.rx.watch(changes.append)
489
+
490
+ # Test changing referenced value
491
+ p.integer = 4
492
+ assert resolved_prx.rx.value == [4]
493
+ assert changes == [[4]]
494
+
495
+ # Test changing reference itself
496
+ p2.parameter = [p.param.number]
497
+ assert resolved_prx.rx.value == [3.14]
498
+ assert changes == [[4], [3.14]]
499
+
500
+ # Ensure no updates triggered when old reference is updated
501
+ p.integer = 5
502
+ assert resolved_prx.rx.value == [3.14]
503
+ assert changes == [[4], [3.14]]
504
+
505
+ def test_reactive_resolve_recursive():
506
+ p = Parameters(integer=3)
507
+ p2 = Parameters(parameter=p.param.integer)
508
+ p3 = Parameters(parameter=p2.param.parameter)
509
+
510
+ prx = p3.param.parameter.rx()
511
+ assert prx.rx.value is p2.param.parameter
512
+
513
+ resolved_prx = prx.rx.resolve(recursive=True)
514
+ assert resolved_prx.rx.value == 3
515
+
516
+ changes = []
517
+ resolved_prx.rx.watch(changes.append)
518
+
519
+ # Test changing referenced value
520
+ p.integer = 4
521
+ assert resolved_prx.rx.value == 4
522
+ assert changes == [4]
523
+
524
+ # Test changing recursive reference
525
+ p2.parameter = p.param.number
526
+ assert resolved_prx.rx.value == 3.14
527
+ assert changes == [4, 3.14]
528
+
529
+ # Ensure no updates triggered when old reference is updated
530
+ p.integer = 5
531
+ assert resolved_prx.rx.value == 3.14
532
+ assert changes == [4, 3.14]
533
+
534
+ # Test changing reference itself
535
+ p3.parameter = p.param.string
536
+ assert resolved_prx.rx.value == 'string'
537
+ assert changes == [4, 3.14, 'string']
538
+
436
539
  async def test_reactive_async_func():
437
540
  async def async_func():
438
541
  await asyncio.sleep(0.02)
@@ -440,8 +543,17 @@ async def test_reactive_async_func():
440
543
 
441
544
  async_rx = rx(async_func) + 2
442
545
  assert async_rx.rx.value is param.Undefined
443
- await asyncio.sleep(0.04)
444
- assert async_rx.rx.value == 4
546
+ await async_wait_until(lambda: async_rx.rx.value == 4)
547
+
548
+ async def test_reactive_pipe_async_func():
549
+ async def async_func(value):
550
+ await asyncio.sleep(0.02)
551
+ return value+2
552
+
553
+ async_rx = rx(0).rx.pipe(async_func)
554
+ async_rx.rx.watch()
555
+ assert async_rx.rx.value is param.Undefined
556
+ await async_wait_until(lambda: async_rx.rx.value == 2)
445
557
 
446
558
  async def test_reactive_gen():
447
559
  def gen():
@@ -453,11 +565,25 @@ async def test_reactive_gen():
453
565
  assert rxgen.rx.value is param.Undefined
454
566
  await asyncio.sleep(0.04)
455
567
  assert rxgen.rx.value == 1
456
- for _ in range(3):
457
- await asyncio.sleep(0.05)
458
- if rxgen.rx.value == 2:
459
- break
460
- assert rxgen.rx.value == 2
568
+ await async_wait_until(lambda: rxgen.rx.value == 2)
569
+
570
+ async def test_reactive_gen_pipe():
571
+ def gen(val):
572
+ yield val+1
573
+ time.sleep(0.05)
574
+ yield val+2
575
+
576
+ rxv = rx(0)
577
+ rxgen = rxv.rx.pipe(gen)
578
+ rxgen.rx.watch()
579
+ assert rxgen.rx.value is param.Undefined
580
+ await asyncio.sleep(0.04)
581
+ assert rxgen.rx.value == 1
582
+ await async_wait_until(lambda: rxgen.rx.value == 2)
583
+ rxv.rx.value = 2
584
+ await asyncio.sleep(0.04)
585
+ assert rxgen.rx.value == 3
586
+ await async_wait_until(lambda: rxgen.rx.value == 4)
461
587
 
462
588
  async def test_reactive_gen_with_dep():
463
589
  def gen(i):
@@ -473,24 +599,53 @@ async def test_reactive_gen_with_dep():
473
599
  irx.rx.value = 3
474
600
  await asyncio.sleep(0.04)
475
601
  assert rxgen.rx.value == 4
476
- for _ in range(3):
477
- await asyncio.sleep(0.05)
478
- if rxgen.rx.value == 5:
479
- break
480
- assert rxgen.rx.value == 5
602
+ await async_wait_until(lambda: rxgen.rx.value == 5)
603
+
604
+ async def test_reactive_gen_pipe_with_dep():
605
+ def gen(value, i):
606
+ yield value+i+1
607
+ time.sleep(0.05)
608
+ yield value+i+2
609
+
610
+ irx = rx(0)
611
+ rxv = rx(0)
612
+ rxgen = rxv.rx.pipe(bind(gen, irx))
613
+ rxgen.rx.watch()
614
+ assert rxgen.rx.value is param.Undefined
615
+ await asyncio.sleep(0.04)
616
+ assert rxgen.rx.value == 1
617
+ irx.rx.value = 3
618
+ await asyncio.sleep(0.04)
619
+ assert rxgen.rx.value == 4
620
+ await async_wait_until(lambda: rxgen.rx.value == 5)
621
+ rxv.rx.value = 5
622
+ await asyncio.sleep(0.04)
623
+ assert rxgen.rx.value == 9
624
+ await async_wait_until(lambda: rxgen.rx.value == 10)
481
625
 
482
626
  async def test_reactive_async_gen():
483
627
  async def gen():
484
628
  yield 1
485
- await asyncio.sleep(0.1)
629
+ await asyncio.sleep(0.05)
486
630
  yield 2
487
631
 
488
632
  rxgen = rx(gen)
489
633
  assert rxgen.rx.value is param.Undefined
490
- await asyncio.sleep(0.05)
634
+ await asyncio.sleep(0.04)
491
635
  assert rxgen.rx.value == 1
492
- await asyncio.sleep(0.1)
493
- assert rxgen.rx.value == 2
636
+ await async_wait_until(lambda: rxgen.rx.value == 2)
637
+
638
+ async def test_reactive_async_gen_pipe():
639
+ async def gen(value):
640
+ yield value + 1
641
+ await asyncio.sleep(0.05)
642
+ yield value + 2
643
+
644
+ rxgen = rx(0).rx.pipe(gen)
645
+ assert rxgen.rx.value is param.Undefined
646
+ await asyncio.sleep(0.04)
647
+ assert rxgen.rx.value == 1
648
+ await async_wait_until(lambda: rxgen.rx.value == 2)
494
649
 
495
650
  async def test_reactive_async_gen_with_dep():
496
651
  async def gen(i):
@@ -508,3 +663,81 @@ async def test_reactive_async_gen_with_dep():
508
663
  irx.rx.value = 4
509
664
  await asyncio.sleep(0.1)
510
665
  assert rxgen.rx.value == 5
666
+
667
+ async def test_reactive_async_gen_pipe_with_dep():
668
+ async def gen(value, i):
669
+ yield value+i+1
670
+ await asyncio.sleep(0.05)
671
+ yield value+i+2
672
+
673
+ irx = rx(0)
674
+ rxv = rx(0)
675
+ rxgen = rxv.rx.pipe(bind(gen, i=irx))
676
+ rxgen.rx.watch()
677
+ assert rxgen.rx.value is param.Undefined
678
+ await asyncio.sleep(0.04)
679
+ assert rxgen.rx.value == 1
680
+ irx.rx.value = 3
681
+ await asyncio.sleep(0.04)
682
+ irx.rx.value = 4
683
+ await asyncio.sleep(0.04)
684
+ assert rxgen.rx.value == 5
685
+ rxv.rx.value = 5
686
+ await asyncio.sleep(0.04)
687
+ assert rxgen.rx.value == 10
688
+ await async_wait_until(lambda: rxgen.rx.value == 11)
689
+
690
+ def test_root_invalidation():
691
+ arx = rx('a')
692
+ brx = rx('b')
693
+
694
+ computed = []
695
+ def debug(value, info):
696
+ computed.append(info)
697
+ return value
698
+
699
+ expr = arx.title().rx.pipe(debug, '1') + brx.title().rx.pipe(debug, '2')
700
+
701
+ assert expr.rx.value == 'AB'
702
+ assert computed == ['1', '2']
703
+
704
+ brx.rx.value = 'c'
705
+
706
+ assert expr.rx.value == 'AC'
707
+ assert computed == ['1', '2', '2']
708
+
709
+ arx.rx.value = 'd'
710
+ assert expr.rx.value == 'DC'
711
+ assert computed == ['1', '2', '2', '1']
712
+
713
+
714
+ def test_ensure_ref_can_update_by_watcher_of_same_parameter():
715
+ # https://github.com/holoviz/param/pull/929
716
+
717
+ class W(param.Parameterized):
718
+ value = param.String()
719
+
720
+
721
+ class T(param.Parameterized):
722
+ lst = param.List(allow_refs=True, allow_None=True)
723
+
724
+ @param.depends("lst", watch=True)
725
+ def test(self):
726
+ lst = self.lst or range(5)
727
+ items = [W(value=str(i)) for i in lst]
728
+ with param.discard_events(self):
729
+ self.lst = param.rx(items).rx.resolve()
730
+ self.items = items
731
+
732
+ def transform(obj):
733
+ if isinstance(obj, W):
734
+ return obj.param.value
735
+ return obj
736
+
737
+
738
+ param.reactive.register_reference_transform(transform)
739
+
740
+ t = T()
741
+ t.lst = list("ABCDE")
742
+ t.items[1].value = "TEST"
743
+ assert t.lst[1] == "TEST"
@@ -97,17 +97,17 @@ def test_signature_position_keywords():
97
97
  def test_signature_warning_by_position():
98
98
  # Simple test as it's tricky to automatically test all the Parameters
99
99
  with pytest.warns(
100
- param._utils.ParamPendingDeprecationWarning,
100
+ param._utils.ParamDeprecationWarning,
101
101
  match=r"Passing 'objects' as positional argument\(s\) to 'param.Selector' has been deprecated since Param 2.0.0 and will raise an error in a future version, please pass them as keyword arguments"
102
102
  ):
103
103
  param.Selector([0, 1]) # objects
104
104
  with pytest.warns(
105
- param._utils.ParamPendingDeprecationWarning,
105
+ param._utils.ParamDeprecationWarning,
106
106
  match=r"Passing 'class_' as positional argument\(s\) to 'param.ClassSelector' has been deprecated since Param 2.0.0 and will raise an error in a future version, please pass them as keyword arguments"
107
107
  ):
108
108
  param.ClassSelector(int) # class_
109
109
  with pytest.warns(
110
- param._utils.ParamPendingDeprecationWarning,
110
+ param._utils.ParamDeprecationWarning,
111
111
  match=r"Passing 'bounds, softbounds' as positional argument\(s\) to 'param.Number' has been deprecated since Param 2.0.0 and will raise an error in a future version, please pass them as keyword arguments"
112
112
  ):
113
113
  param.Number(1, (0, 2), (0, 2)) # default (OK), bounds (not OK), softbounds (not OK)
@@ -652,7 +652,7 @@ class TestWatch(unittest.TestCase):
652
652
 
653
653
  obj.param.watch(lambda: '', ['a', 'b'])
654
654
 
655
- with pytest.warns(FutureWarning):
655
+ with pytest.warns(param._utils.ParamFutureWarning):
656
656
  pw = obj._param_watchers
657
657
  assert isinstance(pw, dict)
658
658
  for pname in ('a', 'b'):
@@ -667,7 +667,7 @@ class TestWatch(unittest.TestCase):
667
667
 
668
668
  obj.param.watch(accumulator, ['a', 'b'])
669
669
 
670
- with pytest.warns(FutureWarning):
670
+ with pytest.warns(param._utils.ParamFutureWarning):
671
671
  pw = obj._param_watchers
672
672
  del pw['a']
673
673
 
@@ -1,4 +1,6 @@
1
+ import asyncio
1
2
  import logging
3
+ import time
2
4
 
3
5
  from contextlib import contextmanager
4
6
 
@@ -111,3 +113,64 @@ def warnings_as_excepts(match=None):
111
113
  raise ValueError(f'Exception emitted {str(e)!r} does not contain {match!r}')
112
114
  finally:
113
115
  param.parameterized.warnings_as_exceptions = orig
116
+
117
+
118
+ async def async_wait_until(fn, timeout=5000, interval=100):
119
+ """
120
+ Exercise a test function in a loop until it evaluates to True
121
+ or times out.
122
+
123
+ The function can either be a simple lambda that returns True or False:
124
+ >>> await async_wait_until(lambda: x.values() == ['x'])
125
+
126
+ Or a defined function with an assert:
127
+ >>> async def _()
128
+ >>> assert x.values() == ['x']
129
+ >>> await async_wait_until(_)
130
+
131
+ Parameters
132
+ ----------
133
+ fn : callable
134
+ Callback
135
+ timeout : int, optional
136
+ Total timeout in milliseconds, by default 5000
137
+ interval : int, optional
138
+ Waiting interval, by default 100
139
+
140
+ Adapted from pytest-qt.
141
+ """
142
+ # Hide this function traceback from the pytest output if the test fails
143
+ __tracebackhide__ = True
144
+
145
+ start = time.monotonic()
146
+
147
+ def timed_out():
148
+ elapsed = time.monotonic() - start
149
+ elapsed_ms = elapsed * 1000
150
+ return elapsed_ms > timeout
151
+
152
+ timeout_msg = f"async_wait_until timed out in {timeout} milliseconds"
153
+
154
+ while True:
155
+ try:
156
+ result = fn()
157
+ if asyncio.iscoroutine(result):
158
+ result = await result
159
+ except AssertionError as e:
160
+ if timed_out():
161
+ raise TimeoutError(timeout_msg) from e
162
+ else:
163
+ if result not in (None, True, False):
164
+ raise ValueError(
165
+ "`async_wait_until` callback must return None, True, or "
166
+ f"False, returned {result!r}"
167
+ )
168
+ # None is returned when the function has an assert
169
+ if result is None:
170
+ return
171
+ # When the function returns True or False
172
+ if result:
173
+ return
174
+ if timed_out():
175
+ raise TimeoutError(timeout_msg)
176
+ await asyncio.sleep(interval / 1000)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes