pulse-framework 0.1.72__py3-none-any.whl → 0.1.73__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 +16 -4
- pulse/cli/processes.py +2 -0
- pulse/debounce.py +79 -0
- pulse/decorators.py +4 -3
- pulse/hooks/effects.py +5 -5
- pulse/hooks/runtime.py +25 -8
- pulse/hooks/setup.py +6 -10
- pulse/hooks/stable.py +5 -9
- pulse/hooks/state.py +4 -8
- pulse/queries/common.py +1 -1
- pulse/queries/infinite_query.py +2 -1
- pulse/queries/mutation.py +2 -1
- pulse/queries/query.py +2 -1
- pulse/render_session.py +2 -2
- pulse/renderer.py +30 -2
- pulse/routing.py +19 -5
- pulse/serializer.py +38 -19
- pulse/state/__init__.py +1 -0
- pulse/state/property.py +218 -0
- pulse/state/query_param.py +538 -0
- pulse/{state.py → state/state.py} +66 -220
- pulse/transpiler/nodes.py +26 -2
- pulse/transpiler/transpiler.py +86 -5
- pulse/transpiler/vdom.py +1 -1
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.73.dist-info}/METADATA +2 -2
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.73.dist-info}/RECORD +28 -24
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.73.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.73.dist-info}/entry_points.txt +0 -0
|
@@ -5,223 +5,23 @@ This module provides the base State class and reactive property system
|
|
|
5
5
|
that enables automatic re-rendering when state changes.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
from abc import ABC, ABCMeta
|
|
10
|
-
from collections.abc import
|
|
8
|
+
import sys
|
|
9
|
+
from abc import ABC, ABCMeta
|
|
10
|
+
from collections.abc import Iterator
|
|
11
11
|
from enum import IntEnum
|
|
12
|
-
from
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
from typing import Any, get_type_hints, override
|
|
13
14
|
|
|
14
15
|
from pulse.helpers import Disposable
|
|
15
|
-
from pulse.reactive import
|
|
16
|
-
AsyncEffect,
|
|
17
|
-
Computed,
|
|
18
|
-
Effect,
|
|
19
|
-
Scope,
|
|
20
|
-
Signal,
|
|
21
|
-
)
|
|
16
|
+
from pulse.reactive import Computed, Effect, Scope, Signal
|
|
22
17
|
from pulse.reactive_extensions import ReactiveProperty
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
StateProperty wraps a Signal and provides automatic reactivity for
|
|
32
|
-
class attributes. When a property is read, it subscribes to the underlying
|
|
33
|
-
Signal. When written, it updates the Signal and triggers re-renders.
|
|
34
|
-
|
|
35
|
-
This class is typically not used directly. Instead, declare typed attributes
|
|
36
|
-
on a State subclass, and the StateMeta metaclass will automatically convert
|
|
37
|
-
them into StateProperty instances.
|
|
38
|
-
|
|
39
|
-
Example:
|
|
40
|
-
|
|
41
|
-
```python
|
|
42
|
-
class MyState(ps.State):
|
|
43
|
-
count: int = 0 # Automatically becomes a StateProperty
|
|
44
|
-
name: str = "default"
|
|
45
|
-
|
|
46
|
-
state = MyState()
|
|
47
|
-
state.count = 5 # Updates the underlying Signal
|
|
48
|
-
print(state.count) # Reads from the Signal, subscribes to changes
|
|
49
|
-
```
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class InitializableProperty(ABC):
|
|
56
|
-
@abstractmethod
|
|
57
|
-
def initialize(self, state: "State", name: str) -> Any: ...
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class ComputedProperty(Generic[T]):
|
|
61
|
-
"""
|
|
62
|
-
Descriptor for computed (derived) properties on State classes.
|
|
63
|
-
|
|
64
|
-
ComputedProperty wraps a method that derives its value from other reactive
|
|
65
|
-
properties. The computed value is cached and only recalculated when its
|
|
66
|
-
dependencies change. Reading a computed property subscribes to it.
|
|
67
|
-
|
|
68
|
-
Created automatically when using the @ps.computed decorator on a State method.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
name: The property name (used for debugging and the private storage key).
|
|
72
|
-
fn: The method that computes the value. Must take only `self` as argument.
|
|
73
|
-
|
|
74
|
-
Example:
|
|
75
|
-
|
|
76
|
-
```python
|
|
77
|
-
class MyState(ps.State):
|
|
78
|
-
count: int = 0
|
|
79
|
-
|
|
80
|
-
@ps.computed
|
|
81
|
-
def doubled(self):
|
|
82
|
-
return self.count * 2
|
|
83
|
-
|
|
84
|
-
state = MyState()
|
|
85
|
-
print(state.doubled) # 0
|
|
86
|
-
state.count = 5
|
|
87
|
-
print(state.doubled) # 10 (automatically recomputed)
|
|
88
|
-
```
|
|
89
|
-
"""
|
|
90
|
-
|
|
91
|
-
name: str
|
|
92
|
-
private_name: str
|
|
93
|
-
fn: "Callable[[State], T]"
|
|
94
|
-
|
|
95
|
-
def __init__(self, name: str, fn: "Callable[[State], T]"):
|
|
96
|
-
self.name = name
|
|
97
|
-
self.private_name = f"__computed_{name}"
|
|
98
|
-
# The computed_template holds the original method
|
|
99
|
-
self.fn = fn
|
|
100
|
-
|
|
101
|
-
def get_computed(self, obj: Any) -> Computed[T]:
|
|
102
|
-
if not isinstance(obj, State):
|
|
103
|
-
raise ValueError(
|
|
104
|
-
f"Computed property {self.name} defined on a non-State class"
|
|
105
|
-
)
|
|
106
|
-
if not hasattr(obj, self.private_name):
|
|
107
|
-
# Create the computed on first access for this instance
|
|
108
|
-
bound_method = self.fn.__get__(obj, obj.__class__)
|
|
109
|
-
new_computed = Computed(
|
|
110
|
-
bound_method,
|
|
111
|
-
name=f"{obj.__class__.__name__}.{self.name}",
|
|
112
|
-
)
|
|
113
|
-
setattr(obj, self.private_name, new_computed)
|
|
114
|
-
return getattr(obj, self.private_name)
|
|
115
|
-
|
|
116
|
-
def __get__(self, obj: Any, objtype: Any = None) -> T:
|
|
117
|
-
if obj is None:
|
|
118
|
-
return self # pyright: ignore[reportReturnType]
|
|
119
|
-
|
|
120
|
-
return self.get_computed(obj).read()
|
|
121
|
-
|
|
122
|
-
def __set__(self, obj: Any, value: Any) -> Never:
|
|
123
|
-
raise AttributeError(f"Cannot set computed property '{self.name}'")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class StateEffect(Generic[T], InitializableProperty):
|
|
127
|
-
"""
|
|
128
|
-
Descriptor for side effects on State classes.
|
|
129
|
-
|
|
130
|
-
StateEffect wraps a method that performs side effects when its dependencies
|
|
131
|
-
change. The effect is initialized when the State instance is created and
|
|
132
|
-
disposed when the State is disposed.
|
|
133
|
-
|
|
134
|
-
Created automatically when using the @ps.effect decorator on a State method.
|
|
135
|
-
Supports both sync and async methods.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
fn: The effect function. Must take only `self` as argument.
|
|
139
|
-
Can return a cleanup function that runs before the next execution
|
|
140
|
-
or when the effect is disposed.
|
|
141
|
-
name: Debug name for the effect. Defaults to "ClassName.method_name".
|
|
142
|
-
immediate: If True, run synchronously when scheduled (sync effects only).
|
|
143
|
-
lazy: If True, don't run on creation; wait for first dependency change.
|
|
144
|
-
on_error: Callback for handling errors during effect execution.
|
|
145
|
-
deps: Explicit dependencies. If provided, auto-tracking is disabled.
|
|
146
|
-
interval: Re-run interval in seconds for polling effects.
|
|
147
|
-
|
|
148
|
-
Example:
|
|
149
|
-
|
|
150
|
-
```python
|
|
151
|
-
class MyState(ps.State):
|
|
152
|
-
count: int = 0
|
|
153
|
-
|
|
154
|
-
@ps.effect
|
|
155
|
-
def log_count(self):
|
|
156
|
-
print(f"Count changed to: {self.count}")
|
|
157
|
-
|
|
158
|
-
@ps.effect
|
|
159
|
-
async def fetch_data(self):
|
|
160
|
-
data = await api.fetch(self.query)
|
|
161
|
-
self.data = data
|
|
162
|
-
|
|
163
|
-
@ps.effect
|
|
164
|
-
def subscribe(self):
|
|
165
|
-
unsub = event_bus.subscribe(self.handle_event)
|
|
166
|
-
return unsub # Cleanup function
|
|
167
|
-
```
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
fn: "Callable[[State], T]"
|
|
171
|
-
name: str | None
|
|
172
|
-
immediate: bool
|
|
173
|
-
on_error: "Callable[[Exception], None] | None"
|
|
174
|
-
lazy: bool
|
|
175
|
-
deps: "list[Signal[Any] | Computed[Any]] | None"
|
|
176
|
-
update_deps: bool | None
|
|
177
|
-
interval: float | None
|
|
178
|
-
|
|
179
|
-
def __init__(
|
|
180
|
-
self,
|
|
181
|
-
fn: "Callable[[State], T]",
|
|
182
|
-
name: str | None = None,
|
|
183
|
-
immediate: bool = False,
|
|
184
|
-
lazy: bool = False,
|
|
185
|
-
on_error: "Callable[[Exception], None] | None" = None,
|
|
186
|
-
deps: "list[Signal[Any] | Computed[Any]] | None" = None,
|
|
187
|
-
update_deps: bool | None = None,
|
|
188
|
-
interval: float | None = None,
|
|
189
|
-
):
|
|
190
|
-
self.fn = fn
|
|
191
|
-
self.name = name
|
|
192
|
-
self.immediate = immediate
|
|
193
|
-
self.on_error = on_error
|
|
194
|
-
self.lazy = lazy
|
|
195
|
-
self.deps = deps
|
|
196
|
-
self.update_deps = update_deps
|
|
197
|
-
self.interval = interval
|
|
198
|
-
|
|
199
|
-
@override
|
|
200
|
-
def initialize(self, state: "State", name: str):
|
|
201
|
-
bound_method = self.fn.__get__(state, state.__class__)
|
|
202
|
-
# Select sync/async effect type based on bound method
|
|
203
|
-
if inspect.iscoroutinefunction(bound_method):
|
|
204
|
-
effect: Effect = AsyncEffect(
|
|
205
|
-
bound_method, # type: ignore[arg-type]
|
|
206
|
-
name=self.name or f"{state.__class__.__name__}.{name}",
|
|
207
|
-
lazy=self.lazy,
|
|
208
|
-
on_error=self.on_error,
|
|
209
|
-
deps=self.deps,
|
|
210
|
-
update_deps=self.update_deps,
|
|
211
|
-
interval=self.interval,
|
|
212
|
-
)
|
|
213
|
-
else:
|
|
214
|
-
effect = Effect(
|
|
215
|
-
bound_method, # type: ignore[arg-type]
|
|
216
|
-
name=self.name or f"{state.__class__.__name__}.{name}",
|
|
217
|
-
immediate=self.immediate,
|
|
218
|
-
lazy=self.lazy,
|
|
219
|
-
on_error=self.on_error,
|
|
220
|
-
deps=self.deps,
|
|
221
|
-
update_deps=self.update_deps,
|
|
222
|
-
interval=self.interval,
|
|
223
|
-
)
|
|
224
|
-
setattr(state, name, effect)
|
|
18
|
+
from pulse.state.property import (
|
|
19
|
+
ComputedProperty,
|
|
20
|
+
InitializableProperty,
|
|
21
|
+
StateEffect,
|
|
22
|
+
StateProperty,
|
|
23
|
+
)
|
|
24
|
+
from pulse.state.query_param import QueryParam, QueryParamProperty, extract_query_param
|
|
225
25
|
|
|
226
26
|
|
|
227
27
|
class StateMeta(ABCMeta):
|
|
@@ -258,18 +58,62 @@ class StateMeta(ABCMeta):
|
|
|
258
58
|
namespace: dict[str, Any],
|
|
259
59
|
**kwargs: Any,
|
|
260
60
|
):
|
|
261
|
-
|
|
61
|
+
declared_annotations = dict(namespace.get("__annotations__", {}))
|
|
62
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
63
|
+
resolved_annotations: dict[str, Any] = {}
|
|
64
|
+
if declared_annotations:
|
|
65
|
+
module = sys.modules.get(cls.__module__)
|
|
66
|
+
globalns = module.__dict__ if module else {}
|
|
67
|
+
if "QueryParam" not in globalns:
|
|
68
|
+
globalns["QueryParam"] = QueryParam
|
|
69
|
+
localns = dict(cls.__dict__)
|
|
70
|
+
try:
|
|
71
|
+
hints = get_type_hints(
|
|
72
|
+
cls,
|
|
73
|
+
globalns=globalns,
|
|
74
|
+
localns=localns,
|
|
75
|
+
)
|
|
76
|
+
except Exception:
|
|
77
|
+
hints = None
|
|
78
|
+
if hints is not None:
|
|
79
|
+
for key, value in declared_annotations.items():
|
|
80
|
+
resolved_annotations[key] = hints.get(key, value)
|
|
81
|
+
else:
|
|
82
|
+
for key, value in declared_annotations.items():
|
|
83
|
+
try:
|
|
84
|
+
holder = SimpleNamespace(__annotations__={key: value})
|
|
85
|
+
resolved = get_type_hints(
|
|
86
|
+
holder,
|
|
87
|
+
globalns=globalns,
|
|
88
|
+
localns=localns,
|
|
89
|
+
).get(key, value)
|
|
90
|
+
except Exception:
|
|
91
|
+
resolved = value
|
|
92
|
+
resolved_annotations[key] = resolved
|
|
262
93
|
|
|
263
94
|
# 1) Turn annotated fields into StateProperty descriptors
|
|
264
|
-
for attr_name in
|
|
95
|
+
for attr_name, annotation in resolved_annotations.items():
|
|
265
96
|
# Do not wrap private/dunder attributes as reactive
|
|
266
97
|
if attr_name.startswith("_"):
|
|
267
98
|
continue
|
|
268
|
-
default_value =
|
|
269
|
-
|
|
99
|
+
default_value = cls.__dict__.get(attr_name)
|
|
100
|
+
value_type, is_query_param = extract_query_param(annotation)
|
|
101
|
+
if is_query_param:
|
|
102
|
+
cls.__annotations__[attr_name] = value_type
|
|
103
|
+
prop = QueryParamProperty(
|
|
104
|
+
attr_name,
|
|
105
|
+
default_value,
|
|
106
|
+
value_type,
|
|
107
|
+
)
|
|
108
|
+
setattr(cls, attr_name, prop)
|
|
109
|
+
prop.__set_name__(cls, attr_name)
|
|
110
|
+
else:
|
|
111
|
+
prop = StateProperty(attr_name, default_value)
|
|
112
|
+
setattr(cls, attr_name, prop)
|
|
113
|
+
prop.__set_name__(cls, attr_name)
|
|
270
114
|
|
|
271
115
|
# 2) Turn non-annotated plain values into StateProperty descriptors
|
|
272
|
-
for attr_name, value in list(
|
|
116
|
+
for attr_name, value in list(cls.__dict__.items()):
|
|
273
117
|
# Do not wrap private/dunder attributes as reactive
|
|
274
118
|
if attr_name.startswith("_"):
|
|
275
119
|
continue
|
|
@@ -285,9 +129,11 @@ class StateMeta(ABCMeta):
|
|
|
285
129
|
):
|
|
286
130
|
continue
|
|
287
131
|
# Convert plain class var into a StateProperty
|
|
288
|
-
|
|
132
|
+
prop = StateProperty(attr_name, value)
|
|
133
|
+
setattr(cls, attr_name, prop)
|
|
134
|
+
prop.__set_name__(cls, attr_name)
|
|
289
135
|
|
|
290
|
-
return
|
|
136
|
+
return cls
|
|
291
137
|
|
|
292
138
|
@override
|
|
293
139
|
def __call__(cls, *args: Any, **kwargs: Any):
|
pulse/transpiler/nodes.py
CHANGED
|
@@ -1150,6 +1150,15 @@ class Unary(Expr):
|
|
|
1150
1150
|
out.append(" ")
|
|
1151
1151
|
else:
|
|
1152
1152
|
out.append(self.op)
|
|
1153
|
+
if (
|
|
1154
|
+
self.op in {"+", "-"}
|
|
1155
|
+
and isinstance(self.operand, Unary)
|
|
1156
|
+
and self.operand.op == self.op
|
|
1157
|
+
):
|
|
1158
|
+
out.append("(")
|
|
1159
|
+
self.operand.emit(out)
|
|
1160
|
+
out.append(")")
|
|
1161
|
+
return
|
|
1153
1162
|
_emit_paren(self.operand, self.op, "unary", out)
|
|
1154
1163
|
|
|
1155
1164
|
@override
|
|
@@ -1574,17 +1583,32 @@ class Assign(Stmt):
|
|
|
1574
1583
|
op: None for =, or "+", "-", etc. for augmented assignment
|
|
1575
1584
|
"""
|
|
1576
1585
|
|
|
1577
|
-
target: str
|
|
1586
|
+
target: str | Identifier | Member | Subscript
|
|
1578
1587
|
value: Expr
|
|
1579
1588
|
declare: Lit["let", "const"] | None = None
|
|
1580
1589
|
op: str | None = None # For augmented: +=, -=, etc.
|
|
1581
1590
|
|
|
1591
|
+
@staticmethod
|
|
1592
|
+
def _validate_target(target: object) -> None:
|
|
1593
|
+
if not isinstance(target, (str, Identifier, Member, Subscript)):
|
|
1594
|
+
raise TypeError(
|
|
1595
|
+
"Assign target must be str, Identifier, Member, or Subscript; "
|
|
1596
|
+
+ f"got {type(target).__name__}: {target!r}"
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
def __post_init__(self) -> None:
|
|
1600
|
+
self._validate_target(self.target)
|
|
1601
|
+
|
|
1582
1602
|
@override
|
|
1583
1603
|
def emit(self, out: list[str]) -> None:
|
|
1604
|
+
self._validate_target(self.target)
|
|
1584
1605
|
if self.declare:
|
|
1585
1606
|
out.append(self.declare)
|
|
1586
1607
|
out.append(" ")
|
|
1587
|
-
|
|
1608
|
+
if isinstance(self.target, str):
|
|
1609
|
+
out.append(self.target)
|
|
1610
|
+
else:
|
|
1611
|
+
_emit_primary(self.target, out)
|
|
1588
1612
|
if self.op:
|
|
1589
1613
|
out.append(" ")
|
|
1590
1614
|
out.append(self.op)
|
pulse/transpiler/transpiler.py
CHANGED
|
@@ -253,6 +253,10 @@ class Transpiler:
|
|
|
253
253
|
return Block([])
|
|
254
254
|
|
|
255
255
|
if isinstance(node, ast.AugAssign):
|
|
256
|
+
if isinstance(node.target, ast.Subscript):
|
|
257
|
+
return self._emit_augmented_subscript_assign(node)
|
|
258
|
+
if isinstance(node.target, ast.Attribute):
|
|
259
|
+
return self._emit_augmented_attribute_assign(node)
|
|
256
260
|
if not isinstance(node.target, ast.Name):
|
|
257
261
|
raise TranspileError(
|
|
258
262
|
"Only simple augmented assignments supported", node=node
|
|
@@ -278,6 +282,12 @@ class Transpiler:
|
|
|
278
282
|
if isinstance(target_node, (ast.Tuple, ast.List)):
|
|
279
283
|
return self._emit_unpacking_assign(target_node, node.value)
|
|
280
284
|
|
|
285
|
+
if isinstance(target_node, ast.Subscript):
|
|
286
|
+
return self._emit_subscript_assign(target_node, node.value)
|
|
287
|
+
|
|
288
|
+
if isinstance(target_node, ast.Attribute):
|
|
289
|
+
return self._emit_attribute_assign(target_node, node.value)
|
|
290
|
+
|
|
281
291
|
if not isinstance(target_node, ast.Name):
|
|
282
292
|
raise TranspileError(
|
|
283
293
|
"Only simple assignments to local names supported", node=node
|
|
@@ -356,6 +366,82 @@ class Transpiler:
|
|
|
356
366
|
|
|
357
367
|
return StmtSequence(stmts)
|
|
358
368
|
|
|
369
|
+
def _emit_subscript_assign(self, target: ast.Subscript, value: ast.expr) -> Stmt:
|
|
370
|
+
"""Emit subscript assignment: obj[key] = value"""
|
|
371
|
+
if isinstance(target.slice, ast.Tuple):
|
|
372
|
+
raise TranspileError(
|
|
373
|
+
"Multiple indices not supported in subscript", node=target.slice
|
|
374
|
+
)
|
|
375
|
+
if isinstance(target.slice, ast.Slice):
|
|
376
|
+
raise TranspileError("Slice assignment not supported", node=target.slice)
|
|
377
|
+
obj_expr = self.emit_expr(target.value)
|
|
378
|
+
target_expr = obj_expr.transpile_subscript(target.slice, self)
|
|
379
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
380
|
+
raise TranspileError(
|
|
381
|
+
"Only simple subscript assignments supported", node=target
|
|
382
|
+
)
|
|
383
|
+
value_expr = self.emit_expr(value)
|
|
384
|
+
return Assign(target_expr, value_expr)
|
|
385
|
+
|
|
386
|
+
def _emit_attribute_assign(self, target: ast.Attribute, value: ast.expr) -> Stmt:
|
|
387
|
+
"""Emit attribute assignment: obj.attr = value"""
|
|
388
|
+
obj_expr = self.emit_expr(target.value)
|
|
389
|
+
value_expr = self.emit_expr(value)
|
|
390
|
+
target_expr = obj_expr.transpile_getattr(target.attr, self)
|
|
391
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
392
|
+
raise TranspileError(
|
|
393
|
+
"Only simple attribute assignments supported", node=target
|
|
394
|
+
)
|
|
395
|
+
return Assign(target_expr, value_expr)
|
|
396
|
+
|
|
397
|
+
def _emit_augmented_subscript_assign(self, node: ast.AugAssign) -> Stmt:
|
|
398
|
+
"""Emit augmented subscript assignment: arr[i] += x"""
|
|
399
|
+
target = node.target
|
|
400
|
+
assert isinstance(target, ast.Subscript)
|
|
401
|
+
|
|
402
|
+
if isinstance(target.slice, ast.Tuple):
|
|
403
|
+
raise TranspileError(
|
|
404
|
+
"Multiple indices not supported in subscript", node=target.slice
|
|
405
|
+
)
|
|
406
|
+
if isinstance(target.slice, ast.Slice):
|
|
407
|
+
raise TranspileError("Slice assignment not supported", node=target.slice)
|
|
408
|
+
|
|
409
|
+
obj_expr = self.emit_expr(target.value)
|
|
410
|
+
op_type = type(node.op)
|
|
411
|
+
if op_type not in ALLOWED_BINOPS:
|
|
412
|
+
raise TranspileError(
|
|
413
|
+
f"Unsupported augmented assignment operator: {op_type.__name__}",
|
|
414
|
+
node=node,
|
|
415
|
+
)
|
|
416
|
+
target_expr = obj_expr.transpile_subscript(target.slice, self)
|
|
417
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
418
|
+
raise TranspileError(
|
|
419
|
+
"Only simple subscript assignments supported", node=target
|
|
420
|
+
)
|
|
421
|
+
value_expr = self.emit_expr(node.value)
|
|
422
|
+
return Assign(target_expr, value_expr, op=ALLOWED_BINOPS[op_type])
|
|
423
|
+
|
|
424
|
+
def _emit_augmented_attribute_assign(self, node: ast.AugAssign) -> Stmt:
|
|
425
|
+
"""Emit augmented attribute assignment: obj.attr += x"""
|
|
426
|
+
target = node.target
|
|
427
|
+
assert isinstance(target, ast.Attribute)
|
|
428
|
+
|
|
429
|
+
obj_expr = self.emit_expr(target.value)
|
|
430
|
+
op_type = type(node.op)
|
|
431
|
+
if op_type not in ALLOWED_BINOPS:
|
|
432
|
+
raise TranspileError(
|
|
433
|
+
f"Unsupported augmented assignment operator: {op_type.__name__}",
|
|
434
|
+
node=node,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
target_expr = obj_expr.transpile_getattr(target.attr, self)
|
|
438
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
439
|
+
raise TranspileError(
|
|
440
|
+
"Only simple attribute assignments supported", node=target
|
|
441
|
+
)
|
|
442
|
+
value_expr = self.emit_expr(node.value)
|
|
443
|
+
return Assign(target_expr, value_expr, op=ALLOWED_BINOPS[op_type])
|
|
444
|
+
|
|
359
445
|
def _emit_for_loop(self, node: ast.For) -> Stmt:
|
|
360
446
|
"""Emit a for loop."""
|
|
361
447
|
# Handle tuple unpacking in for target
|
|
@@ -772,11 +858,6 @@ class Transpiler:
|
|
|
772
858
|
if isinstance(node.slice, ast.Slice):
|
|
773
859
|
return self._emit_slice(value, node.slice)
|
|
774
860
|
|
|
775
|
-
# Negative index: use .at()
|
|
776
|
-
if isinstance(node.slice, ast.UnaryOp) and isinstance(node.slice.op, ast.USub):
|
|
777
|
-
idx_expr = self.emit_expr(node.slice.operand)
|
|
778
|
-
return Call(Member(value, "at"), [Unary("-", idx_expr)])
|
|
779
|
-
|
|
780
861
|
# Delegate to Expr.transpile_subscript (default returns Subscript)
|
|
781
862
|
return value.transpile_subscript(node.slice, self)
|
|
782
863
|
|
pulse/transpiler/vdom.py
CHANGED
|
@@ -157,7 +157,7 @@ CallbackPlaceholder: TypeAlias = Literal["$cb"]
|
|
|
157
157
|
|
|
158
158
|
The callback invocation target is derived from the element path + prop name.
|
|
159
159
|
Because the prop name is known from `VDOMElement.eval`, the placeholder can be a
|
|
160
|
-
single sentinel string.
|
|
160
|
+
single sentinel string. Debounced callbacks use "$cb:<delay_ms>" in the wire format.
|
|
161
161
|
"""
|
|
162
162
|
|
|
163
163
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pulse-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.73
|
|
4
4
|
Summary: Pulse - Full-stack framework for building real-time React applications in Python
|
|
5
5
|
Requires-Dist: fastapi>=0.128.0
|
|
6
6
|
Requires-Dist: uvicorn>=0.24.0
|
|
@@ -15,7 +15,7 @@ Requires-Dist: urllib3>=2.6.3
|
|
|
15
15
|
Requires-Dist: watchfiles>=1.1.0
|
|
16
16
|
Requires-Dist: httpx>=0.28.1
|
|
17
17
|
Requires-Dist: aiohttp>=3.12.0
|
|
18
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# Pulse Python
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
pulse/__init__.py,sha256=
|
|
1
|
+
pulse/__init__.py,sha256=GUPD8THGENBgHsZvixIP8wOiEtdEMtIVpr8N8MpuRL4,32675
|
|
2
2
|
pulse/_examples.py,sha256=dFuhD2EVXsbvAeexoG57s4VuN4gWLaTMOEMNYvlPm9A,561
|
|
3
3
|
pulse/app.py,sha256=Bi94rYG-MoldkGa-_CscLMstjTEV8BHVAgDbvapRGzI,36167
|
|
4
4
|
pulse/channel.py,sha256=ePpvD2mDbddt_LMxxxDjNRgOLbVi8Ed6TmJFgkrALB0,15790
|
|
@@ -10,7 +10,7 @@ pulse/cli/helpers.py,sha256=XXRRXeGFgeq-jbp0QGFFVq_aGg_Kp7_AkYsTK8LfSdg,7810
|
|
|
10
10
|
pulse/cli/logging.py,sha256=3uuB1dqI-lHJkodNUURN6UMWdKF5UQ9spNG-hBG7bA4,2516
|
|
11
11
|
pulse/cli/models.py,sha256=NBV5byBDNoAQSk0vKwibLjoxuA85XBYIyOVJn64L8oU,858
|
|
12
12
|
pulse/cli/packages.py,sha256=DSnhxz61AoLVvBre3c0dnVYSpiKPI0rKFq4YmgM_VlA,7220
|
|
13
|
-
pulse/cli/processes.py,sha256=
|
|
13
|
+
pulse/cli/processes.py,sha256=rtMTpl0e3Wx1IL_Kx7b6Loflst1HXW4Wm7XGk6N2-xc,7634
|
|
14
14
|
pulse/cli/secrets.py,sha256=dNfQe6AzSYhZuWveesjCRHIbvaPd3-F9lEJ-kZA7ROw,921
|
|
15
15
|
pulse/cli/uvicorn_log_config.py,sha256=f7ikDc5foXh3TmFMrnfnW8yev48ZAdlo8F4F_aMVoVk,2391
|
|
16
16
|
pulse/code_analysis.py,sha256=NBba_7VtOxZYMyfku_p-bWkG0O_1pi1AxcaNyVM1nmY,1024
|
|
@@ -28,7 +28,8 @@ pulse/components/if_.py,sha256=5IOq3R70B-JdI-fvDNYDyAaSEtO8L5OaiqHp-jUn-Kw,2153
|
|
|
28
28
|
pulse/components/react_router.py,sha256=Nl6juntLSowFc38q7g_VMdcc4ju6lj8DUhpNR2NuOKQ,2934
|
|
29
29
|
pulse/context.py,sha256=odTQlOhVRIwNvtatrmPe_Fd8Zk0rMcbcqQHBxvWYH5o,2677
|
|
30
30
|
pulse/cookies.py,sha256=ozfdBKExdbpeM5ileIA1z8BZA5hoUrZ5_iO9fIMrgRk,8768
|
|
31
|
-
pulse/
|
|
31
|
+
pulse/debounce.py,sha256=IUi5TAjPfavGXKJ2oQoJPwvBmeqnEf0Tsu29FFdFYJk,2262
|
|
32
|
+
pulse/decorators.py,sha256=Lskni9Keqfb-xmUliFQe5x-4AcNqrwdvoh0kuz2fXa0,9958
|
|
32
33
|
pulse/dom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
34
|
pulse/dom/elements.py,sha256=YHXkVpfMAC4-0o61fK-E0LGTOM3KMCtBfpHHAwLx7dw,23241
|
|
34
35
|
pulse/dom/events.py,sha256=yHioH8Y-b7raOaZ43JuCxk2lUBryUAcDSc-5VhXtiSI,14699
|
|
@@ -41,12 +42,12 @@ pulse/forms.py,sha256=0irpErCMJk8-YO1BrxjMkFb8dnvSz3rfzTywmMeib7g,14042
|
|
|
41
42
|
pulse/helpers.py,sha256=imVA9XzkYrYmdeqEdD7ot0g99adL1SVKv5bQGkKb-aQ,9504
|
|
42
43
|
pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
44
|
pulse/hooks/core.py,sha256=tDEcB_CTD4yI5bNKn7CtB40sRKIanGNqPD5_qLgSzf4,10982
|
|
44
|
-
pulse/hooks/effects.py,sha256=
|
|
45
|
+
pulse/hooks/effects.py,sha256=KjAS8CgDVWhM6vbEpzL87TKK9q7K_MVZFt7k_f0HF9M,2961
|
|
45
46
|
pulse/hooks/init.py,sha256=PhjVBLlHpodPzVrRcx_QfEUrsx_6gEX_NuVhe6ojiYI,17834
|
|
46
|
-
pulse/hooks/runtime.py,sha256=
|
|
47
|
-
pulse/hooks/setup.py,sha256=
|
|
48
|
-
pulse/hooks/stable.py,sha256=
|
|
49
|
-
pulse/hooks/state.py,sha256=
|
|
47
|
+
pulse/hooks/runtime.py,sha256=ogrm4Prvr9ZNaBb5bLfZBHrzbJuYe1zKpIPkbdnIzsw,12286
|
|
48
|
+
pulse/hooks/setup.py,sha256=NcQPKnMV5dO0vUsWi4u9c9LB0wqFstrtiPGdvihtGiQ,6872
|
|
49
|
+
pulse/hooks/stable.py,sha256=hLCOl_oAbgdNwiaWwwUTk7ZsHvMqXFFozFztJZMyGbQ,3627
|
|
50
|
+
pulse/hooks/state.py,sha256=zRFlcOUdl-SBkJ-EzVXRLrLXzAc4-uRzgH4hD9rM1oU,5251
|
|
50
51
|
pulse/js/__init__.py,sha256=tj1A6-eR5WS83UNgHb3Dw23m37oJsEuyV0ezUB6kXbg,3636
|
|
51
52
|
pulse/js/__init__.pyi,sha256=WN22WsJB-XFk6auL9zklwG2Kof3zeOsc56A56dJ3MWg,3097
|
|
52
53
|
pulse/js/_types.py,sha256=F4Go2JtJ2dbxq1fXpc2ablG_nyvhvHzOlZLlEv0VmyU,7421
|
|
@@ -78,24 +79,27 @@ pulse/proxy.py,sha256=c13b0fE3sq82sFo46vv0emWLQ_ePwRkI7hiPZrnQDCE,22780
|
|
|
78
79
|
pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
80
|
pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
81
|
pulse/queries/client.py,sha256=KMGT92dESMrzpLlhd701fyh7Wrs3VKmM5cZoRQ0AEzg,18994
|
|
81
|
-
pulse/queries/common.py,sha256=
|
|
82
|
+
pulse/queries/common.py,sha256=Adz_qDEnHHKAb0L2tVwSc14J0PXghhYw6B3AO2_Ud-E,4284
|
|
82
83
|
pulse/queries/effect.py,sha256=1ePUi2TwP49L9LhlkKI2qV_HhIO4jKj1r5jyPaWiUn8,1508
|
|
83
|
-
pulse/queries/infinite_query.py,sha256=
|
|
84
|
-
pulse/queries/mutation.py,sha256=
|
|
84
|
+
pulse/queries/infinite_query.py,sha256=YrBI1TlgQ7edBs-LbY6R57fMPz8rj23uKHYHfsv0kp4,50527
|
|
85
|
+
pulse/queries/mutation.py,sha256=CrVFPrLJ7TNYOP5Sa-U9JwropxLfTFSxJiKZVNOE9lM,8312
|
|
85
86
|
pulse/queries/protocol.py,sha256=TOrUiI4QK55xuh0i4ch1u96apNl12QeYafkf6RVDd08,3544
|
|
86
|
-
pulse/queries/query.py,sha256=
|
|
87
|
+
pulse/queries/query.py,sha256=67hohYL1Gj2RdsNxTs-GfoN_hhpmQvpjPtPVYL2d5XI,41887
|
|
87
88
|
pulse/queries/store.py,sha256=iw05_EFpyfiXv5_FV_x4aHtCo00mk0dDPFD461cajcg,3850
|
|
88
89
|
pulse/react_component.py,sha256=8RLg4Bi7IcjqbnbEnp4hJpy8t1UsE7mG0UR1Q655LDk,2332
|
|
89
90
|
pulse/reactive.py,sha256=GSh9wSH3THCBjDTafwWttyx7djeKBWV_KqjaKRYUNsA,31393
|
|
90
91
|
pulse/reactive_extensions.py,sha256=yQ1PpdAh4kMvll7R15T72FOg8NFdG_HGBsGc63dawYk,33754
|
|
91
|
-
pulse/render_session.py,sha256=
|
|
92
|
-
pulse/renderer.py,sha256=
|
|
92
|
+
pulse/render_session.py,sha256=WKWDOqtIjy9n00HxMiViI-pBHw34QOEhLgZap28BCMg,23431
|
|
93
|
+
pulse/renderer.py,sha256=a4gTEFZuhAc1V5uTcFFcsOREDg6ZU9-jf4Ic7qLo2CY,16902
|
|
93
94
|
pulse/request.py,sha256=N0oFOLiGxpbgSgxznjvu64lG3YyOcZPKC8JFyKx6X7w,6023
|
|
94
95
|
pulse/requirements.py,sha256=nMnE25Uu-TUuQd88jW7m2xwus6fD-HvXxQ9UNb7OOGc,1254
|
|
95
|
-
pulse/routing.py,sha256=
|
|
96
|
+
pulse/routing.py,sha256=oRfVaeIrsbDR9yW9BYwxVWV3HZI7wk21yZX69IVADIU,17279
|
|
96
97
|
pulse/scheduling.py,sha256=D-L5mVsbakK_-1NCq62dU6wEJpq6I_HxI4PCmR9aj9w,11714
|
|
97
|
-
pulse/serializer.py,sha256=
|
|
98
|
-
pulse/state.py,sha256=
|
|
98
|
+
pulse/serializer.py,sha256=Fz02bUVdpUwI4tBW44naFKBlFcujfyh5ykpwxs5XjAU,8218
|
|
99
|
+
pulse/state/__init__.py,sha256=oipHut-JH15hWrxeo3JpKH1YqDHNc4-xAbSqQ1Yt-Pk,29
|
|
100
|
+
pulse/state/property.py,sha256=pitudvCGBNqK6lePdPrs4Inyy7szo8qcR_0BI30veek,6274
|
|
101
|
+
pulse/state/query_param.py,sha256=7faf244_KJ3yrsBimKTBlNLg6wP96FUuWoZl40oxpyY,14478
|
|
102
|
+
pulse/state/state.py,sha256=_4DcB-og2s5Ui01kDx9HzpNwmAVD_pdCLB0TfYbcnzM,11603
|
|
99
103
|
pulse/test_helpers.py,sha256=4iO5Ymy3SMvSjh-UaAaSdqm1I_SAJMNjdY2iYVro5f8,436
|
|
100
104
|
pulse/transpiler/__init__.py,sha256=wDDnzqxgHpp_OLtcgyrJEg2jVoTnFIe3SSSTOsMDW8w,4700
|
|
101
105
|
pulse/transpiler/assets.py,sha256=digd5hKYPEgLOzMtDBHULX3Adj1sfngdvnx3quQmgPY,2299
|
|
@@ -114,15 +118,15 @@ pulse/transpiler/modules/math.py,sha256=8gjvdYTMqtuOnXrvX_Lwuo0ywAdSl7cpss4TMk6m
|
|
|
114
118
|
pulse/transpiler/modules/pulse/__init__.py,sha256=TfMsiiB53ZFlxdNl7jfCAiMZs-vSRUTxUmqzkLTj-po,91
|
|
115
119
|
pulse/transpiler/modules/pulse/tags.py,sha256=FMN1mWMlnsXa2qO6VmXxUAhFn1uOfGoKPQOjH4ZPlRE,6218
|
|
116
120
|
pulse/transpiler/modules/typing.py,sha256=J9QCkXE6zzwMjiprX2q1BtK-iKLIiS21sQ78JH4RSMc,1716
|
|
117
|
-
pulse/transpiler/nodes.py,sha256=
|
|
121
|
+
pulse/transpiler/nodes.py,sha256=vebA81QXkWoJMygX2CHH_94azKPRATaB6eBs5wdX4rE,52420
|
|
118
122
|
pulse/transpiler/py_module.py,sha256=um4BYLrbs01bpgv2LEBHTbhXXh8Bs174c3ygv5tHHOg,4410
|
|
119
|
-
pulse/transpiler/transpiler.py,sha256=
|
|
120
|
-
pulse/transpiler/vdom.py,sha256=
|
|
123
|
+
pulse/transpiler/transpiler.py,sha256=pxNFVnJB_zpUnp12cfzeOqUINAb1ZKhlU2E1gYUE6mk,35752
|
|
124
|
+
pulse/transpiler/vdom.py,sha256=Bf1yw10hQl8BXa6rhr5byRa5ua3qgRsVGNgEtQneA2A,6460
|
|
121
125
|
pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
122
126
|
pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
|
|
123
127
|
pulse/user_session.py,sha256=nsnsMgqq2xGJZLpbHRMHUHcLrElMP8WcA4gjGMrcoBk,10208
|
|
124
128
|
pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
|
|
125
|
-
pulse_framework-0.1.
|
|
126
|
-
pulse_framework-0.1.
|
|
127
|
-
pulse_framework-0.1.
|
|
128
|
-
pulse_framework-0.1.
|
|
129
|
+
pulse_framework-0.1.73.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
130
|
+
pulse_framework-0.1.73.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
|
|
131
|
+
pulse_framework-0.1.73.dist-info/METADATA,sha256=PIqxEHW98zMJlT1C_cLNisDzX73GDvGKFW8e4UtVTfk,8299
|
|
132
|
+
pulse_framework-0.1.73.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|