param 2.3.3b0__tar.gz → 2.4.0a0__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 (73) hide show
  1. {param-2.3.3b0 → param-2.4.0a0}/PKG-INFO +2 -1
  2. {param-2.3.3b0 → param-2.4.0a0}/param/__init__.py +5 -6
  3. {param-2.3.3b0 → param-2.4.0a0}/param/_utils.py +88 -65
  4. {param-2.3.3b0 → param-2.4.0a0}/param/_version.py +2 -2
  5. param-2.4.0a0/param/depends.py +233 -0
  6. {param-2.3.3b0 → param-2.4.0a0}/param/display.py +2 -2
  7. {param-2.3.3b0 → param-2.4.0a0}/param/ipython.py +29 -17
  8. {param-2.3.3b0 → param-2.4.0a0}/param/parameterized.py +853 -485
  9. {param-2.3.3b0 → param-2.4.0a0}/param/parameters.py +1760 -593
  10. param-2.4.0a0/param/py.typed +0 -0
  11. {param-2.3.3b0 → param-2.4.0a0}/param/reactive.py +223 -95
  12. {param-2.3.3b0 → param-2.4.0a0}/param/serializer.py +94 -47
  13. {param-2.3.3b0 → param-2.4.0a0}/pyproject.toml +54 -2
  14. {param-2.3.3b0 → param-2.4.0a0}/tests/testimports.py +1 -7
  15. {param-2.3.3b0 → param-2.4.0a0}/tests/testparameterizedobject.py +24 -0
  16. {param-2.3.3b0 → param-2.4.0a0}/tests/testreactive.py +318 -130
  17. {param-2.3.3b0 → param-2.4.0a0}/tests/testsignatures.py +29 -11
  18. {param-2.3.3b0 → param-2.4.0a0}/tests/utils.py +1 -2
  19. param-2.3.3b0/param/depends.py +0 -154
  20. {param-2.3.3b0 → param-2.4.0a0}/.gitignore +0 -0
  21. {param-2.3.3b0 → param-2.4.0a0}/LICENSE.txt +0 -0
  22. {param-2.3.3b0 → param-2.4.0a0}/README.md +0 -0
  23. {param-2.3.3b0 → param-2.4.0a0}/numbergen/__init__.py +0 -0
  24. {param-2.3.3b0 → param-2.4.0a0}/param/version.py +0 -0
  25. {param-2.3.3b0 → param-2.4.0a0}/tests/__init__.py +0 -0
  26. {param-2.3.3b0 → param-2.4.0a0}/tests/conftest.py +0 -0
  27. {param-2.3.3b0 → param-2.4.0a0}/tests/testaddparameter.py +0 -0
  28. {param-2.3.3b0 → param-2.4.0a0}/tests/testbind.py +0 -0
  29. {param-2.3.3b0 → param-2.4.0a0}/tests/testbooleanparam.py +0 -0
  30. {param-2.3.3b0 → param-2.4.0a0}/tests/testbytesparam.py +0 -0
  31. {param-2.3.3b0 → param-2.4.0a0}/tests/testcalendardateparam.py +0 -0
  32. {param-2.3.3b0 → param-2.4.0a0}/tests/testcalendardaterangeparam.py +0 -0
  33. {param-2.3.3b0 → param-2.4.0a0}/tests/testcallable.py +0 -0
  34. {param-2.3.3b0 → param-2.4.0a0}/tests/testclassselector.py +0 -0
  35. {param-2.3.3b0 → param-2.4.0a0}/tests/testcolorparameter.py +0 -0
  36. {param-2.3.3b0 → param-2.4.0a0}/tests/testcomparator.py +0 -0
  37. {param-2.3.3b0 → param-2.4.0a0}/tests/testcompositeparams.py +0 -0
  38. {param-2.3.3b0 → param-2.4.0a0}/tests/testcustomparam.py +0 -0
  39. {param-2.3.3b0 → param-2.4.0a0}/tests/testdateparam.py +0 -0
  40. {param-2.3.3b0 → param-2.4.0a0}/tests/testdaterangeparam.py +0 -0
  41. {param-2.3.3b0 → param-2.4.0a0}/tests/testdefaultfactory.py +0 -0
  42. {param-2.3.3b0 → param-2.4.0a0}/tests/testdefaults.py +0 -0
  43. {param-2.3.3b0 → param-2.4.0a0}/tests/testdeprecations.py +0 -0
  44. {param-2.3.3b0 → param-2.4.0a0}/tests/testdynamicparams.py +0 -0
  45. {param-2.3.3b0 → param-2.4.0a0}/tests/testfiledeserialization.py +0 -0
  46. {param-2.3.3b0 → param-2.4.0a0}/tests/testfileselector.py +0 -0
  47. {param-2.3.3b0 → param-2.4.0a0}/tests/testipythonmagic.py +0 -0
  48. {param-2.3.3b0 → param-2.4.0a0}/tests/testjsonserialization.py +0 -0
  49. {param-2.3.3b0 → param-2.4.0a0}/tests/testlist.py +0 -0
  50. {param-2.3.3b0 → param-2.4.0a0}/tests/testlistselector.py +0 -0
  51. {param-2.3.3b0 → param-2.4.0a0}/tests/testmultifileselector.py +0 -0
  52. {param-2.3.3b0 → param-2.4.0a0}/tests/testnumbergen.py +0 -0
  53. {param-2.3.3b0 → param-2.4.0a0}/tests/testnumberparameter.py +0 -0
  54. {param-2.3.3b0 → param-2.4.0a0}/tests/testnumpy.py +0 -0
  55. {param-2.3.3b0 → param-2.4.0a0}/tests/testobjectselector.py +0 -0
  56. {param-2.3.3b0 → param-2.4.0a0}/tests/testpandas.py +0 -0
  57. {param-2.3.3b0 → param-2.4.0a0}/tests/testparamdepends.py +0 -0
  58. {param-2.3.3b0 → param-2.4.0a0}/tests/testparameter.py +0 -0
  59. {param-2.3.3b0 → param-2.4.0a0}/tests/testparameterizedrepr.py +0 -0
  60. {param-2.3.3b0 → param-2.4.0a0}/tests/testparamoutput.py +0 -0
  61. {param-2.3.3b0 → param-2.4.0a0}/tests/testparamunion.py +0 -0
  62. {param-2.3.3b0 → param-2.4.0a0}/tests/testpathparam.py +0 -0
  63. {param-2.3.3b0 → param-2.4.0a0}/tests/testpickle.py +0 -0
  64. {param-2.3.3b0 → param-2.4.0a0}/tests/testrangeparameter.py +0 -0
  65. {param-2.3.3b0 → param-2.4.0a0}/tests/testrefs.py +0 -0
  66. {param-2.3.3b0 → param-2.4.0a0}/tests/testreprhtml.py +0 -0
  67. {param-2.3.3b0 → param-2.4.0a0}/tests/testselector.py +0 -0
  68. {param-2.3.3b0 → param-2.4.0a0}/tests/teststringparam.py +0 -0
  69. {param-2.3.3b0 → param-2.4.0a0}/tests/testtimedependent.py +0 -0
  70. {param-2.3.3b0 → param-2.4.0a0}/tests/testtupleparam.py +0 -0
  71. {param-2.3.3b0 → param-2.4.0a0}/tests/testutils.py +0 -0
  72. {param-2.3.3b0 → param-2.4.0a0}/tests/testversion.py +0 -0
  73. {param-2.3.3b0 → param-2.4.0a0}/tests/testwatch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: param
3
- Version: 2.3.3b0
3
+ Version: 2.4.0a0
4
4
  Summary: Declarative parameters for robust Python classes and a rich API for reactive programming
5
5
  Project-URL: Homepage, https://param.holoviz.org/
6
6
  Project-URL: Tracker, https://github.com/holoviz/param/issues
@@ -25,6 +25,7 @@ Classifier: Programming Language :: Python :: 3.13
25
25
  Classifier: Programming Language :: Python :: 3.14
26
26
  Classifier: Topic :: Scientific/Engineering
27
27
  Classifier: Topic :: Software Development :: Libraries
28
+ Classifier: Typing :: Typed
28
29
  Requires-Python: >=3.10
29
30
  Provides-Extra: all
30
31
  Requires-Dist: aiohttp; extra == 'all'
@@ -112,10 +112,11 @@ from ._utils import (
112
112
  try:
113
113
  # For performance reasons on imports, avoid importing setuptools_scm
114
114
  # if not in a .git folder
115
+ __version__ : str
115
116
  if os.path.exists(os.path.join(os.path.dirname(__file__), "..", ".git")):
116
117
  # If setuptools_scm is installed (e.g. in a development environment with
117
118
  # an editable install), then use it to determine the version dynamically.
118
- from setuptools_scm import get_version
119
+ from setuptools_scm import get_version # type: ignore[unresolved-import,import-untyped,reportMissingImports]
119
120
 
120
121
  # This will fail with LookupError if the package is not installed in
121
122
  # editable mode or if Git is not installed.
@@ -127,7 +128,7 @@ except (ImportError, LookupError, FileNotFoundError):
127
128
  try:
128
129
  # __version__ was added in _version in setuptools-scm 7.0.0, we rely on
129
130
  # the hopefully stable version variable.
130
- from ._version import version as __version__
131
+ from ._version import version as __version__ # type: ignore[unresolved-import,no-redef] # pyright: ignore[reportMissingImports]
131
132
  except (ModuleNotFoundError, ImportError):
132
133
  # Either _version doesn't exist (ModuleNotFoundError) or version isn't
133
134
  # in _version (ImportError). ModuleNotFoundError is a subclass of
@@ -137,7 +138,7 @@ except (ImportError, LookupError, FileNotFoundError):
137
138
  from importlib.metadata import version as mversion, PackageNotFoundError
138
139
 
139
140
  try:
140
- __version__ = mversion("param")
141
+ __version__ = str(mversion("param"))
141
142
  except PackageNotFoundError:
142
143
  # The user is probably trying to run this without having installed
143
144
  # the package.
@@ -145,8 +146,7 @@ except (ImportError, LookupError, FileNotFoundError):
145
146
 
146
147
  #: Top-level object to allow messaging not tied to a particular
147
148
  #: Parameterized object, as in 'param.main.warning("Invalid option")'.
148
- main=Parameterized(name="main")
149
-
149
+ main = Parameterized(name="main")
150
150
 
151
151
  # A global random seed (integer or rational) available for controlling
152
152
  # the behaviour of Parameterized objects with random state.
@@ -227,7 +227,6 @@ __all__ = (
227
227
  'resolve_path',
228
228
  'rx',
229
229
  'script_repr',
230
- 'serializer',
231
230
  'shared_parameters',
232
231
  'version',
233
232
  )
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import collections
4
3
  import datetime as dt
5
4
  import functools
6
5
  import inspect
@@ -8,20 +7,19 @@ import numbers
8
7
  import os
9
8
  import re
10
9
  import traceback
10
+ import typing as t
11
11
  import warnings
12
12
  from collections import OrderedDict, abc, defaultdict
13
13
  from contextlib import contextmanager
14
- from numbers import Real
15
14
  from textwrap import dedent
16
15
  from threading import get_ident
17
- from typing import TYPE_CHECKING, Callable, TypeVar
18
16
 
19
- if TYPE_CHECKING:
20
- from typing_extensions import Concatenate, ParamSpec
17
+ if t.TYPE_CHECKING:
18
+ from param.parameterized import Parameter
21
19
 
22
- P = ParamSpec("P")
23
- R = TypeVar("R")
24
- CallableT = TypeVar("CallableT", bound=Callable)
20
+ _P = t.ParamSpec("_P")
21
+ _R = t.TypeVar("_R")
22
+ _CallableT = t.TypeVar("_CallableT", bound=abc.Callable)
25
23
 
26
24
  DEFAULT_SIGNATURE = inspect.Signature([
27
25
  inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
@@ -99,8 +97,8 @@ class Skip(Exception):
99
97
  """
100
98
 
101
99
 
102
- def _deprecated(extra_msg="", warning_cat=ParamDeprecationWarning):
103
- def decorator(func):
100
+ def _deprecated(extra_msg: str = "", warning_cat: type[Warning] = ParamDeprecationWarning):
101
+ def decorator(func: abc.Callable[..., t.Any]) -> abc.Callable[..., t.Any]:
104
102
  """Mark a function or method as deprecated.
105
103
 
106
104
  This internal decorator issues a warning when the decorated function
@@ -119,7 +117,8 @@ def _deprecated(extra_msg="", warning_cat=ParamDeprecationWarning):
119
117
  """
120
118
  @functools.wraps(func)
121
119
  def inner(*args, **kwargs):
122
- msg = f"{func.__name__!r} has been deprecated and will be removed in a future version."
120
+ func_name = getattr(func, "__name__", repr(func))
121
+ msg = f"{func_name!r} has been deprecated and will be removed in a future version."
123
122
  if extra_msg:
124
123
  em = dedent(extra_msg)
125
124
  em = em.strip().replace('\n', ' ')
@@ -152,8 +151,9 @@ def _recursive_repr(fillvalue='...'):
152
151
  return decorating_function
153
152
 
154
153
 
155
- def _is_auto_name(class_name, instance_name):
156
- return re.match('^'+class_name+'[0-9]{5}$', instance_name)
154
+ def _is_auto_name(class_name: str, instance_name: str) -> bool:
155
+ pattern = rf'^{re.escape(class_name)}[0-9]{{5}}$'
156
+ return bool(re.match(pattern, instance_name))
157
157
 
158
158
 
159
159
  def _find_pname(pclass):
@@ -163,12 +163,14 @@ def _find_pname(pclass):
163
163
  """
164
164
  stack = traceback.extract_stack()
165
165
  for frame in stack:
166
- match = re.match(r"^(\S+)\s*=\s*(param|pm)\." + pclass + r"\(", frame.line)
166
+ if frame.line is None:
167
+ continue
168
+ match = re.match(rf"^(\S+)\s*=\s*(param|pm)\.{pclass}\(", frame.line)
167
169
  if match:
168
170
  return match.group(1)
169
171
 
170
172
 
171
- def _validate_error_prefix(parameter, attribute=None):
173
+ def _validate_error_prefix(parameter: Parameter, attribute: str | None = None) -> str:
172
174
  """
173
175
  Generate an error prefix suitable for Parameters when they raise a validation
174
176
  error.
@@ -279,11 +281,11 @@ def flatten(line):
279
281
 
280
282
 
281
283
  def accept_arguments(
282
- f: Callable[Concatenate[CallableT, P], R]
283
- ) -> Callable[P, Callable[[CallableT], R]]:
284
+ f: abc.Callable[t.Concatenate[_CallableT, _P], _R]
285
+ ) -> abc.Callable[..., abc.Callable[[_CallableT], _R]]:
284
286
  """Decorate a decorator to accept arguments."""
285
287
  @functools.wraps(f)
286
- def _f(*args: P.args, **kwargs: P.kwargs) -> Callable[[CallableT], R]:
288
+ def _f(*args: _P.args, **kwargs: _P.kwargs) -> abc.Callable[[_CallableT], _R]:
287
289
  return lambda actual_f: f(actual_f, *args, **kwargs)
288
290
  return _f
289
291
 
@@ -308,10 +310,10 @@ def _hashable(x):
308
310
  part of the object has changed. Does not (currently) recursively
309
311
  replace mutable subobjects.
310
312
  """
311
- if isinstance(x, collections.abc.MutableSequence):
313
+ if isinstance(x, abc.MutableSequence):
312
314
  return tuple(x)
313
- elif isinstance(x, collections.abc.MutableMapping):
314
- return tuple([(k,v) for k,v in x.items()])
315
+ elif isinstance(x, abc.MutableMapping):
316
+ return tuple([(k, v) for k, v in x.items()])
315
317
  else:
316
318
  return x
317
319
 
@@ -351,39 +353,52 @@ def _named_objs(objlist, namesdict=None):
351
353
 
352
354
  def _get_min_max_value(min, max, value=None, step=None):
353
355
  """Return min, max, value given input values with possible None."""
354
- # Either min and max need to be given, or value needs to be given
356
+ fmin = float(min) if min is not None else None
357
+ fmax = float(max) if max is not None else None
358
+
355
359
  if value is None:
356
- if min is None or max is None:
357
- raise ValueError(
358
- f'unable to infer range, value from: ({min}, {max}, {value})'
359
- )
360
- diff = max - min
361
- value = min + (diff / 2)
362
- # Ensure that value has the same type as diff
363
- if not isinstance(value, type(diff)):
364
- value = min + (diff // 2)
365
- else: # value is not None
366
- if not isinstance(value, Real):
367
- raise TypeError('expected a real number, got: %r' % value)
368
- # Infer min/max from value
369
- if value == 0:
370
- # This gives (0, 1) of the correct type
371
- vrange = (value, value + 1)
372
- elif value > 0:
373
- vrange = (-value, 3*value)
374
- else:
375
- vrange = (3*value, -value)
376
- if min is None:
377
- min = vrange[0]
378
- if max is None:
379
- max = vrange[1]
360
+ if fmin is None or fmax is None:
361
+ raise ValueError(f"unable to infer range, value from: ({min}, {max}, {value})")
362
+ fvalue = (fmin + fmax) / 2.0
363
+ else:
364
+ fvalue = float(value)
365
+ if fmin is None or fmax is None:
366
+ if fvalue == 0.0:
367
+ low, high = 0.0, 1.0
368
+ elif fvalue > 0.0:
369
+ low, high = -fvalue, 3.0 * fvalue
370
+ else:
371
+ low, high = 3.0 * fvalue, -fvalue
372
+ if fmin is None:
373
+ fmin = low
374
+ if fmax is None:
375
+ fmax = high
376
+
377
+ # Safety: ensure bounds exist
378
+ if fmin is None or fmax is None:
379
+ raise RuntimeError("internal error: bounds not resolved")
380
+
381
+ # Normalize so fmin <= fmax
382
+ if fmin > fmax:
383
+ fmin, fmax = fmax, fmin
384
+
385
+ # Snap to step if requested
380
386
  if step is not None:
381
- # ensure value is on a step
382
- tick = int((value - min) / step)
383
- value = min + tick * step
384
- if not min <= value <= max:
385
- raise ValueError(f'value must be between min and max (min={min}, value={value}, max={max})')
386
- return min, max, value
387
+ fstep = abs(float(step))
388
+ if fstep == 0.0:
389
+ raise ValueError("step must be non-zero")
390
+ ticks = round((fvalue - fmin) / fstep) # nearest tick; use math.floor for always-down
391
+ fvalue = fmin + ticks * fstep
392
+ # Clamp after snapping
393
+ if fvalue < fmin:
394
+ fvalue = fmin
395
+ if fvalue > fmax:
396
+ fvalue = fmax
397
+
398
+ if not (fmin <= fvalue <= fmax):
399
+ raise ValueError(f"value must be between min and max (min={fmin}, value={fvalue}, max={fmax})")
400
+
401
+ return fmin, fmax, fvalue
387
402
 
388
403
 
389
404
  def _deserialize_from_path(ext_to_routine, path, type_name):
@@ -425,10 +440,7 @@ def _is_number(obj):
425
440
  def _is_abstract(class_: type) -> bool:
426
441
  if inspect.isabstract(class_):
427
442
  return True
428
- try:
429
- return class_.abstract
430
- except AttributeError:
431
- return False
443
+ return bool(getattr(class_, "abstract", False))
432
444
 
433
445
 
434
446
  def descendents(class_: type, concrete: bool = False) -> list[type]:
@@ -470,7 +482,7 @@ def descendents(class_: type, concrete: bool = False) -> list[type]:
470
482
  if not isinstance(class_, type):
471
483
  raise TypeError(f"descendents expected a class object, not {type(class_).__name__}")
472
484
  q = [class_]
473
- out = []
485
+ out: list[type] = []
474
486
  while len(q):
475
487
  x = q.pop(0)
476
488
  out.insert(0, x)
@@ -580,12 +592,13 @@ def exceptions_summarized():
580
592
  except Exception:
581
593
  import sys
582
594
  etype, value, tb = sys.exc_info()
583
- print(f"{etype.__name__}: {value}", file=sys.stderr)
595
+ if etype is not None:
596
+ print(f"{etype.__name__}: {value}", file=sys.stderr)
584
597
 
585
598
 
586
599
  def _in_ipython():
587
600
  try:
588
- get_ipython
601
+ get_ipython() # type: ignore[name-defined,ty:unresolved-reference] # pyright: ignore[reportUndefinedVariable]
589
602
  return True
590
603
  except NameError:
591
604
  return False
@@ -605,21 +618,27 @@ def async_executor(func):
605
618
  else:
606
619
  event_loop.run_until_complete(func())
607
620
 
621
+ @t.runtime_checkable
622
+ class _HasTypes(t.Protocol):
623
+ @classmethod
624
+ def types(cls) -> abc.Iterable[type]: ...
625
+
608
626
  class _GeneratorIsMeta(type):
609
- def __instancecheck__(cls, inst):
627
+ def __instancecheck__(cls: type[_HasTypes], inst):
610
628
  return isinstance(inst, tuple(cls.types()))
611
629
 
612
- def __subclasscheck__(cls, sub):
630
+ def __subclasscheck__(cls: type[_HasTypes], sub: type) -> bool:
613
631
  return issubclass(sub, tuple(cls.types()))
614
632
 
615
- def __iter__(cls):
633
+ def __iter__(cls: type[_HasTypes]) -> abc.Iterator[type]:
616
634
  yield from cls.types()
617
635
 
618
636
  class _GeneratorIs(metaclass=_GeneratorIsMeta):
619
637
  @classmethod
620
- def __iter__(cls):
638
+ def __iter__(cls: type[_HasTypes]) -> abc.Iterator[type]:
621
639
  yield from cls.types()
622
640
 
641
+
623
642
  def gen_types(gen_func):
624
643
  """Decorate a generator function to support type checking.
625
644
 
@@ -656,8 +675,12 @@ def _find_stack_level() -> int:
656
675
  import numbergen
657
676
  import param
658
677
 
659
- ng_dir = os.path.dirname(numbergen.__file__)
660
- param_dir = os.path.dirname(param.__file__)
678
+ numbergen_file = getattr(numbergen, "__file__", None)
679
+ param_file = getattr(param, "__file__", None)
680
+ if numbergen_file is None or param_file is None:
681
+ return 2
682
+ ng_dir = os.path.dirname(numbergen_file)
683
+ param_dir = os.path.dirname(param_file)
661
684
 
662
685
  # https://stackoverflow.com/questions/17407119/python-inspect-stack-is-slow
663
686
  frame = inspect.currentframe()
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.3.3b0'
22
- __version_tuple__ = version_tuple = (2, 3, 3, 'b0')
21
+ __version__ = version = '2.4.0a0'
22
+ __version_tuple__ = version_tuple = (2, 4, 0, 'a0')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import typing as t
5
+
6
+ from collections import defaultdict
7
+ from functools import wraps
8
+
9
+ from .parameterized import (
10
+ Event, Parameter, Parameterized, ParameterizedMetaclass, transform_reference,
11
+ )
12
+ from ._utils import iscoroutinefunction
13
+
14
+ if t.TYPE_CHECKING:
15
+ from collections.abc import AsyncGenerator, Callable, Generator
16
+
17
+ _Y = t.TypeVar("_Y")
18
+ _S = t.TypeVar("_S")
19
+ _T = t.TypeVar("_T")
20
+
21
+ _P = t.ParamSpec("_P")
22
+ _R = t.TypeVar("_R", covariant=True)
23
+ Dependency = Parameter | str
24
+
25
+ class DependencyInfo(t.TypedDict):
26
+ dependencies: tuple[Dependency, ...]
27
+ kw: dict[str, Dependency]
28
+ watch: bool
29
+ on_init: bool
30
+
31
+ class DependsFunc(t.Protocol[_P, _R]):
32
+ _dinfo: DependencyInfo
33
+ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
34
+
35
+
36
+ @t.overload
37
+ def depends(
38
+ func: Callable[_P, _R], /, *dependencies: Dependency, watch: bool = False, on_init: bool = False, **kw: Dependency
39
+ ) -> DependsFunc[_P, _R]:
40
+ ...
41
+
42
+ @t.overload
43
+ def depends(
44
+ *dependencies: str, watch: bool = False, on_init: bool = False
45
+ ) -> Callable[[Callable[_P, _R]], DependsFunc[_P, _R]]:
46
+ ...
47
+
48
+ @t.overload
49
+ def depends(
50
+ *dependencies: Parameter, watch: bool = False, on_init: bool = False, **kw: Parameter
51
+ ) -> Callable[[Callable[_P, _R]], DependsFunc[_P, _R]]:
52
+ ...
53
+
54
+ def depends(
55
+ *dependencies: Dependency | Callable[_P, _R], watch: bool = False, on_init: bool = False, **kw: Dependency
56
+ ) -> DependsFunc[_P, _R] | Callable[[Callable[_P, _R]], DependsFunc[_P, _R]]:
57
+ """
58
+ Annotates a function or :class:`Parameterized` method to express its dependencies.
59
+
60
+ The specified dependencies can be either be :class:`Parameter` instances or if a
61
+ method is supplied they can be defined as strings referring to Parameters
62
+ of the class, or Parameters of subobjects (Parameterized objects that are
63
+ values of this object's parameters). Dependencies can either be on
64
+ Parameter values, or on other metadata about the Parameter.
65
+
66
+ Parameters
67
+ ----------
68
+ watch : bool, optional
69
+ Whether to invoke the function/method when the dependency is updated,
70
+ by default ``False``.
71
+ on_init : bool, optional
72
+ Whether to invoke the function/method when the instance is created,
73
+ by default ``False``.
74
+
75
+ """
76
+ if dependencies and callable(dependencies[0]) and not isinstance(dependencies[0], (str, Parameter)):
77
+ func = t.cast("Callable[_P, _R]", dependencies[0])
78
+ deps = t.cast("tuple[Dependency, ...]", dependencies[1:])
79
+ return _depends_impl(func, *deps, watch=watch, on_init=on_init, **kw)
80
+
81
+ deps = t.cast("tuple[Dependency, ...]", dependencies)
82
+
83
+ def _decorator(func: Callable[_P, _R]) -> DependsFunc[_P, _R]:
84
+ return _depends_impl(func, *deps, watch=watch, on_init=on_init, **kw)
85
+
86
+ return _decorator
87
+
88
+
89
+ def _depends_impl(
90
+ func: Callable[_P, _R], /, *dependencies: Dependency, watch: bool = False, on_init: bool = False, **kw: Dependency
91
+ ) -> DependsFunc[_P, _R]:
92
+ dependencies, kw = (
93
+ tuple(transform_reference(arg) for arg in dependencies),
94
+ {key: transform_reference(arg) for key, arg in kw.items()}
95
+ )
96
+
97
+ if inspect.isgeneratorfunction(func):
98
+ func_gen = t.cast("Callable[_P, Generator[t.Any, t.Any, t.Any]]", func)
99
+ @wraps(func)
100
+ def _depends_gen(*args, **kw):
101
+ for val in func_gen(*args, **kw):
102
+ yield val
103
+ _depends = t.cast("Callable[_P, _R]", _depends_gen)
104
+ elif inspect.isasyncgenfunction(func):
105
+ func_agen = t.cast("Callable[_P, AsyncGenerator[t.Any, t.Any]]", func)
106
+ @wraps(func)
107
+ async def _depends_async_gen(*args, **kw):
108
+ async for val in func_agen(*args, **kw):
109
+ yield val
110
+ _depends = t.cast("Callable[_P, _R]", _depends_async_gen)
111
+ elif iscoroutinefunction(func):
112
+ F = t.cast("Callable[_P, t.Awaitable[_R]]", func)
113
+ @wraps(func)
114
+ async def _depends_coro(*args, **kw):
115
+ return await F(*args, **kw)
116
+ _depends = t.cast("Callable[_P, _R]", _depends_coro)
117
+ else:
118
+ @wraps(func)
119
+ def _depends_sync(*args, **kw):
120
+ return func(*args, **kw)
121
+ _depends = t.cast("Callable[_P, _R]", _depends_sync)
122
+
123
+ deps = list(dependencies)+list(kw.values())
124
+ string_specs = False
125
+ for dep in deps:
126
+ if isinstance(dep, str):
127
+ string_specs = True
128
+ elif hasattr(dep, '_dinfo'):
129
+ pass
130
+ elif not isinstance(dep, Parameter):
131
+ raise ValueError('The depends decorator only accepts string '
132
+ 'types referencing a parameter or parameter '
133
+ 'instances, found %s type instead.' %
134
+ type(dep).__name__)
135
+ elif not (isinstance(dep.owner, Parameterized) or
136
+ (isinstance(dep.owner, ParameterizedMetaclass))):
137
+ owner = 'None' if dep.owner is None else '%s class' % type(dep.owner).__name__
138
+ raise ValueError('Parameters supplied to the depends decorator, '
139
+ 'must be bound to a Parameterized class or '
140
+ 'instance, not %s.' % owner)
141
+
142
+ if (any(isinstance(dep, Parameter) for dep in deps) and
143
+ any(isinstance(dep, str) for dep in deps)):
144
+ raise ValueError('Dependencies must either be defined as strings '
145
+ 'referencing parameters on the class defining '
146
+ 'the decorated method or as parameter instances. '
147
+ 'Mixing of string specs and parameter instances '
148
+ 'is not supported.')
149
+ elif string_specs and kw:
150
+ raise AssertionError('Supplying keywords to the decorated method '
151
+ 'or function is not supported when referencing '
152
+ 'parameters by name.')
153
+
154
+ _dinfo = t.cast("DependencyInfo", dict(getattr(func, '_dinfo', {})))
155
+ _dinfo.update({'dependencies': dependencies,
156
+ 'kw': kw, 'watch': watch, 'on_init': on_init})
157
+
158
+ typed_depends = t.cast("DependsFunc[_P, _R]", _depends)
159
+ typed_depends._dinfo = _dinfo
160
+
161
+ if string_specs or not watch:
162
+ # string_specs case handled elsewhere (later), in Parameterized.__init__
163
+ return typed_depends
164
+ param_args = [dep for dep in dependencies if isinstance(dep, Parameter)]
165
+ param_kwargs = {n: dep for n, dep in kw.items() if isinstance(dep, Parameter)}
166
+ param_deps = list(param_args) + list(param_kwargs.values())
167
+
168
+ def _dep_owner_name(dep: Parameter) -> tuple[Parameterized, str] | None:
169
+ owner = dep.owner
170
+ name = dep.name
171
+ if owner is None or name is None:
172
+ return None
173
+ return owner, name
174
+
175
+ def _resolve_args() -> tuple[t.Any, ...]:
176
+ args: list[t.Any] = []
177
+ for dep in param_args:
178
+ owner_name = _dep_owner_name(dep)
179
+ if owner_name is None:
180
+ continue
181
+ owner, name = owner_name
182
+ args.append(getattr(owner, name))
183
+ return tuple(args)
184
+
185
+ def _resolve_kwargs() -> dict[str, t.Any]:
186
+ dep_kwargs: dict[str, t.Any] = {}
187
+ for key, dep in param_kwargs.items():
188
+ owner_name = _dep_owner_name(dep)
189
+ if owner_name is None:
190
+ continue
191
+ owner, name = owner_name
192
+ dep_kwargs[key] = getattr(owner, name)
193
+ return dep_kwargs
194
+
195
+ if inspect.isgeneratorfunction(func):
196
+ def cb_gen(*events: Event):
197
+ args = _resolve_args()
198
+ dep_kwargs = _resolve_kwargs()
199
+ func_gen = t.cast("Callable[_P, Generator[t.Any, t.Any, t.Any]]", func)
200
+ for val in func_gen(*args, **dep_kwargs):
201
+ yield val
202
+ cb = cb_gen
203
+ elif inspect.isasyncgenfunction(func):
204
+ async def cb_async_gen(*events: Event):
205
+ args = _resolve_args()
206
+ dep_kwargs = _resolve_kwargs()
207
+ func_agen = t.cast("Callable[_P, AsyncGenerator[t.Any, t.Any]]", func)
208
+ async for val in func_agen(*args, **dep_kwargs):
209
+ yield val
210
+ cb = cb_async_gen
211
+ elif iscoroutinefunction(func):
212
+ async def cb_coro(*events: Event):
213
+ args = _resolve_args()
214
+ dep_kwargs = _resolve_kwargs()
215
+ func_coro = t.cast("Callable[_P, t.Awaitable[t.Any]]", func)
216
+ await func_coro(*args, **dep_kwargs)
217
+ cb = cb_coro
218
+ else:
219
+ def cb_sync(*events: Event):
220
+ args = _resolve_args()
221
+ dep_kwargs = _resolve_kwargs()
222
+ return func(*args, **dep_kwargs)
223
+ cb = cb_sync
224
+
225
+ grouped = defaultdict(list)
226
+ for dep in param_deps:
227
+ grouped[id(dep.owner)].append(dep)
228
+ for group in grouped.values():
229
+ if group[0].owner is None:
230
+ continue
231
+ group[0].owner.param.watch(cb, [dep.name for dep in group])
232
+
233
+ return typed_depends
@@ -1,7 +1,7 @@
1
1
  import weakref
2
2
 
3
- _display_accessors = {}
4
- _reactive_display_objs = weakref.WeakSet()
3
+ _display_accessors: dict[str, object] = {}
4
+ _reactive_display_objs: weakref.WeakSet[object] = weakref.WeakSet()
5
5
 
6
6
  def register_display_accessor(name, accessor, force=False):
7
7
  if name in _display_accessors and not force: