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