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.
- pulse/__init__.py +1493 -0
- pulse/_examples.py +29 -0
- pulse/app.py +1086 -0
- pulse/channel.py +607 -0
- pulse/cli/__init__.py +0 -0
- pulse/cli/cmd.py +575 -0
- pulse/cli/dependencies.py +181 -0
- pulse/cli/folder_lock.py +134 -0
- pulse/cli/helpers.py +271 -0
- pulse/cli/logging.py +102 -0
- pulse/cli/models.py +35 -0
- pulse/cli/packages.py +262 -0
- pulse/cli/processes.py +292 -0
- pulse/cli/secrets.py +39 -0
- pulse/cli/uvicorn_log_config.py +87 -0
- pulse/code_analysis.py +38 -0
- pulse/codegen/__init__.py +0 -0
- pulse/codegen/codegen.py +359 -0
- pulse/codegen/templates/__init__.py +0 -0
- pulse/codegen/templates/layout.py +106 -0
- pulse/codegen/templates/route.py +345 -0
- pulse/codegen/templates/routes_ts.py +42 -0
- pulse/codegen/utils.py +20 -0
- pulse/component.py +237 -0
- pulse/components/__init__.py +0 -0
- pulse/components/for_.py +83 -0
- pulse/components/if_.py +86 -0
- pulse/components/react_router.py +94 -0
- pulse/context.py +108 -0
- pulse/cookies.py +322 -0
- pulse/decorators.py +344 -0
- pulse/dom/__init__.py +0 -0
- pulse/dom/elements.py +1024 -0
- pulse/dom/events.py +445 -0
- pulse/dom/props.py +1250 -0
- pulse/dom/svg.py +0 -0
- pulse/dom/tags.py +328 -0
- pulse/dom/tags.pyi +480 -0
- pulse/env.py +178 -0
- pulse/form.py +538 -0
- pulse/helpers.py +541 -0
- pulse/hooks/__init__.py +0 -0
- pulse/hooks/core.py +452 -0
- pulse/hooks/effects.py +88 -0
- pulse/hooks/init.py +668 -0
- pulse/hooks/runtime.py +464 -0
- pulse/hooks/setup.py +254 -0
- pulse/hooks/stable.py +138 -0
- pulse/hooks/state.py +192 -0
- pulse/js/__init__.py +125 -0
- pulse/js/__init__.pyi +115 -0
- pulse/js/_types.py +299 -0
- pulse/js/array.py +339 -0
- pulse/js/console.py +50 -0
- pulse/js/date.py +119 -0
- pulse/js/document.py +145 -0
- pulse/js/error.py +140 -0
- pulse/js/json.py +66 -0
- pulse/js/map.py +97 -0
- pulse/js/math.py +69 -0
- pulse/js/navigator.py +79 -0
- pulse/js/number.py +57 -0
- pulse/js/obj.py +81 -0
- pulse/js/object.py +172 -0
- pulse/js/promise.py +172 -0
- pulse/js/pulse.py +115 -0
- pulse/js/react.py +495 -0
- pulse/js/regexp.py +57 -0
- pulse/js/set.py +124 -0
- pulse/js/string.py +38 -0
- pulse/js/weakmap.py +53 -0
- pulse/js/weakset.py +48 -0
- pulse/js/window.py +205 -0
- pulse/messages.py +202 -0
- pulse/middleware.py +471 -0
- pulse/plugin.py +96 -0
- pulse/proxy.py +242 -0
- pulse/py.typed +0 -0
- pulse/queries/__init__.py +0 -0
- pulse/queries/client.py +609 -0
- pulse/queries/common.py +101 -0
- pulse/queries/effect.py +55 -0
- pulse/queries/infinite_query.py +1418 -0
- pulse/queries/mutation.py +295 -0
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +1314 -0
- pulse/queries/store.py +120 -0
- pulse/react_component.py +88 -0
- pulse/reactive.py +1208 -0
- pulse/reactive_extensions.py +1172 -0
- pulse/render_session.py +768 -0
- pulse/renderer.py +584 -0
- pulse/request.py +205 -0
- pulse/routing.py +598 -0
- pulse/serializer.py +279 -0
- pulse/state.py +556 -0
- pulse/test_helpers.py +15 -0
- pulse/transpiler/__init__.py +111 -0
- pulse/transpiler/assets.py +81 -0
- pulse/transpiler/builtins.py +1029 -0
- pulse/transpiler/dynamic_import.py +130 -0
- pulse/transpiler/emit_context.py +49 -0
- pulse/transpiler/errors.py +96 -0
- pulse/transpiler/function.py +611 -0
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +341 -0
- pulse/transpiler/js_module.py +336 -0
- pulse/transpiler/modules/__init__.py +33 -0
- pulse/transpiler/modules/asyncio.py +57 -0
- pulse/transpiler/modules/json.py +24 -0
- pulse/transpiler/modules/math.py +265 -0
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +250 -0
- pulse/transpiler/modules/typing.py +63 -0
- pulse/transpiler/nodes.py +1987 -0
- pulse/transpiler/py_module.py +135 -0
- pulse/transpiler/transpiler.py +1100 -0
- pulse/transpiler/vdom.py +256 -0
- pulse/types/__init__.py +0 -0
- pulse/types/event_handler.py +50 -0
- pulse/user_session.py +386 -0
- pulse/version.py +69 -0
- pulse_framework-0.1.62.dist-info/METADATA +198 -0
- pulse_framework-0.1.62.dist-info/RECORD +126 -0
- pulse_framework-0.1.62.dist-info/WHEEL +4 -0
- pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
pulse/decorators.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# Separate file from reactive.py due to needing to import from state too
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any, ParamSpec, Protocol, TypeVar, cast, overload
|
|
6
|
+
|
|
7
|
+
from pulse.hooks.core import HOOK_CONTEXT
|
|
8
|
+
from pulse.hooks.effects import inline_effect_hook
|
|
9
|
+
from pulse.hooks.state import collect_component_identity
|
|
10
|
+
from pulse.reactive import (
|
|
11
|
+
AsyncEffect,
|
|
12
|
+
AsyncEffectFn,
|
|
13
|
+
Computed,
|
|
14
|
+
Effect,
|
|
15
|
+
EffectCleanup,
|
|
16
|
+
EffectFn,
|
|
17
|
+
Signal,
|
|
18
|
+
)
|
|
19
|
+
from pulse.state import ComputedProperty, State, StateEffect
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
TState = TypeVar("TState", bound=State)
|
|
23
|
+
P = ParamSpec("P")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def computed(fn: Callable[[], T], *, name: str | None = None) -> Computed[T]: ...
|
|
28
|
+
@overload
|
|
29
|
+
def computed(
|
|
30
|
+
fn: Callable[[TState], T], *, name: str | None = None
|
|
31
|
+
) -> ComputedProperty[T]: ...
|
|
32
|
+
@overload
|
|
33
|
+
def computed(
|
|
34
|
+
fn: None = None, *, name: str | None = None
|
|
35
|
+
) -> Callable[[Callable[[], T]], Computed[T]]: ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def computed(
|
|
39
|
+
fn: Callable[..., Any] | None = None,
|
|
40
|
+
*,
|
|
41
|
+
name: str | None = None,
|
|
42
|
+
) -> (
|
|
43
|
+
Computed[T]
|
|
44
|
+
| ComputedProperty[T]
|
|
45
|
+
| Callable[[Callable[..., Any]], Computed[T] | ComputedProperty[T]]
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Decorator for computed (derived) properties.
|
|
49
|
+
|
|
50
|
+
Creates a cached, reactive value that automatically recalculates when its
|
|
51
|
+
dependencies change. The computed tracks which Signals/Computeds are read
|
|
52
|
+
during execution and subscribes to them.
|
|
53
|
+
|
|
54
|
+
Can be used in two ways:
|
|
55
|
+
1. On a State method (with single `self` argument) - creates a ComputedProperty
|
|
56
|
+
2. As a standalone function (with no arguments) - creates a Computed
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
fn: The function to compute the value. Must take no arguments (standalone) or only `self` (State method).
|
|
60
|
+
name: Optional debug name for the computed. Defaults to the function name.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Computed wrapper or decorator depending on usage.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
TypeError: If the function takes arguments other than `self`.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
On a State method:
|
|
70
|
+
|
|
71
|
+
class MyState(ps.State):
|
|
72
|
+
count: int = 0
|
|
73
|
+
|
|
74
|
+
@ps.computed
|
|
75
|
+
def doubled(self):
|
|
76
|
+
return self.count * 2
|
|
77
|
+
|
|
78
|
+
As a standalone computed:
|
|
79
|
+
|
|
80
|
+
signal = Signal(5)
|
|
81
|
+
|
|
82
|
+
@ps.computed
|
|
83
|
+
def doubled():
|
|
84
|
+
return signal() * 2
|
|
85
|
+
|
|
86
|
+
With explicit name:
|
|
87
|
+
|
|
88
|
+
@ps.computed(name="my_computed")
|
|
89
|
+
def doubled(self):
|
|
90
|
+
return self.count * 2
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# The type checker is not happy if I don't specify the `/` here.
|
|
94
|
+
def decorator(fn: Callable[..., Any], /):
|
|
95
|
+
sig = inspect.signature(fn)
|
|
96
|
+
params = list(sig.parameters.values())
|
|
97
|
+
# Check if it's a method with exactly one argument called 'self'
|
|
98
|
+
if len(params) == 1 and params[0].name == "self":
|
|
99
|
+
return ComputedProperty(fn.__name__, fn)
|
|
100
|
+
# If it has any arguments at all, it's not allowed (except for 'self')
|
|
101
|
+
if len(params) > 0:
|
|
102
|
+
raise TypeError(
|
|
103
|
+
f"@computed: Function '{fn.__name__}' must take no arguments or a single 'self' argument"
|
|
104
|
+
)
|
|
105
|
+
return Computed(fn, name=name or fn.__name__)
|
|
106
|
+
|
|
107
|
+
if fn is not None:
|
|
108
|
+
return decorator(fn)
|
|
109
|
+
else:
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
StateEffectFn = Callable[[TState], EffectCleanup | None]
|
|
114
|
+
AsyncStateEffectFn = Callable[[TState], Awaitable[EffectCleanup | None]]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class EffectBuilder(Protocol):
|
|
118
|
+
@overload
|
|
119
|
+
def __call__(self, fn: EffectFn | StateEffectFn[Any]) -> Effect: ...
|
|
120
|
+
@overload
|
|
121
|
+
def __call__(self, fn: AsyncEffectFn | AsyncStateEffectFn[Any]) -> AsyncEffect: ...
|
|
122
|
+
def __call__(
|
|
123
|
+
self,
|
|
124
|
+
fn: EffectFn | StateEffectFn[Any] | AsyncEffectFn | AsyncStateEffectFn[Any],
|
|
125
|
+
) -> Effect | AsyncEffect: ...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@overload
|
|
129
|
+
def effect(
|
|
130
|
+
fn: EffectFn,
|
|
131
|
+
*,
|
|
132
|
+
name: str | None = None,
|
|
133
|
+
immediate: bool = False,
|
|
134
|
+
lazy: bool = False,
|
|
135
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
136
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
137
|
+
update_deps: bool | None = None,
|
|
138
|
+
interval: float | None = None,
|
|
139
|
+
key: str | None = None,
|
|
140
|
+
) -> Effect: ...
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@overload
|
|
144
|
+
def effect(
|
|
145
|
+
fn: AsyncEffectFn,
|
|
146
|
+
*,
|
|
147
|
+
name: str | None = None,
|
|
148
|
+
immediate: bool = False,
|
|
149
|
+
lazy: bool = False,
|
|
150
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
151
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
152
|
+
update_deps: bool | None = None,
|
|
153
|
+
interval: float | None = None,
|
|
154
|
+
key: str | None = None,
|
|
155
|
+
) -> AsyncEffect: ...
|
|
156
|
+
# In practice this overload returns a StateEffect, but it gets converted into an
|
|
157
|
+
# Effect at state instantiation.
|
|
158
|
+
@overload
|
|
159
|
+
def effect(fn: StateEffectFn[Any]) -> Effect: ...
|
|
160
|
+
@overload
|
|
161
|
+
def effect(fn: AsyncStateEffectFn[Any]) -> AsyncEffect: ...
|
|
162
|
+
@overload
|
|
163
|
+
def effect(
|
|
164
|
+
fn: None = None,
|
|
165
|
+
*,
|
|
166
|
+
name: str | None = None,
|
|
167
|
+
immediate: bool = False,
|
|
168
|
+
lazy: bool = False,
|
|
169
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
170
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
171
|
+
update_deps: bool | None = None,
|
|
172
|
+
interval: float | None = None,
|
|
173
|
+
key: str | None = None,
|
|
174
|
+
) -> EffectBuilder: ...
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def effect(
|
|
178
|
+
fn: Callable[..., Any] | None = None,
|
|
179
|
+
*,
|
|
180
|
+
name: str | None = None,
|
|
181
|
+
immediate: bool = False,
|
|
182
|
+
lazy: bool = False,
|
|
183
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
184
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
185
|
+
update_deps: bool | None = None,
|
|
186
|
+
interval: float | None = None,
|
|
187
|
+
key: str | None = None,
|
|
188
|
+
):
|
|
189
|
+
"""
|
|
190
|
+
Decorator for side effects that run when dependencies change.
|
|
191
|
+
|
|
192
|
+
Creates an effect that automatically re-runs when any of its tracked
|
|
193
|
+
dependencies change. Dependencies are automatically tracked by observing
|
|
194
|
+
which Signals/Computeds are read during execution.
|
|
195
|
+
|
|
196
|
+
Can be used in two ways:
|
|
197
|
+
1. On a State method (with single `self` argument) - creates a StateEffect
|
|
198
|
+
2. As a standalone function (with no arguments) - creates an Effect
|
|
199
|
+
|
|
200
|
+
Supports both sync and async functions. Async effects cannot use `immediate=True`.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
fn: The effect function. Must take no arguments (standalone) or only
|
|
204
|
+
`self` (State method). Can return a cleanup function.
|
|
205
|
+
name: Optional debug name. Defaults to "ClassName.method_name" or function name.
|
|
206
|
+
immediate: If True, run synchronously when scheduled instead of batching.
|
|
207
|
+
Only valid for sync effects.
|
|
208
|
+
lazy: If True, don't run on creation; wait for first dependency change.
|
|
209
|
+
on_error: Callback invoked if the effect throws an exception.
|
|
210
|
+
deps: Explicit list of dependencies. If provided, auto-tracking is disabled
|
|
211
|
+
and the effect only re-runs when these specific dependencies change.
|
|
212
|
+
interval: Re-run interval in seconds. Creates a polling effect that runs
|
|
213
|
+
periodically regardless of dependency changes.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Effect, AsyncEffect, or StateEffect depending on usage.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
TypeError: If the function takes arguments other than `self`.
|
|
220
|
+
ValueError: If `immediate=True` is used with an async function.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
State method effect:
|
|
224
|
+
|
|
225
|
+
class MyState(ps.State):
|
|
226
|
+
count: int = 0
|
|
227
|
+
|
|
228
|
+
@ps.effect
|
|
229
|
+
def log_changes(self):
|
|
230
|
+
print(f"Count is {self.count}")
|
|
231
|
+
|
|
232
|
+
Async effect:
|
|
233
|
+
|
|
234
|
+
class MyState(ps.State):
|
|
235
|
+
query: str = ""
|
|
236
|
+
|
|
237
|
+
@ps.effect
|
|
238
|
+
async def fetch_data(self):
|
|
239
|
+
data = await api.fetch(self.query)
|
|
240
|
+
self.data = data
|
|
241
|
+
|
|
242
|
+
Effect with cleanup:
|
|
243
|
+
|
|
244
|
+
@ps.effect
|
|
245
|
+
def subscribe(self):
|
|
246
|
+
unsub = event_bus.subscribe(self.handle)
|
|
247
|
+
return unsub # Called before next run or on dispose
|
|
248
|
+
|
|
249
|
+
Polling effect:
|
|
250
|
+
|
|
251
|
+
@ps.effect(interval=5.0)
|
|
252
|
+
async def poll_status(self):
|
|
253
|
+
self.status = await api.get_status()
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def decorator(func: Callable[..., Any], /):
|
|
257
|
+
sig = inspect.signature(func)
|
|
258
|
+
params = list(sig.parameters.values())
|
|
259
|
+
|
|
260
|
+
# Disallow immediate + async
|
|
261
|
+
if immediate and inspect.iscoroutinefunction(func):
|
|
262
|
+
raise ValueError("Async effects cannot have immediate=True")
|
|
263
|
+
|
|
264
|
+
# State method - unchanged behavior
|
|
265
|
+
if len(params) == 1 and params[0].name == "self":
|
|
266
|
+
return StateEffect(
|
|
267
|
+
func,
|
|
268
|
+
name=name,
|
|
269
|
+
immediate=immediate,
|
|
270
|
+
lazy=lazy,
|
|
271
|
+
on_error=on_error,
|
|
272
|
+
deps=deps,
|
|
273
|
+
update_deps=update_deps,
|
|
274
|
+
interval=interval,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Allow params with defaults (used for variable binding in loops)
|
|
278
|
+
# Reject only if there are required params (no default)
|
|
279
|
+
required_params = [p for p in params if p.default is inspect.Parameter.empty]
|
|
280
|
+
if required_params:
|
|
281
|
+
raise TypeError(
|
|
282
|
+
f"@effect: Function '{func.__name__}' must take no arguments, a single 'self' argument, "
|
|
283
|
+
+ "or only arguments with defaults (for variable binding)"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Check if we're in a hook context (component render)
|
|
287
|
+
ctx = HOOK_CONTEXT.get()
|
|
288
|
+
|
|
289
|
+
def create_effect() -> Effect | AsyncEffect:
|
|
290
|
+
if inspect.iscoroutinefunction(func):
|
|
291
|
+
return AsyncEffect(
|
|
292
|
+
func, # type: ignore[arg-type]
|
|
293
|
+
name=name or func.__name__,
|
|
294
|
+
lazy=lazy,
|
|
295
|
+
on_error=on_error,
|
|
296
|
+
deps=deps,
|
|
297
|
+
update_deps=update_deps,
|
|
298
|
+
interval=interval,
|
|
299
|
+
)
|
|
300
|
+
return Effect(
|
|
301
|
+
func, # type: ignore[arg-type]
|
|
302
|
+
name=name or func.__name__,
|
|
303
|
+
immediate=immediate,
|
|
304
|
+
lazy=lazy,
|
|
305
|
+
on_error=on_error,
|
|
306
|
+
deps=deps,
|
|
307
|
+
update_deps=update_deps,
|
|
308
|
+
interval=interval,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if ctx is None:
|
|
312
|
+
# Not in component - create standalone effect (current behavior)
|
|
313
|
+
return create_effect()
|
|
314
|
+
|
|
315
|
+
# In component render - use inline caching
|
|
316
|
+
|
|
317
|
+
# Get the frame where the decorator was applied.
|
|
318
|
+
# When called as `@ps.effect` (no parens), the call stack is:
|
|
319
|
+
# decorator -> effect -> component
|
|
320
|
+
# When called as `@ps.effect(...)` (with parens), the stack is:
|
|
321
|
+
# decorator -> component
|
|
322
|
+
# We detect which case by checking if the immediate caller is effect().
|
|
323
|
+
frame = inspect.currentframe()
|
|
324
|
+
assert frame is not None
|
|
325
|
+
caller = frame.f_back
|
|
326
|
+
assert caller is not None
|
|
327
|
+
# If the immediate caller is the effect function itself, go back one more
|
|
328
|
+
if (
|
|
329
|
+
caller.f_code.co_name == "effect"
|
|
330
|
+
and "decorators" in caller.f_code.co_filename
|
|
331
|
+
):
|
|
332
|
+
caller = caller.f_back
|
|
333
|
+
assert caller is not None
|
|
334
|
+
if key is None:
|
|
335
|
+
identity = collect_component_identity(caller)
|
|
336
|
+
else:
|
|
337
|
+
identity = key
|
|
338
|
+
|
|
339
|
+
state = inline_effect_hook()
|
|
340
|
+
return state.get_or_create(cast(Any, identity), key, create_effect)
|
|
341
|
+
|
|
342
|
+
if fn is not None:
|
|
343
|
+
return decorator(fn)
|
|
344
|
+
return decorator
|
pulse/dom/__init__.py
ADDED
|
File without changes
|