pulse-framework 0.1.62__py3-none-any.whl

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 (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1172 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import weakref
5
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
6
+ from dataclasses import MISSING as _DC_MISSING
7
+ from dataclasses import FrozenInstanceError as _DC_FrozenInstanceError
8
+ from dataclasses import InitVar as _DC_InitVar
9
+ from dataclasses import dataclass as _dc_dataclass
10
+ from dataclasses import fields as _dc_fields
11
+ from dataclasses import is_dataclass
12
+ from typing import Any as _Any
13
+ from typing import (
14
+ Generic,
15
+ Protocol,
16
+ SupportsIndex,
17
+ TypeAlias,
18
+ TypeVar,
19
+ cast,
20
+ overload,
21
+ override,
22
+ )
23
+
24
+ from pulse.reactive import Computed, Signal, Untrack
25
+
26
+ T1 = TypeVar("T1")
27
+ T1_co = TypeVar("T1_co", covariant=True)
28
+ T2 = TypeVar("T2")
29
+ T2_co = TypeVar("T2_co", covariant=True)
30
+ T3 = TypeVar("T3")
31
+
32
+ T = TypeVar("T")
33
+ S = TypeVar("S")
34
+
35
+ KT = TypeVar
36
+
37
+
38
+ _MISSING = object()
39
+
40
+
41
+ class SupportsKeysAndGetItem(Protocol[T1, T2_co]):
42
+ def keys(self) -> Iterable[T1]: ...
43
+ def __getitem__(self, key: T1, /) -> T2_co: ...
44
+
45
+
46
+ # Return an iterable view that subscribes to per-key signals during iteration
47
+ class ReactiveDictItems(Generic[T1, T2]):
48
+ __slots__ = ("_host",) # pyright: ignore[reportUnannotatedClassAttribute]
49
+ _host: ReactiveDict[T1, T2]
50
+
51
+ def __init__(self, host: ReactiveDict[T1, T2]) -> None:
52
+ self._host = host
53
+
54
+ def __iter__(self):
55
+ for k in self._host:
56
+ yield (k, self._host[k])
57
+
58
+ def __len__(self) -> int:
59
+ return len(self._host)
60
+
61
+
62
+ class ReactiveDictValues(Generic[T1, T2]):
63
+ __slots__ = ("_host",) # pyright: ignore[reportUnannotatedClassAttribute]
64
+ _host: ReactiveDict[T1, T2]
65
+
66
+ def __init__(self, host: ReactiveDict[T1, T2]) -> None:
67
+ self._host = host
68
+
69
+ def __iter__(self):
70
+ for k in self._host:
71
+ yield self._host[k]
72
+
73
+ def __len__(self) -> int:
74
+ return len(self._host)
75
+
76
+
77
+ class ReactiveDict(dict[T1, T2]):
78
+ """A dict-like container with per-key reactivity.
79
+
80
+ Reading a key registers a dependency on that key's Signal. Writing a key
81
+ updates only that key's Signal. Iteration, membership checks, and len are
82
+ reactive to structural changes.
83
+
84
+ Args:
85
+ initial: Initial key-value pairs to populate the dict.
86
+
87
+ Example:
88
+
89
+ ```python
90
+ data = ReactiveDict({"name": "Alice", "age": 30})
91
+ print(data["name"]) # "Alice" (registers dependency)
92
+ data["age"] = 31 # Updates age signal only
93
+ data.unwrap() # {"name": "Alice", "age": 31}
94
+ ```
95
+ """
96
+
97
+ __slots__ = ("_signals", "_structure") # pyright: ignore[reportUnannotatedClassAttribute]
98
+
99
+ def __init__(self, initial: Mapping[T1, T2] | None = None) -> None:
100
+ super().__init__()
101
+ self._signals: dict[T1, Signal[_Any]] = {}
102
+ self._structure: Signal[int] = Signal(0)
103
+ if initial:
104
+ for k, v in initial.items():
105
+ v = reactive(v)
106
+ super().__setitem__(k, v)
107
+ self._signals[k] = Signal(v)
108
+
109
+ # ---- helpers ----
110
+ def _bump_structure(self) -> None:
111
+ self._structure.write(self._structure.read() + 1)
112
+
113
+ # --- Mapping protocol ---
114
+ @override
115
+ def __getitem__(self, key: T1) -> T2:
116
+ if key not in self._signals:
117
+ # Lazily create missing key with sentinel so it can be reactive
118
+ self._signals[key] = Signal(_MISSING)
119
+ val = self._signals[key].read()
120
+ # Preserve dict.__getitem__ typing by casting. Semantics: return None
121
+ # only if the stored value is explicitly None; otherwise unwrap sentinel.
122
+ return cast(T2, None) if val is _MISSING else cast(T2, val)
123
+
124
+ @override
125
+ def __setitem__(self, key: T1, value: T2) -> None:
126
+ self.set(key, value)
127
+
128
+ @override
129
+ def __delitem__(self, key: T1) -> None:
130
+ # Remove from mapping but preserve signal object for subscribers
131
+ if key not in self._signals:
132
+ self._signals[key] = Signal(_MISSING)
133
+ else:
134
+ self._signals[key].write(_MISSING)
135
+ if super().__contains__(key):
136
+ super().__delitem__(key)
137
+ self._bump_structure()
138
+
139
+ @overload
140
+ def get(self, key: T1, default: None = None, /) -> T2 | None: ...
141
+ @overload
142
+ def get(self, key: T1, default: T2, /) -> T2: ...
143
+ @overload
144
+ def get(self, key: T1, default: T3) -> T2 | T3: ...
145
+ @override
146
+ def get(self, key: T1, default: T3 | None = None) -> T2 | T3 | None:
147
+ # Ensure a per-key signal exists so get() can subscribe even when absent
148
+ sig = self._signals.get(key)
149
+ if sig is None:
150
+ sig = cast(Signal[T2], Signal(_MISSING))
151
+ self._signals[key] = sig
152
+ val = sig.read()
153
+ return default if val is _MISSING else val
154
+
155
+ @override
156
+ def __iter__(self) -> Iterator[T1]:
157
+ # Reactive to structural changes
158
+ self._structure.read()
159
+ return super().__iter__()
160
+
161
+ @override
162
+ def __len__(self) -> int:
163
+ self._structure.read()
164
+ return super().__len__()
165
+
166
+ # The base __contains__ annotates key as type `object`, which is not strict enough
167
+ @override
168
+ def __contains__(self, key: T1) -> bool: # pyright: ignore[reportIncompatibleMethodOverride]
169
+ # Subscribe to the per-key value signal so presence checks are reactive
170
+ sig = self._signals.get(key)
171
+ if sig is None:
172
+ sig = Signal(_MISSING)
173
+ self._signals[key] = sig
174
+ sig.read()
175
+ return dict.__contains__(self, key)
176
+
177
+ # --- Mutation helpers ---
178
+ def set(self, key: T1, value: T2) -> None:
179
+ value = reactive(value)
180
+ was_present = super().__contains__(key)
181
+ sig = self._signals.get(key)
182
+ if sig is None:
183
+ self._signals[key] = Signal(value)
184
+ else:
185
+ sig.write(value)
186
+ super().__setitem__(key, value)
187
+ if not was_present:
188
+ self._bump_structure()
189
+
190
+ @overload
191
+ def update(self, m: SupportsKeysAndGetItem[T1, T2], /) -> None: ...
192
+ @overload
193
+ def update(
194
+ self: "ReactiveDict[str, T2]",
195
+ m: SupportsKeysAndGetItem[str, T2],
196
+ /,
197
+ **kwargs: T2,
198
+ ) -> None: ...
199
+ @overload
200
+ def update(self, m: Iterable[tuple[T1, T2]], /) -> None: ...
201
+ @overload
202
+ def update(
203
+ self: "ReactiveDict[str, T2]", m: Iterable[tuple[str, T2]], /, **kwargs: T2
204
+ ) -> None: ...
205
+ @overload
206
+ def update(self: "ReactiveDict[str, T2]", **kwargs: T2) -> None: ...
207
+ @override
208
+ # Pyright also doesn't want to accept this overloaded override, but it's
209
+ # exactly correct.
210
+ def update( # pyright: ignore[reportIncompatibleMethodOverride]
211
+ self,
212
+ other: _Any = None,
213
+ **kwargs: T2,
214
+ ) -> None:
215
+ # Match dict.update semantics
216
+ if other is not None:
217
+ if isinstance(other, Mapping) or hasattr(other, "keys"):
218
+ # Mapping-like: iterate keys and fetch via __getitem__
219
+ keys_iter = other.keys()
220
+ for k in keys_iter:
221
+ self.set(cast(T1, k), cast(T2, other[k]))
222
+ else:
223
+ # Iterable of key/value pairs
224
+ for k, v in other:
225
+ self.set(k, v)
226
+ if kwargs:
227
+ for k, v in kwargs.items():
228
+ self.set(cast(T1, k), v)
229
+
230
+ def delete(self, key: T1) -> None:
231
+ if key in self._signals:
232
+ # Preserve signal and mark as not present; do not raise
233
+ self._signals[key].write(_MISSING)
234
+ if super().__contains__(key):
235
+ super().__delitem__(key)
236
+ self._bump_structure()
237
+
238
+ # ---- standard dict methods ----
239
+ # I have no idea why Pyright is not happy with this override, but *shrug*
240
+ @override
241
+ def keys(self):
242
+ self._structure.read()
243
+ return super().keys()
244
+
245
+ # This overload is incompatible because we return a different wrapper
246
+ @override
247
+ def items(self): # pyright: ignore[reportIncompatibleMethodOverride]
248
+ return ReactiveDictItems(self)
249
+
250
+ # This overload is incompatible because we return a different wrapper
251
+ @override
252
+ def values(self): # pyright: ignore[reportIncompatibleMethodOverride]
253
+ return ReactiveDictValues(self)
254
+
255
+ @overload
256
+ def pop(self, key: T1, /) -> T2: ...
257
+
258
+ @overload
259
+ def pop(self, key: T1, default: T2, /) -> T2: ...
260
+ @overload
261
+ def pop(self, key: T1, default: T3, /) -> T2 | T3: ...
262
+
263
+ @override
264
+ def pop(self, key: T1, default: T3 = _MISSING) -> T2 | T3:
265
+ if super().__contains__(key):
266
+ val = cast(T2, dict.__getitem__(self, key))
267
+ self.__delitem__(key)
268
+ return val
269
+ if default is _MISSING:
270
+ raise KeyError(key)
271
+ return default
272
+
273
+ @override
274
+ def popitem(self) -> tuple[T1, T2]:
275
+ if not super().__len__():
276
+ raise KeyError("popitem(): dictionary is empty")
277
+ k, v = super().popitem()
278
+ # Preserve and update reactive metadata
279
+ sig = self._signals.get(k)
280
+ if sig is None:
281
+ self._signals[k] = Signal(_MISSING)
282
+ else:
283
+ sig.write(_MISSING)
284
+ self._bump_structure()
285
+ return k, v
286
+
287
+ @overload
288
+ def setdefault(self, key: T1, default: None = None, /) -> T2 | None: ...
289
+ @overload
290
+ def setdefault(self, key: T1, default: T2, /) -> T2: ...
291
+
292
+ @override
293
+ def setdefault(self, key: T1, default: T2 | None = None) -> T2 | None:
294
+ if super().__contains__(key):
295
+ # Return current value without structural change
296
+ if key not in self._signals:
297
+ self._signals[key] = Signal(_MISSING)
298
+ return self._signals[key].read()
299
+ # Insert default
300
+ self.set(key, default) # pyright: ignore[reportArgumentType]
301
+ # Read structure after write to suppress immediate rerun of the current
302
+ # effect (if this is used in an effect) caused by the structural bump
303
+ # performed in set().
304
+ self._structure.read()
305
+ sig = self._signals.get(key)
306
+ if sig is None:
307
+ sig = cast(Signal[T2], Signal(_MISSING))
308
+ self._signals[key] = sig
309
+ return sig.read()
310
+
311
+ @override
312
+ def clear(self) -> None:
313
+ if not super().__len__():
314
+ return
315
+ for k in list(super().keys()):
316
+ # Use our deletion to keep signals/presence updated
317
+ self.__delitem__(k)
318
+ # bump already done per key; nothing else needed
319
+
320
+ @override
321
+ def copy(self):
322
+ # Shallow copy preserving current values
323
+ return ReactiveDict(self)
324
+
325
+ def __copy__(self):
326
+ return self.copy()
327
+
328
+ def __deepcopy__(self, memo: dict[int, _Any]):
329
+ if id(self) in memo:
330
+ return memo[id(self)]
331
+ result = type(self)()
332
+ memo[id(self)] = result
333
+ for key in dict.__iter__(self):
334
+ key = cast(T1, key)
335
+ key_copy = copy.deepcopy(key, memo)
336
+ value_copy = copy.deepcopy(cast(T2, dict.__getitem__(self, key)), memo)
337
+ result.set(key_copy, value_copy)
338
+ return result
339
+
340
+ @overload
341
+ @classmethod
342
+ def fromkeys(
343
+ cls, iterable: Iterable[S], value: None = None, /
344
+ ) -> ReactiveDict[S, _Any | None]: ...
345
+ @overload
346
+ @classmethod
347
+ def fromkeys(cls, iterable: Iterable[S], value: T, /) -> ReactiveDict[S, T]: ...
348
+
349
+ @override
350
+ @classmethod
351
+ def fromkeys(
352
+ cls, iterable: Iterable[S], value: T | None = None, /
353
+ ) -> ReactiveDict[S, _Any | None] | ReactiveDict[S, T]:
354
+ rd: ReactiveDict[S, T | None] = cls() # pyright: ignore[reportAssignmentType]
355
+ for k in iterable:
356
+ rd.set(k, value)
357
+ return rd
358
+
359
+ # PEP 584 dict union operators
360
+ @override
361
+ def __ior__(self, other: Mapping[T1, T2]) -> ReactiveDict[T1, T2]: # pyright: ignore[reportIncompatibleMethodOverride]
362
+ self.update(other)
363
+ return self
364
+
365
+ @override
366
+ def __or__(self, other: Mapping[T1, T2]) -> ReactiveDict[T1, T2]: # pyright: ignore[reportIncompatibleMethodOverride]
367
+ result = ReactiveDict(self)
368
+ result.update(other)
369
+ return result
370
+
371
+ @override
372
+ def __ror__(self, other: Mapping[T1, T2]) -> ReactiveDict[T1, T2]: # pyright: ignore[reportIncompatibleMethodOverride]
373
+ result = ReactiveDict(other)
374
+ result.update(self)
375
+ return result
376
+
377
+ def unwrap(self) -> dict[T1, _Any]:
378
+ """Return a plain dict while subscribing to contained signals.
379
+
380
+ Returns:
381
+ A plain dict with all reactive containers recursively unwrapped.
382
+ """
383
+ self._structure.read()
384
+ result: dict[T1, _Any] = {}
385
+ for key in dict.__iter__(self):
386
+ result[key] = unwrap(self[key])
387
+ return result
388
+
389
+
390
+ # Copied from the built-in types
391
+ # =====
392
+ _T_contra = TypeVar("_T_contra", contravariant=True)
393
+
394
+
395
+ class SupportsBool(Protocol):
396
+ def __bool__(self) -> bool: ...
397
+
398
+
399
+ class SupportsDunderLT(Protocol[_T_contra]):
400
+ def __lt__(self, other: _T_contra, /) -> SupportsBool: ...
401
+
402
+
403
+ class SupportsDunderGT(Protocol[_T_contra]):
404
+ def __gt__(self, other: _T_contra, /) -> SupportsBool: ...
405
+
406
+
407
+ SupportsRichComparison: TypeAlias = SupportsDunderLT[_Any] | SupportsDunderGT[_Any]
408
+ SupportsRichComparisonT = TypeVar(
409
+ "SupportsRichComparisonT", bound=SupportsRichComparison
410
+ )
411
+ # ====
412
+
413
+
414
+ class ReactiveList(list[T1]):
415
+ """A list with item-level reactivity and structural change signaling.
416
+
417
+ Index reads depend on that index's Signal. Setting an index writes to that
418
+ index's Signal. Structural operations (append/insert/pop/etc.) trigger a
419
+ structural version Signal. Iteration subscribes to all item signals and
420
+ structural changes. len() subscribes to structural changes.
421
+
422
+ Args:
423
+ initial: Initial items to populate the list.
424
+
425
+ Example:
426
+
427
+ ```python
428
+ items = ReactiveList([1, 2, 3])
429
+ print(items[0]) # 1 (registers dependency on index 0)
430
+ items.append(4) # Triggers structural change
431
+ items.unwrap() # [1, 2, 3, 4]
432
+ ```
433
+ """
434
+
435
+ __slots__ = ("_signals", "_structure") # pyright: ignore[reportUnannotatedClassAttribute]
436
+
437
+ def __init__(self, initial: Iterable[T1] | None = None) -> None:
438
+ super().__init__()
439
+ self._signals: list[Signal[T1]] = []
440
+ self._structure: Signal[int] = Signal(0)
441
+ if initial:
442
+ for item in initial:
443
+ v = reactive(item)
444
+ self._signals.append(Signal(v))
445
+ super().append(v)
446
+
447
+ # ---- helpers ----
448
+ def _bump_structure(self):
449
+ self._structure.write(self._structure.read() + 1)
450
+
451
+ @property
452
+ def version(self) -> int:
453
+ """Reactive counter that increments on any structural change."""
454
+ return self._structure.read()
455
+
456
+ @overload
457
+ def __getitem__(self, i: SupportsIndex, /) -> T1:
458
+ """Return self[index]."""
459
+ ...
460
+
461
+ @overload
462
+ def __getitem__(self, s: slice, /) -> list[T1]:
463
+ """Return self[index]."""
464
+ ...
465
+
466
+ @override
467
+ def __getitem__(self, idx: SupportsIndex | slice):
468
+ if isinstance(idx, slice):
469
+ # Return a plain list of values (non-reactive slice)
470
+ start, stop, step = idx.indices(len(self))
471
+ return [self._signals[i].read() for i in range(start, stop, step)]
472
+ return self._signals[idx].read()
473
+
474
+ @overload
475
+ def __setitem__(self, key: SupportsIndex, value: T1, /) -> None:
476
+ """Set self[key] to value."""
477
+ ...
478
+
479
+ @overload
480
+ def __setitem__(self, key: slice, value: Iterable[T1], /) -> None:
481
+ """Set self[key] to value."""
482
+ ...
483
+
484
+ @override
485
+ def __setitem__(self, key: SupportsIndex | slice, value: T1 | Iterable[T1]):
486
+ if isinstance(key, slice):
487
+ value = cast(Iterable[T1], value)
488
+ replacement_seq = list(value)
489
+ start, stop, step = key.indices(len(self))
490
+ target_indices = list(range(start, stop, step))
491
+
492
+ if len(replacement_seq) == len(target_indices):
493
+ wrapped = [reactive(v) for v in replacement_seq]
494
+ super().__setitem__(key, wrapped)
495
+ for i, v in zip(target_indices, wrapped, strict=True):
496
+ self._signals[i].write(v)
497
+ return
498
+
499
+ super().__setitem__(key, replacement_seq)
500
+ self._signals = [Signal(reactive(v)) for v in super().__iter__()]
501
+ self._bump_structure()
502
+ return
503
+ # normal index
504
+ value = cast(T1, value)
505
+ v = reactive(value)
506
+ super().__setitem__(key, v)
507
+ self._signals[key].write(v)
508
+
509
+ @override
510
+ def __delitem__(self, idx: SupportsIndex | slice):
511
+ if isinstance(idx, slice):
512
+ super().__delitem__(idx)
513
+ self._signals = [Signal(v) for v in super().__iter__()]
514
+ self._bump_structure()
515
+ return
516
+ super().__delitem__(idx)
517
+ del self._signals[idx]
518
+ self._bump_structure()
519
+
520
+ # ---- structural operations ----
521
+ @override
522
+ def append(self, value: T1) -> None:
523
+ v = reactive(value)
524
+ super().append(v)
525
+ self._signals.append(Signal(v))
526
+ self._bump_structure()
527
+
528
+ @override
529
+ def extend(self, values: Iterable[T1]) -> None:
530
+ any_added = False
531
+ for v in values:
532
+ vv = reactive(v)
533
+ super().append(vv)
534
+ self._signals.append(Signal(vv))
535
+ any_added = True
536
+ if any_added:
537
+ self._bump_structure()
538
+
539
+ @override
540
+ def insert(self, index: SupportsIndex, value: T1) -> None:
541
+ v = reactive(value)
542
+ super().insert(index, v)
543
+ self._signals.insert(index, Signal(v))
544
+ self._bump_structure()
545
+
546
+ @override
547
+ def pop(self, index: SupportsIndex = -1):
548
+ val = super().pop(index)
549
+ del self._signals[index]
550
+ self._bump_structure()
551
+ return val
552
+
553
+ def unwrap(self) -> list[_Any]:
554
+ """Return a plain list while subscribing to element signals.
555
+
556
+ Returns:
557
+ A plain list with all reactive containers recursively unwrapped.
558
+ """
559
+ self._structure()
560
+ return [unwrap(self[i]) for i in range(len(self._signals))]
561
+
562
+ @override
563
+ def remove(self, value: _Any) -> None:
564
+ idx = super().index(value)
565
+ self.pop(idx)
566
+
567
+ @override
568
+ def clear(self) -> None:
569
+ super().clear()
570
+ self._signals.clear()
571
+ self._bump_structure()
572
+
573
+ @override
574
+ def reverse(self) -> None:
575
+ super().reverse()
576
+ self._signals.reverse()
577
+ self._bump_structure()
578
+
579
+ @overload
580
+ def sort(
581
+ self: ReactiveList[SupportsRichComparisonT],
582
+ *,
583
+ key: None = None,
584
+ reverse: bool = False,
585
+ ) -> None:
586
+ """
587
+ Sort the list in ascending order and return None.
588
+
589
+ The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
590
+ order of two equal elements is maintained).
591
+
592
+ If a key function is given, apply it once to each list item and sort them,
593
+ ascending or descending, according to their function values.
594
+
595
+ The reverse flag can be set to sort in descending order.
596
+ """
597
+ ...
598
+
599
+ @overload
600
+ def sort(
601
+ self, *, key: Callable[[T1], SupportsRichComparison], reverse: bool = False
602
+ ) -> None:
603
+ """
604
+ Sort the list in ascending order and return None.
605
+
606
+ The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
607
+ order of two equal elements is maintained).
608
+
609
+ If a key function is given, apply it once to each list item and sort them,
610
+ ascending or descending, according to their function values.
611
+
612
+ The reverse flag can be set to sort in descending order.
613
+ """
614
+ ...
615
+
616
+ @override
617
+ def sort(
618
+ self,
619
+ key: Callable[[T1], SupportsRichComparison] | None = None,
620
+ reverse: bool = False,
621
+ ) -> None:
622
+ # To preserve per-index subscriptions, we have to reorder signals to match
623
+ # new order. We'll compute the permutation by sorting indices based on
624
+ # current values.
625
+ current = list(super().__iter__())
626
+ idxs = list(range(len(current)))
627
+
628
+ # Create a key that uses the same key as provided to sort, but applied to value.
629
+ def key_for_index(i: int):
630
+ v = current[i]
631
+ return key(v) if callable(key) else v
632
+
633
+ # Apply sort to underlying list
634
+ idxs.sort(key=key_for_index, reverse=reverse) # pyright: ignore[reportCallIssue, reportArgumentType]
635
+ # Reorder signals to match
636
+ self._signals = [self._signals[i] for i in idxs]
637
+ self._bump_structure()
638
+
639
+ # Make len() and iteration reactive to structural changes
640
+ @override
641
+ def __len__(self) -> int:
642
+ self._structure.read()
643
+ return super().__len__()
644
+
645
+ @override
646
+ def __iter__(self) -> Iterator[T1]:
647
+ self._structure.read()
648
+ for sig in self._signals:
649
+ yield sig.read()
650
+
651
+ def __copy__(self):
652
+ result = type(self)()
653
+ for value in super().__iter__():
654
+ result.append(copy.copy(value))
655
+ return result
656
+
657
+ def __deepcopy__(self, memo: dict[int, _Any]):
658
+ if id(self) in memo:
659
+ return memo[id(self)]
660
+ result = type(self)()
661
+ memo[id(self)] = result
662
+ for value in super().__iter__():
663
+ result.append(copy.deepcopy(value, memo))
664
+ return result
665
+
666
+
667
+ class ReactiveSet(set[T1]):
668
+ """A set with per-element membership reactivity.
669
+
670
+ `x in s` reads a membership Signal for element `x`. Mutations update
671
+ membership Signals for affected elements. Iteration subscribes to
672
+ membership signals for all elements.
673
+
674
+ Args:
675
+ initial: Initial elements to populate the set.
676
+
677
+ Example:
678
+
679
+ ```python
680
+ tags = ReactiveSet({"python", "react"})
681
+ print("python" in tags) # True (registers dependency)
682
+ tags.add("typescript") # Updates membership signal
683
+ tags.unwrap() # {"python", "react", "typescript"}
684
+ ```
685
+ """
686
+
687
+ __slots__ = ("_signals",) # pyright: ignore[reportUnannotatedClassAttribute]
688
+
689
+ def __init__(self, initial: Iterable[T1] | None = None) -> None:
690
+ super().__init__()
691
+ self._signals: dict[T1, Signal[bool]] = {}
692
+ if initial:
693
+ for v in initial:
694
+ vv = reactive(v)
695
+ super().add(vv)
696
+ self._signals[vv] = Signal(True)
697
+
698
+ # same as dict, set.__contains__ defines the argument as `object`, which is not correct
699
+ @override
700
+ def __contains__(self, element: T1) -> bool: # pyright: ignore[reportIncompatibleMethodOverride]
701
+ sig = self._signals.get(element)
702
+ if sig is None:
703
+ present = set.__contains__(self, element)
704
+ self._signals[element] = Signal(bool(present))
705
+ sig = self._signals[element]
706
+ return bool(sig.read())
707
+
708
+ @override
709
+ def __iter__(self) -> Iterator[T1]:
710
+ # Subscribe to membership signals and return present elements
711
+ present = [elem for elem, sig in self._signals.items() if sig.read()]
712
+ return iter(present)
713
+
714
+ @override
715
+ def add(self, element: T1) -> None:
716
+ element = reactive(element)
717
+ super().add(element)
718
+ sig = self._signals.get(element)
719
+ if sig is None:
720
+ self._signals[element] = Signal(True)
721
+ else:
722
+ sig.write(True)
723
+
724
+ @override
725
+ def discard(self, element: T1) -> None:
726
+ element = reactive(element)
727
+ if element in self:
728
+ super().discard(element)
729
+ sig = self._signals.get(element)
730
+ if sig is None:
731
+ self._signals[element] = Signal(False)
732
+ else:
733
+ sig.write(False)
734
+
735
+ @override
736
+ def remove(self, element: T1) -> None:
737
+ if element not in self:
738
+ raise KeyError(element)
739
+ self.discard(element)
740
+
741
+ @override
742
+ def clear(self) -> None:
743
+ for v in list(self):
744
+ self.discard(v)
745
+
746
+ @override
747
+ def update(self, *others: Iterable[T1]) -> None:
748
+ for it in others:
749
+ for v in it:
750
+ self.add(v)
751
+
752
+ @override
753
+ def difference_update(self, *others: Iterable[T1]) -> None:
754
+ to_remove: set[T1] = set()
755
+ for it in others:
756
+ for v in it:
757
+ if v in self:
758
+ to_remove.add(v)
759
+ for v in to_remove:
760
+ self.discard(v)
761
+
762
+ def unwrap(self) -> set[_Any]:
763
+ """Return a plain set while subscribing to membership signals.
764
+
765
+ Returns:
766
+ A plain set with all reactive containers recursively unwrapped.
767
+ """
768
+ result: set[_Any] = set()
769
+ for value in set.__iter__(self):
770
+ _ = value in self
771
+ result.add(unwrap(value))
772
+ return result
773
+
774
+ def __copy__(self):
775
+ return type(self)(list(set.__iter__(self))) # pyright: ignore[reportUnknownArgumentType]
776
+
777
+ def __deepcopy__(self, memo: dict[int, _Any]):
778
+ if id(self) in memo:
779
+ return memo[id(self)]
780
+ result = type(self)()
781
+ memo[id(self)] = result
782
+ for value in set.__iter__(self):
783
+ result.add(copy.deepcopy(cast(T1, value), memo))
784
+ return result
785
+
786
+
787
+ # ---- Reactive dataclass support ----
788
+
789
+
790
+ # Fallback storage for signal instances on objects that cannot hold attributes
791
+ # (e.g., slotted dataclasses). Keys are object ids; entries are cleaned up via
792
+ # a weakref finalizer when possible. This avoids requiring objects to be hashable.
793
+ _INSTANCE_SIGNAL_STORE_BY_ID: dict[int, dict[str, Signal[_Any]]] = {}
794
+ _INSTANCE_STORE_WEAKREFS: dict[int, weakref.ref[_Any]] = {}
795
+
796
+ # Cache mapping original dataclass type -> generated reactive dataclass subclass
797
+ _REACTIVE_DATACLASS_CACHE: dict[type, type] = {}
798
+
799
+ # Track objects currently initializing via dataclass __init__ or reactive upgrade
800
+ _INITIALIZING_OBJECT_IDS: set[int] = set()
801
+
802
+
803
+ def _copy_dataclass_params(parent: type) -> dict[str, _Any]:
804
+ params = getattr(parent, "__dataclass_params__", None)
805
+ if params is None:
806
+ return {}
807
+ copied: dict[str, _Any] = {}
808
+ for key in (
809
+ "init",
810
+ "repr",
811
+ "eq",
812
+ "order",
813
+ "unsafe_hash",
814
+ "frozen",
815
+ "match_args",
816
+ "kw_only",
817
+ "slots",
818
+ "weakref_slot",
819
+ ):
820
+ if hasattr(params, key):
821
+ copied[key] = getattr(params, key)
822
+ return copied
823
+
824
+
825
+ def _get_reactive_dataclass_class(parent: type) -> type:
826
+ # Already reactive?
827
+ if getattr(parent, "__is_reactive_dataclass__", False):
828
+ return parent
829
+ cached = _REACTIVE_DATACLASS_CACHE.get(parent)
830
+ if cached is not None:
831
+ return cached
832
+ if not is_dataclass(parent):
833
+ raise TypeError("_get_reactive_dataclass_class expects a dataclass type")
834
+
835
+ subclass_name = f"Reactive{parent.__name__}"
836
+ subclass = type(
837
+ subclass_name,
838
+ (parent,),
839
+ {
840
+ "__module__": parent.__module__,
841
+ "__doc__": getattr(parent, "__doc__", None),
842
+ },
843
+ )
844
+
845
+ # Mirror parent dataclass parameters when generating dataclass on subclass
846
+ dc_kwargs = _copy_dataclass_params(parent)
847
+ reactive_subclass = reactive_dataclass(subclass, **dc_kwargs) # type: ignore[arg-type]
848
+ reactive_subclass.__is_reactive_dataclass__ = True
849
+ reactive_subclass.__reactive_base__ = parent
850
+
851
+ # Hide InitVar attributes on instances by shadowing with a descriptor that raises
852
+ class _HiddenInitVar:
853
+ def __get__(self, obj: _Any, objtype: type[_Any] | None = None):
854
+ raise AttributeError
855
+
856
+ parent_annotations = getattr(parent, "__annotations__", {}) or {}
857
+ for _name, _anno in parent_annotations.items():
858
+ # Detect dataclasses.InitVar annotations (e.g., InitVar[int])
859
+ try:
860
+ if isinstance(_anno, _DC_InitVar):
861
+ setattr(reactive_subclass, _name, _HiddenInitVar())
862
+ except Exception:
863
+ pass
864
+
865
+ # Wrap __init__ to allow field assignment during construction even if frozen
866
+ original_init = getattr(reactive_subclass, "__init__", None)
867
+ if callable(original_init):
868
+
869
+ def _wrapped_init(self: _Any, *args: _Any, **kwargs: _Any):
870
+ _INITIALIZING_OBJECT_IDS.add(id(self))
871
+ try:
872
+ return original_init(self, *args, **kwargs)
873
+ finally:
874
+ _INITIALIZING_OBJECT_IDS.discard(id(self))
875
+
876
+ reactive_subclass.__init__ = _wrapped_init # pyright: ignore[reportAttributeAccessIssue]
877
+
878
+ _REACTIVE_DATACLASS_CACHE[parent] = reactive_subclass
879
+ return reactive_subclass
880
+
881
+
882
+ class ReactiveProperty(Generic[T1]):
883
+ """Unified reactive descriptor used for State fields and dataclass fields."""
884
+
885
+ name: str | None
886
+ private_name: str | None
887
+ owner_name: str | None
888
+ default: T1 | _Any
889
+
890
+ def __init__(self, name: str | None = None, default: T1 | None = _MISSING):
891
+ self.name = name
892
+ self.private_name = None
893
+ self.owner_name = None
894
+ self.default = reactive(default) if default is not _MISSING else _MISSING
895
+
896
+ def __set_name__(self, owner: type[_Any], name: str):
897
+ self.name = self.name or name
898
+ self.private_name = f"__signal_{self.name}"
899
+ self.owner_name = getattr(owner, "__name__", owner.__class__.__name__)
900
+
901
+ def _get_signal(self, obj: _Any) -> Signal[T1]:
902
+ priv = cast(str, self.private_name)
903
+ # Try fast path: attribute on the instance
904
+ try:
905
+ sig = getattr(obj, priv)
906
+ except AttributeError:
907
+ sig = None
908
+
909
+ # Fallback store for slotted instances (no __dict__) using id(obj)
910
+ if sig is None:
911
+ per_obj = _INSTANCE_SIGNAL_STORE_BY_ID.get(id(obj))
912
+ if per_obj is not None:
913
+ sig = per_obj.get(priv)
914
+
915
+ if sig is None:
916
+ init_value = None if self.default is _MISSING else self.default
917
+ sig = Signal(init_value, name=f"{self.owner_name}.{self.name}")
918
+ # Try to attach to the instance; if that fails (e.g., __slots__), use fallback store
919
+ try:
920
+ setattr(obj, priv, sig)
921
+ except Exception:
922
+ obj_id = id(obj)
923
+ mapping = _INSTANCE_SIGNAL_STORE_BY_ID.get(obj_id)
924
+ if mapping is None:
925
+ mapping = {}
926
+ _INSTANCE_SIGNAL_STORE_BY_ID[obj_id] = mapping
927
+ # Install a weakref to clean up when object is GC'd
928
+ try:
929
+ _INSTANCE_STORE_WEAKREFS[obj_id] = weakref.ref(
930
+ obj,
931
+ lambda _r, oid=obj_id: (
932
+ _INSTANCE_SIGNAL_STORE_BY_ID.pop(oid, None),
933
+ _INSTANCE_STORE_WEAKREFS.pop(oid, None),
934
+ ),
935
+ )
936
+ except TypeError:
937
+ # Object not weakref-able; best effort leak-free by reusing id slot if recreated
938
+ pass
939
+ mapping[priv] = sig
940
+ return cast(Signal[T1], sig)
941
+
942
+ def __get__(self, obj: _Any, objtype: type[_Any] | None = None) -> T1:
943
+ if obj is None:
944
+ return self # pyright: ignore[reportReturnType]
945
+ # If there is no signal yet and there was no default, mirror normal attribute error
946
+ priv = cast(str, self.private_name)
947
+ sig = getattr(obj, priv, None)
948
+ if sig is None and self.default is _MISSING:
949
+ owner = self.owner_name or obj.__class__.__name__
950
+ raise AttributeError(
951
+ f"Reactive property '{owner}.{self.name}' accessed before initialization"
952
+ )
953
+ return self._get_signal(obj).read()
954
+
955
+ def __set__(self, obj: _Any, value: T1) -> None:
956
+ sig = self._get_signal(obj)
957
+ value = reactive(value)
958
+ sig.write(value)
959
+
960
+ # Helper for State.properties() discovery
961
+ def get_signal(self, obj: _Any) -> Signal[_Any]:
962
+ return self._get_signal(obj)
963
+
964
+
965
+ class DataclassReactiveProperty(ReactiveProperty[T1]):
966
+ """Reactive descriptor for dataclass fields with frozen enforcement."""
967
+
968
+ def __init__(self, name: str | None = None, default: T1 | None = _MISSING):
969
+ super().__init__(name, default)
970
+ self.owner_cls: type | None = None
971
+
972
+ @override
973
+ def __set_name__(self, owner: type[_Any], name: str):
974
+ super().__set_name__(owner, name)
975
+ self.owner_cls = owner
976
+
977
+ @override
978
+ def __set__(self, obj: _Any, value: T1) -> None:
979
+ # Enforce frozen dataclasses semantics
980
+ owner = self.owner_cls or obj.__class__
981
+ params = getattr(owner, "__dataclass_params__", None)
982
+ if (
983
+ params is not None
984
+ and getattr(params, "frozen", False)
985
+ and id(obj) not in _INITIALIZING_OBJECT_IDS
986
+ ):
987
+ # Match dataclasses' message
988
+ raise _DC_FrozenInstanceError(f"cannot assign to field '{self.name}'")
989
+ super().__set__(obj, value)
990
+
991
+
992
+ @overload
993
+ def reactive_dataclass(cls: type, /, **dataclass_kwargs: _Any) -> type: ...
994
+ @overload
995
+ def reactive_dataclass(
996
+ **dataclass_kwargs: _Any,
997
+ ) -> Callable[[type], type]: ...
998
+
999
+
1000
+ def reactive_dataclass(
1001
+ cls: type | None = None, /, **dataclass_kwargs: _Any
1002
+ ) -> Callable[[type], type] | type:
1003
+ """Decorator to make a dataclass' fields reactive.
1004
+
1005
+ Usage:
1006
+ @reactive_dataclass
1007
+ @dataclass
1008
+ class Model: ...
1009
+
1010
+ Or simply:
1011
+ @reactive_dataclass
1012
+ class Model: ... # will be dataclass()-ed with defaults
1013
+ """
1014
+
1015
+ def _wrap(
1016
+ cls_param: type,
1017
+ ) -> type:
1018
+ # ensure it's a dataclass
1019
+ klass: type = cls_param
1020
+ if not is_dataclass(klass):
1021
+ klass = cast(type, _dc_dataclass(klass, **dataclass_kwargs)) # pyright: ignore[reportUnknownArgumentType]
1022
+
1023
+ # Replace fields with DataclassReactiveProperty descriptors
1024
+ for f in _dc_fields(klass):
1025
+ # Skip ClassVars or InitVars implicitly as dataclasses excludes them from fields()
1026
+ default_val = f.default if f.default is not _DC_MISSING else _MISSING
1027
+ rp = DataclassReactiveProperty(f.name, default_val)
1028
+ setattr(klass, f.name, rp)
1029
+ # When assigning descriptors post-class-creation, __set_name__ is not called automatically
1030
+ rp.__set_name__(klass, f.name)
1031
+
1032
+ return klass
1033
+
1034
+ if cls is None:
1035
+ return _wrap
1036
+ return _wrap(cls)
1037
+
1038
+
1039
+ # ---- Auto-wrapping helpers ----
1040
+
1041
+
1042
+ @overload
1043
+ def reactive(value: dict[T1, T2]) -> ReactiveDict[T1, T2]: ...
1044
+ @overload
1045
+ def reactive(value: list[T1]) -> ReactiveList[T1]: ...
1046
+ @overload
1047
+ def reactive(value: set[T1]) -> ReactiveSet[T1]: ...
1048
+ @overload
1049
+ def reactive(value: T1) -> T1: ...
1050
+
1051
+
1052
+ def reactive(value: _Any) -> _Any:
1053
+ """Wrap built-in collections in their reactive counterparts if not already reactive.
1054
+
1055
+ Converts:
1056
+ - dict -> ReactiveDict
1057
+ - list -> ReactiveList
1058
+ - set -> ReactiveSet
1059
+ - dataclass instance -> reactive dataclass with Signal-backed fields
1060
+
1061
+ Leaves other values (primitives, already-reactive containers) untouched.
1062
+
1063
+ Args:
1064
+ value: The value to make reactive.
1065
+
1066
+ Returns:
1067
+ The reactive version of the value, or the original if already reactive
1068
+ or not a supported collection type.
1069
+
1070
+ Example:
1071
+
1072
+ ```python
1073
+ data = reactive({"key": "value"}) # ReactiveDict
1074
+ items = reactive([1, 2, 3]) # ReactiveList
1075
+ tags = reactive({"a", "b"}) # ReactiveSet
1076
+ ```
1077
+ """
1078
+ if isinstance(value, ReactiveDict | ReactiveList | ReactiveSet):
1079
+ return value
1080
+ # Dataclass instance: upgrade to reactive subclass in-place
1081
+ if not isinstance(value, type) and is_dataclass(value):
1082
+ # Already reactive instance?
1083
+ if getattr(type(value), "__is_reactive_dataclass__", False):
1084
+ return value
1085
+ base_cls = cast(type, type(value))
1086
+ reactive_cls = _get_reactive_dataclass_class(base_cls)
1087
+ # Capture current field values
1088
+ field_values: dict[str, _Any] = {}
1089
+ for f in _dc_fields(base_cls): # type: ignore[arg-type]
1090
+ try:
1091
+ field_values[f.name] = getattr(value, f.name)
1092
+ except Exception:
1093
+ field_values[f.name] = None
1094
+ # For dict-backed instances, drop raw attrs to avoid stale shadowing
1095
+ if hasattr(value, "__dict__") and isinstance(value.__dict__, dict):
1096
+ for name in field_values.keys():
1097
+ value.__dict__.pop(name, None)
1098
+ # Swap class
1099
+ value.__class__ = reactive_cls # pyright: ignore[reportAttributeAccessIssue]
1100
+ # Write back via descriptors (handles frozen via object.__setattr__)
1101
+ _INITIALIZING_OBJECT_IDS.add(id(value))
1102
+ try:
1103
+ for name, v in field_values.items():
1104
+ object.__setattr__(value, name, reactive(v))
1105
+ finally:
1106
+ _INITIALIZING_OBJECT_IDS.discard(id(value))
1107
+ return value
1108
+ if isinstance(value, dict):
1109
+ return ReactiveDict(value) # pyright: ignore[reportUnknownArgumentType]
1110
+ if isinstance(value, list):
1111
+ return ReactiveList(value) # pyright: ignore[reportUnknownArgumentType]
1112
+ if isinstance(value, set):
1113
+ return ReactiveSet(value) # pyright: ignore[reportUnknownArgumentType]
1114
+ if isinstance(value, type) and is_dataclass(value):
1115
+ return _get_reactive_dataclass_class(value)
1116
+ return value
1117
+
1118
+
1119
+ def unwrap(value: _Any, untrack: bool = False) -> _Any:
1120
+ """Recursively unwrap reactive containers into plain Python values.
1121
+
1122
+ Converts:
1123
+ - Signal/Computed -> their read() value
1124
+ - ReactiveDict -> dict
1125
+ - ReactiveList -> list
1126
+ - ReactiveSet -> set
1127
+ - Other Mapping/Sequence types are recursively unwrapped
1128
+
1129
+ Args:
1130
+ value: The value to unwrap.
1131
+ untrack: If True, don't track dependencies during unwrapping.
1132
+ Defaults to False.
1133
+
1134
+ Returns:
1135
+ A plain Python value with all reactive containers unwrapped.
1136
+
1137
+ Example:
1138
+
1139
+ ```python
1140
+ count = Signal(5)
1141
+ data = ReactiveDict({"count": count})
1142
+ unwrap(data) # {"count": 5}
1143
+ ```
1144
+ """
1145
+
1146
+ def _unwrap(v: _Any) -> _Any:
1147
+ if isinstance(v, (Signal, Computed)):
1148
+ return _unwrap(v.unwrap())
1149
+ if isinstance(v, ReactiveDict):
1150
+ return v.unwrap()
1151
+ if isinstance(v, ReactiveList):
1152
+ return v.unwrap()
1153
+ if isinstance(v, ReactiveSet):
1154
+ return v.unwrap()
1155
+ if isinstance(v, Mapping):
1156
+ return {k: _unwrap(val) for k, val in v.items()}
1157
+ if isinstance(v, Sequence) and not isinstance(v, (str, bytes, bytearray)):
1158
+ if isinstance(v, tuple):
1159
+ # Preserve namedtuple types
1160
+ if hasattr(v, "_fields"):
1161
+ return type(v)(*(_unwrap(val) for val in v))
1162
+ else:
1163
+ return tuple(_unwrap(val) for val in v)
1164
+ return [_unwrap(val) for val in v]
1165
+ if isinstance(v, set):
1166
+ return {_unwrap(val) for val in v}
1167
+ return v
1168
+
1169
+ if untrack:
1170
+ with Untrack():
1171
+ return _unwrap(value)
1172
+ return _unwrap(value)