pulse-framework 0.1.44__py3-none-any.whl → 0.1.47__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.
- pulse/__init__.py +10 -24
- pulse/app.py +3 -25
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +40 -8
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/queries/client.py +7 -7
- pulse/queries/effect.py +16 -0
- pulse/queries/infinite_query.py +138 -29
- pulse/queries/mutation.py +1 -15
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +610 -174
- pulse/queries/store.py +11 -14
- pulse/react_component.py +167 -14
- pulse/reactive.py +19 -1
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +185 -59
- pulse/renderer.py +80 -158
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/types/event_handler.py +3 -2
- pulse/vdom.py +212 -13
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
- pulse_framework-0.1.47.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.44.dist-info/RECORD +0 -79
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/entry_points.txt +0 -0
pulse/queries/store.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import datetime as dt
|
|
2
|
-
from collections.abc import
|
|
2
|
+
from collections.abc import Callable
|
|
3
3
|
from typing import Any, TypeVar, cast
|
|
4
4
|
|
|
5
|
+
from pulse.helpers import MISSING
|
|
5
6
|
from pulse.queries.common import QueryKey
|
|
6
7
|
from pulse.queries.infinite_query import InfiniteQuery, Page
|
|
7
|
-
from pulse.queries.query import RETRY_DELAY_DEFAULT,
|
|
8
|
+
from pulse.queries.query import RETRY_DELAY_DEFAULT, KeyedQuery
|
|
8
9
|
|
|
9
10
|
T = TypeVar("T")
|
|
10
11
|
|
|
@@ -15,26 +16,25 @@ class QueryStore:
|
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
def __init__(self):
|
|
18
|
-
self._entries: dict[QueryKey,
|
|
19
|
+
self._entries: dict[QueryKey, KeyedQuery[Any] | InfiniteQuery[Any, Any]] = {}
|
|
19
20
|
|
|
20
21
|
def items(self):
|
|
21
22
|
"""Iterate over all (key, query) pairs in the store."""
|
|
22
23
|
return self._entries.items()
|
|
23
24
|
|
|
24
|
-
def get_any(self, key: QueryKey)
|
|
25
|
+
def get_any(self, key: QueryKey):
|
|
25
26
|
"""Get any query (regular or infinite) by key, or None if not found."""
|
|
26
27
|
return self._entries.get(key)
|
|
27
28
|
|
|
28
29
|
def ensure(
|
|
29
30
|
self,
|
|
30
31
|
key: QueryKey,
|
|
31
|
-
|
|
32
|
-
initial_data: T | None = None,
|
|
32
|
+
initial_data: T | None = MISSING,
|
|
33
33
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
34
34
|
gc_time: float = 300.0,
|
|
35
35
|
retries: int = 3,
|
|
36
36
|
retry_delay: float = RETRY_DELAY_DEFAULT,
|
|
37
|
-
) ->
|
|
37
|
+
) -> KeyedQuery[T]:
|
|
38
38
|
# Return existing entry if present
|
|
39
39
|
existing = self._entries.get(key)
|
|
40
40
|
if existing:
|
|
@@ -42,15 +42,14 @@ class QueryStore:
|
|
|
42
42
|
raise TypeError(
|
|
43
43
|
"Query key is already used for an infinite query; cannot reuse for regular query"
|
|
44
44
|
)
|
|
45
|
-
return cast(
|
|
45
|
+
return cast(KeyedQuery[T], existing)
|
|
46
46
|
|
|
47
|
-
def _on_dispose(e:
|
|
47
|
+
def _on_dispose(e: KeyedQuery[Any]) -> None:
|
|
48
48
|
if e.key in self._entries and self._entries[e.key] is e:
|
|
49
49
|
del self._entries[e.key]
|
|
50
50
|
|
|
51
|
-
entry =
|
|
51
|
+
entry = KeyedQuery(
|
|
52
52
|
key,
|
|
53
|
-
fetch_fn,
|
|
54
53
|
initial_data=initial_data,
|
|
55
54
|
initial_data_updated_at=initial_data_updated_at,
|
|
56
55
|
gc_time=gc_time,
|
|
@@ -61,7 +60,7 @@ class QueryStore:
|
|
|
61
60
|
self._entries[key] = entry
|
|
62
61
|
return entry
|
|
63
62
|
|
|
64
|
-
def get(self, key: QueryKey) ->
|
|
63
|
+
def get(self, key: QueryKey) -> KeyedQuery[Any] | None:
|
|
65
64
|
"""
|
|
66
65
|
Get an existing regular query by key, or None if not found.
|
|
67
66
|
"""
|
|
@@ -82,7 +81,6 @@ class QueryStore:
|
|
|
82
81
|
def ensure_infinite(
|
|
83
82
|
self,
|
|
84
83
|
key: QueryKey,
|
|
85
|
-
query_fn: Callable[[Any], Awaitable[Any]],
|
|
86
84
|
*,
|
|
87
85
|
initial_page_param: Any,
|
|
88
86
|
get_next_page_param: Callable[[list[Page[Any, Any]]], Any | None],
|
|
@@ -108,7 +106,6 @@ class QueryStore:
|
|
|
108
106
|
|
|
109
107
|
entry = InfiniteQuery(
|
|
110
108
|
key,
|
|
111
|
-
query_fn,
|
|
112
109
|
initial_page_param=initial_page_param,
|
|
113
110
|
get_next_page_param=get_next_page_param,
|
|
114
111
|
get_previous_page_param=get_previous_page_param,
|
pulse/react_component.py
CHANGED
|
@@ -13,6 +13,7 @@ from types import UnionType
|
|
|
13
13
|
from typing import (
|
|
14
14
|
Annotated,
|
|
15
15
|
Any,
|
|
16
|
+
ClassVar,
|
|
16
17
|
Generic,
|
|
17
18
|
Literal,
|
|
18
19
|
ParamSpec,
|
|
@@ -24,9 +25,18 @@ from typing import (
|
|
|
24
25
|
override,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
|
-
from pulse.codegen.imports import Imported, ImportStatement
|
|
28
28
|
from pulse.helpers import Sentinel
|
|
29
29
|
from pulse.reactive_extensions import unwrap
|
|
30
|
+
from pulse.transpiler.errors import JSCompilationError
|
|
31
|
+
from pulse.transpiler.imports import Import
|
|
32
|
+
from pulse.transpiler.nodes import (
|
|
33
|
+
JSExpr,
|
|
34
|
+
JSMember,
|
|
35
|
+
JSSpread,
|
|
36
|
+
JSXElement,
|
|
37
|
+
JSXProp,
|
|
38
|
+
JSXSpreadProp,
|
|
39
|
+
)
|
|
30
40
|
from pulse.vdom import Child, Element, Node
|
|
31
41
|
|
|
32
42
|
T = TypeVar("T")
|
|
@@ -295,25 +305,112 @@ def default_fn_signature_without_children(
|
|
|
295
305
|
) -> Element: ...
|
|
296
306
|
|
|
297
307
|
|
|
298
|
-
|
|
308
|
+
# ----------------------------------------------------------------------------
|
|
309
|
+
# JSX transpilation helpers
|
|
310
|
+
# ----------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _build_jsx_props(kwargs: dict[str, Any]) -> list[JSXProp | JSXSpreadProp]:
|
|
314
|
+
"""Build JSX props list from kwargs dict.
|
|
315
|
+
|
|
316
|
+
Kwargs maps:
|
|
317
|
+
- "propName" -> value for named props
|
|
318
|
+
- "__spread_N" -> JSSpread(expr) for spread props
|
|
319
|
+
"""
|
|
320
|
+
props: list[JSXProp | JSXSpreadProp] = []
|
|
321
|
+
for key, value in kwargs.items():
|
|
322
|
+
if isinstance(value, JSSpread):
|
|
323
|
+
props.append(JSXSpreadProp(value.expr))
|
|
324
|
+
else:
|
|
325
|
+
props.append(JSXProp(key, JSExpr.of(value)))
|
|
326
|
+
return props
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _flatten_children(items: list[Any], out: list[JSExpr | JSXElement | str]) -> None:
|
|
330
|
+
"""Flatten arrays and handle spreads in children list."""
|
|
331
|
+
from pulse.transpiler.nodes import JSArray, JSString
|
|
332
|
+
|
|
333
|
+
for it in items:
|
|
334
|
+
# Convert raw values first
|
|
335
|
+
it = JSExpr.of(it) if not isinstance(it, JSExpr) else it
|
|
336
|
+
if isinstance(it, JSArray):
|
|
337
|
+
_flatten_children(list(it.elements), out)
|
|
338
|
+
elif isinstance(it, JSSpread):
|
|
339
|
+
out.append(it.expr)
|
|
340
|
+
elif isinstance(it, JSString):
|
|
341
|
+
out.append(it.value)
|
|
342
|
+
else:
|
|
343
|
+
out.append(it)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ReactComponentCallExpr(JSExpr):
|
|
347
|
+
"""JSX call expression for a ReactComponent.
|
|
348
|
+
|
|
349
|
+
Created when a ReactComponent is called with props. Supports subscripting
|
|
350
|
+
to add children, producing the final JSXElement.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
is_jsx: ClassVar[bool] = True
|
|
354
|
+
component: "ReactComponent[...]"
|
|
355
|
+
props: tuple[JSXProp | JSXSpreadProp, ...]
|
|
356
|
+
children: tuple[str | JSExpr | JSXElement, ...]
|
|
357
|
+
|
|
358
|
+
def __init__(
|
|
359
|
+
self,
|
|
360
|
+
component: "ReactComponent[...]",
|
|
361
|
+
props: tuple[JSXProp | JSXSpreadProp, ...],
|
|
362
|
+
children: tuple[str | JSExpr | JSXElement, ...],
|
|
363
|
+
) -> None:
|
|
364
|
+
self.component = component
|
|
365
|
+
self.props = props
|
|
366
|
+
self.children = children
|
|
367
|
+
|
|
368
|
+
@override
|
|
369
|
+
def emit(self) -> str:
|
|
370
|
+
return JSXElement(self.component, self.props, self.children).emit()
|
|
371
|
+
|
|
372
|
+
@override
|
|
373
|
+
def emit_subscript(self, indices: list[Any]) -> JSExpr:
|
|
374
|
+
"""Handle Component(props...)[children] -> JSXElement."""
|
|
375
|
+
extra_children: list[JSExpr | JSXElement | str] = []
|
|
376
|
+
_flatten_children(indices, extra_children)
|
|
377
|
+
all_children = list(self.children) + extra_children
|
|
378
|
+
return JSXElement(self.component, self.props, all_children)
|
|
379
|
+
|
|
380
|
+
@override
|
|
381
|
+
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
382
|
+
"""Calling an already-called component is an error."""
|
|
383
|
+
raise JSCompilationError(
|
|
384
|
+
f"Cannot call <{self.component.name}> - already called. "
|
|
385
|
+
+ "Use subscript for children: Component(props...)[children]"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class ReactComponent(JSExpr, Generic[P]):
|
|
299
390
|
"""
|
|
300
391
|
A React component that can be used within the UI tree.
|
|
301
392
|
Returns a function that creates mount point UITreeNode instances.
|
|
302
393
|
|
|
303
394
|
Args:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
alias: Optional alias for the component in the registry
|
|
395
|
+
name: Name of the component (or "default" for default export)
|
|
396
|
+
src: Module path to import the component from
|
|
307
397
|
is_default: True if this is a default export, else named export
|
|
308
|
-
|
|
398
|
+
prop: Optional property name to access the component from the imported object
|
|
399
|
+
lazy: Whether to lazy load the component
|
|
400
|
+
version: Optional npm semver constraint for this component's package
|
|
401
|
+
prop_spec: Optional PropSpec for the component
|
|
402
|
+
fn_signature: Function signature to parse for props
|
|
403
|
+
extra_imports: Additional imports to include (CSS files, etc.)
|
|
309
404
|
|
|
310
405
|
Returns:
|
|
311
406
|
A function that creates Node instances with mount point tags
|
|
312
407
|
"""
|
|
313
408
|
|
|
409
|
+
import_: Import
|
|
314
410
|
props_spec: PropSpec
|
|
315
411
|
fn_signature: Callable[P, Element]
|
|
316
412
|
lazy: bool
|
|
413
|
+
_prop: str | None # Property access like AppShell.Header
|
|
317
414
|
|
|
318
415
|
def __init__(
|
|
319
416
|
self,
|
|
@@ -326,11 +423,14 @@ class ReactComponent(Generic[P], Imported):
|
|
|
326
423
|
version: str | None = None,
|
|
327
424
|
prop_spec: PropSpec | None = None,
|
|
328
425
|
fn_signature: Callable[P, Element] = default_signature,
|
|
329
|
-
extra_imports: tuple[
|
|
330
|
-
| list[ImportStatement]
|
|
331
|
-
| None = None,
|
|
426
|
+
extra_imports: tuple[Import, ...] | list[Import] | None = None,
|
|
332
427
|
):
|
|
333
|
-
|
|
428
|
+
# Create the Import directly (prop is stored separately on ReactComponent)
|
|
429
|
+
if is_default:
|
|
430
|
+
self.import_ = Import.default(name, src)
|
|
431
|
+
else:
|
|
432
|
+
self.import_ = Import.named(name, src)
|
|
433
|
+
self._prop = prop
|
|
334
434
|
|
|
335
435
|
# Build props_spec from fn_signature if provided and props not provided
|
|
336
436
|
if prop_spec:
|
|
@@ -347,10 +447,62 @@ class ReactComponent(Generic[P], Imported):
|
|
|
347
447
|
self.lazy = lazy
|
|
348
448
|
# Optional npm semver constraint for this component's package
|
|
349
449
|
self.version: str | None = version
|
|
350
|
-
# Additional
|
|
351
|
-
self.extra_imports: list[
|
|
450
|
+
# Additional imports to include in route where this component is used
|
|
451
|
+
self.extra_imports: list[Import] = list(extra_imports or [])
|
|
352
452
|
COMPONENT_REGISTRY.get().add(self)
|
|
353
453
|
|
|
454
|
+
@override
|
|
455
|
+
def emit(self) -> str:
|
|
456
|
+
if self.prop:
|
|
457
|
+
return JSMember(self.import_, self.prop).emit()
|
|
458
|
+
return self.import_.emit()
|
|
459
|
+
|
|
460
|
+
@override
|
|
461
|
+
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
462
|
+
"""Handle Component(props...) -> ReactComponentCallExpr."""
|
|
463
|
+
props_list = _build_jsx_props(kwargs)
|
|
464
|
+
children_list: list[JSExpr | JSXElement | str] = []
|
|
465
|
+
_flatten_children(args, children_list)
|
|
466
|
+
return ReactComponentCallExpr(self, tuple(props_list), tuple(children_list))
|
|
467
|
+
|
|
468
|
+
@override
|
|
469
|
+
def emit_subscript(self, indices: list[Any]) -> JSExpr:
|
|
470
|
+
"""Direct subscript on ReactComponent is not allowed.
|
|
471
|
+
|
|
472
|
+
Use Component(props...)[children] instead of Component[children].
|
|
473
|
+
"""
|
|
474
|
+
raise JSCompilationError(
|
|
475
|
+
f"Cannot subscript ReactComponent '{self.name}' directly. "
|
|
476
|
+
+ "Use Component(props...)[children] or Component()[children] instead."
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def name(self) -> str:
|
|
481
|
+
return self.import_.name
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def src(self) -> str:
|
|
485
|
+
return self.import_.src
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def is_default(self) -> bool:
|
|
489
|
+
return self.import_.is_default
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def prop(self) -> str | None:
|
|
493
|
+
return self._prop
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def expr(self) -> str:
|
|
497
|
+
"""Expression for the component in the registry and VDOM tags.
|
|
498
|
+
|
|
499
|
+
Uses the import's js_name (with unique ID suffix) to match the
|
|
500
|
+
unified registry on the client side.
|
|
501
|
+
"""
|
|
502
|
+
if self.prop:
|
|
503
|
+
return f"{self.import_.js_name}.{self.prop}"
|
|
504
|
+
return self.import_.js_name
|
|
505
|
+
|
|
354
506
|
@override
|
|
355
507
|
def __repr__(self) -> str:
|
|
356
508
|
default_part = ", default=True" if self.is_default else ""
|
|
@@ -359,7 +511,8 @@ class ReactComponent(Generic[P], Imported):
|
|
|
359
511
|
props_part = f", props_spec={self.props_spec!r}"
|
|
360
512
|
return f"ReactComponent(name='{self.name}', src='{self.src}'{prop_part}{default_part}{lazy_part}{props_part})"
|
|
361
513
|
|
|
362
|
-
|
|
514
|
+
@override
|
|
515
|
+
def __call__(self, *children: P.args, **props: P.kwargs) -> Node: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
363
516
|
key = props.get("key")
|
|
364
517
|
if key is not None and not isinstance(key, str):
|
|
365
518
|
raise ValueError("key must be a string or None")
|
|
@@ -806,7 +959,7 @@ def react_component(
|
|
|
806
959
|
is_default: bool = False,
|
|
807
960
|
lazy: bool = False,
|
|
808
961
|
version: str | None = None,
|
|
809
|
-
extra_imports: list[
|
|
962
|
+
extra_imports: list[Import] | None = None,
|
|
810
963
|
) -> Callable[[Callable[P, None] | Callable[P, Element]], ReactComponent[P]]:
|
|
811
964
|
"""
|
|
812
965
|
Decorator to define a React component wrapper. The decorated function is
|
pulse/reactive.py
CHANGED
|
@@ -191,7 +191,8 @@ class Computed(Generic[T_co]):
|
|
|
191
191
|
if len(scope.effects) > 0:
|
|
192
192
|
raise RuntimeError(
|
|
193
193
|
"An effect was created within a computed variable's function. "
|
|
194
|
-
+ "This
|
|
194
|
+
+ "This is most likely unintended. If you need to create an effect here, "
|
|
195
|
+
+ "wrap the effect creation with Untrack()."
|
|
195
196
|
)
|
|
196
197
|
finally:
|
|
197
198
|
self.on_stack = False
|
|
@@ -274,6 +275,7 @@ class Effect(Disposable):
|
|
|
274
275
|
_interval_handle: asyncio.TimerHandle | None
|
|
275
276
|
explicit_deps: bool
|
|
276
277
|
batch: "Batch | None"
|
|
278
|
+
paused: bool
|
|
277
279
|
|
|
278
280
|
def __init__(
|
|
279
281
|
self,
|
|
@@ -301,6 +303,7 @@ class Effect(Disposable):
|
|
|
301
303
|
self._lazy = lazy
|
|
302
304
|
self._interval = interval
|
|
303
305
|
self._interval_handle = None
|
|
306
|
+
self.paused = False
|
|
304
307
|
|
|
305
308
|
if immediate and lazy:
|
|
306
309
|
raise ValueError("An effect cannot be boht immediate and lazy")
|
|
@@ -358,7 +361,20 @@ class Effect(Disposable):
|
|
|
358
361
|
self._interval_handle.cancel()
|
|
359
362
|
self._interval_handle = None
|
|
360
363
|
|
|
364
|
+
def pause(self):
|
|
365
|
+
"""Pause the effect - it won't run when dependencies change."""
|
|
366
|
+
self.paused = True
|
|
367
|
+
self.cancel(cancel_interval=True)
|
|
368
|
+
|
|
369
|
+
def resume(self):
|
|
370
|
+
"""Resume a paused effect and schedule it to run."""
|
|
371
|
+
if self.paused:
|
|
372
|
+
self.paused = False
|
|
373
|
+
self.schedule()
|
|
374
|
+
|
|
361
375
|
def schedule(self):
|
|
376
|
+
if self.paused:
|
|
377
|
+
return
|
|
362
378
|
# Immediate effects run right away when scheduled and do not enter a batch
|
|
363
379
|
if self.immediate:
|
|
364
380
|
self.run()
|
|
@@ -383,6 +399,8 @@ class Effect(Disposable):
|
|
|
383
399
|
self._cancel_interval()
|
|
384
400
|
|
|
385
401
|
def push_change(self):
|
|
402
|
+
if self.paused:
|
|
403
|
+
return
|
|
386
404
|
# Short-circuit if already scheduled in a batch.
|
|
387
405
|
# This avoids redundant schedule() calls and O(n) list checks
|
|
388
406
|
# when the same effect is reached through multiple dependency paths.
|
pulse/reactive_extensions.py
CHANGED
|
@@ -45,7 +45,7 @@ class SupportsKeysAndGetItem(Protocol[T1, T2_co]):
|
|
|
45
45
|
|
|
46
46
|
# Return an iterable view that subscribes to per-key signals during iteration
|
|
47
47
|
class ReactiveDictItems(Generic[T1, T2]):
|
|
48
|
-
__slots__
|
|
48
|
+
__slots__ = ("_host",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
49
49
|
_host: ReactiveDict[T1, T2]
|
|
50
50
|
|
|
51
51
|
def __init__(self, host: ReactiveDict[T1, T2]) -> None:
|
|
@@ -60,7 +60,7 @@ class ReactiveDictItems(Generic[T1, T2]):
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class ReactiveDictValues(Generic[T1, T2]):
|
|
63
|
-
__slots__
|
|
63
|
+
__slots__ = ("_host",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
64
64
|
_host: ReactiveDict[T1, T2]
|
|
65
65
|
|
|
66
66
|
def __init__(self, host: ReactiveDict[T1, T2]) -> None:
|
|
@@ -84,7 +84,7 @@ class ReactiveDict(dict[T1, T2]):
|
|
|
84
84
|
- Iteration, membership checks, and len are reactive to structural changes
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
-
__slots__
|
|
87
|
+
__slots__ = ("_signals", "_structure") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
88
88
|
|
|
89
89
|
def __init__(self, initial: Mapping[T1, T2] | None = None) -> None:
|
|
90
90
|
super().__init__()
|
|
@@ -409,7 +409,7 @@ class ReactiveList(list[T1]):
|
|
|
409
409
|
- len() subscribes to structural changes
|
|
410
410
|
"""
|
|
411
411
|
|
|
412
|
-
__slots__
|
|
412
|
+
__slots__ = ("_signals", "_structure") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
413
413
|
|
|
414
414
|
def __init__(self, initial: Iterable[T1] | None = None) -> None:
|
|
415
415
|
super().__init__()
|
|
@@ -645,7 +645,7 @@ class ReactiveSet(set[T1]):
|
|
|
645
645
|
- Iteration subscribes to membership signals for all elements
|
|
646
646
|
"""
|
|
647
647
|
|
|
648
|
-
__slots__
|
|
648
|
+
__slots__ = ("_signals",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
649
649
|
|
|
650
650
|
def __init__(self, initial: Iterable[T1] | None = None) -> None:
|
|
651
651
|
super().__init__()
|