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/hooks/stable.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, TypeVar, overload
|
|
3
|
+
|
|
4
|
+
from pulse.hooks.core import MISSING, HookMetadata, HookState, hooks
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StableEntry:
|
|
11
|
+
"""Container for a stable value and its wrapper function.
|
|
12
|
+
|
|
13
|
+
Holds a value and a wrapper function that always delegates to the
|
|
14
|
+
current value, allowing the wrapper reference to remain stable while
|
|
15
|
+
the underlying value can change.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
value: The current wrapped value.
|
|
19
|
+
wrapper: Stable function that delegates to the current value.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__slots__ = ("value", "wrapper") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
23
|
+
value: Any
|
|
24
|
+
wrapper: Callable[..., Any]
|
|
25
|
+
|
|
26
|
+
def __init__(self, value: Any) -> None:
|
|
27
|
+
self.value = value
|
|
28
|
+
|
|
29
|
+
def wrapper(*args: Any, **kwargs: Any):
|
|
30
|
+
current = self.value
|
|
31
|
+
if callable(current):
|
|
32
|
+
return current(*args, **kwargs)
|
|
33
|
+
return current
|
|
34
|
+
|
|
35
|
+
self.wrapper = wrapper
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class StableRegistry(HookState):
|
|
39
|
+
"""Internal hook state that stores stable entries by key.
|
|
40
|
+
|
|
41
|
+
Maintains a dictionary of StableEntry objects, allowing stable
|
|
42
|
+
wrappers to persist across renders while their underlying values
|
|
43
|
+
can be updated.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("entries",) # pyright: ignore[reportUnannotatedClassAttribute]
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self.entries: dict[str, StableEntry] = {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _stable_factory(*_: object) -> StableRegistry:
|
|
54
|
+
return StableRegistry()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_stable_hook = hooks.create(
|
|
58
|
+
"pulse:core.stable",
|
|
59
|
+
_stable_factory,
|
|
60
|
+
metadata=HookMetadata(
|
|
61
|
+
owner="pulse.core",
|
|
62
|
+
description="Internal registry for pulse.stable values",
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@overload
|
|
68
|
+
def stable(key: str) -> Any: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@overload
|
|
72
|
+
def stable(key: str, value: TCallable) -> TCallable: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def stable(key: str, value: T) -> Callable[[], T]: ...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def stable(key: str, value: Any = MISSING) -> Any:
|
|
80
|
+
"""Return a stable wrapper that always calls the latest value.
|
|
81
|
+
|
|
82
|
+
Creates a wrapper function that maintains a stable reference across renders
|
|
83
|
+
while delegating to the current value. Useful for event handlers and callbacks
|
|
84
|
+
that need to stay referentially stable.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
key: Unique identifier for this stable value within the component.
|
|
88
|
+
value: Optional value or callable to wrap. If provided, updates the
|
|
89
|
+
stored value and returns the wrapper. If omitted, returns the
|
|
90
|
+
existing wrapper for the key.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A stable wrapper function that delegates to the current value. If the
|
|
94
|
+
value is callable, the wrapper calls it with any provided arguments.
|
|
95
|
+
If not callable, the wrapper returns the value directly.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If key is empty.
|
|
99
|
+
KeyError: If value is not provided and no entry exists for the key.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
def my_component():
|
|
105
|
+
s = ps.state("data", lambda: DataState())
|
|
106
|
+
|
|
107
|
+
# Without stable, this would create a new function each render
|
|
108
|
+
handle_click = ps.stable("click", lambda: s.increment())
|
|
109
|
+
|
|
110
|
+
return m.Button("Click", on_click=handle_click)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use Cases:
|
|
114
|
+
- Event handlers passed to child components to prevent unnecessary re-renders
|
|
115
|
+
- Callbacks registered with external systems
|
|
116
|
+
- Any function reference that needs to stay stable across renders
|
|
117
|
+
) -> Any:
|
|
118
|
+
"""
|
|
119
|
+
if not key:
|
|
120
|
+
raise ValueError("stable() requires a non-empty string key")
|
|
121
|
+
|
|
122
|
+
registry = _stable_hook()
|
|
123
|
+
entry = registry.entries.get(key)
|
|
124
|
+
|
|
125
|
+
if value is not MISSING:
|
|
126
|
+
if entry is None:
|
|
127
|
+
entry = StableEntry(value)
|
|
128
|
+
registry.entries[key] = entry
|
|
129
|
+
else:
|
|
130
|
+
entry.value = value
|
|
131
|
+
return entry.wrapper
|
|
132
|
+
|
|
133
|
+
if entry is None:
|
|
134
|
+
raise KeyError(f"stable(): no value registered for key '{key}'")
|
|
135
|
+
return entry.wrapper
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = ["stable", "StableRegistry", "StableEntry"]
|
pulse/hooks/state.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from types import CodeType, FrameType
|
|
4
|
+
from typing import Any, TypeVar, override
|
|
5
|
+
|
|
6
|
+
from pulse.component import is_component_code
|
|
7
|
+
from pulse.hooks.core import HookMetadata, HookState, hooks
|
|
8
|
+
from pulse.state import State
|
|
9
|
+
|
|
10
|
+
S = TypeVar("S", bound=State)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StateHookState(HookState):
|
|
14
|
+
"""Internal hook state for managing State instances across renders.
|
|
15
|
+
|
|
16
|
+
Stores State instances keyed by string identifier and tracks which keys
|
|
17
|
+
have been accessed during the current render cycle.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("instances", "called_keys") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
21
|
+
instances: dict[tuple[str, Any], State]
|
|
22
|
+
called_keys: set[tuple[str, Any]]
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.instances = {}
|
|
27
|
+
self.called_keys = set()
|
|
28
|
+
|
|
29
|
+
def _make_key(self, identity: Any, key: str | None) -> tuple[str, Any]:
|
|
30
|
+
if key is None:
|
|
31
|
+
return ("code", identity)
|
|
32
|
+
return ("key", key)
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
def on_render_start(self, render_cycle: int) -> None:
|
|
36
|
+
super().on_render_start(render_cycle)
|
|
37
|
+
self.called_keys.clear()
|
|
38
|
+
|
|
39
|
+
def get_or_create_state(
|
|
40
|
+
self,
|
|
41
|
+
identity: Any,
|
|
42
|
+
key: str | None,
|
|
43
|
+
arg: State | Callable[[], State],
|
|
44
|
+
) -> State:
|
|
45
|
+
full_identity = self._make_key(identity, key)
|
|
46
|
+
if full_identity in self.called_keys:
|
|
47
|
+
if key is None:
|
|
48
|
+
raise RuntimeError(
|
|
49
|
+
"`pulse.state` can only be called once per component render at the same location. "
|
|
50
|
+
+ "Use the `key` parameter to disambiguate: ps.state(..., key=unique_value)"
|
|
51
|
+
)
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
f"`pulse.state` can only be called once per component render with key='{key}'"
|
|
54
|
+
)
|
|
55
|
+
self.called_keys.add(full_identity)
|
|
56
|
+
|
|
57
|
+
existing = self.instances.get(full_identity)
|
|
58
|
+
if existing is not None:
|
|
59
|
+
# Dispose any State instances passed directly as args that aren't being used
|
|
60
|
+
if isinstance(arg, State) and arg is not existing:
|
|
61
|
+
arg.dispose()
|
|
62
|
+
if existing.__disposed__:
|
|
63
|
+
key_label = f"key='{key}'" if key is not None else "callsite"
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
"`pulse.state` found a disposed cached State for "
|
|
66
|
+
+ key_label
|
|
67
|
+
+ ". Do not dispose states returned by `pulse.state`."
|
|
68
|
+
)
|
|
69
|
+
return existing
|
|
70
|
+
|
|
71
|
+
# Create new state
|
|
72
|
+
instance = _instantiate_state(arg)
|
|
73
|
+
if instance.__disposed__:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"`pulse.state` received a disposed State instance. "
|
|
76
|
+
+ "Do not dispose states passed to `pulse.state`."
|
|
77
|
+
)
|
|
78
|
+
self.instances[full_identity] = instance
|
|
79
|
+
return instance
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
def dispose(self) -> None:
|
|
83
|
+
for instance in self.instances.values():
|
|
84
|
+
try:
|
|
85
|
+
if not instance.__disposed__:
|
|
86
|
+
instance.dispose()
|
|
87
|
+
except RuntimeError:
|
|
88
|
+
# Already disposed, ignore
|
|
89
|
+
pass
|
|
90
|
+
self.instances.clear()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _instantiate_state(arg: State | Callable[[], State]) -> State:
|
|
94
|
+
instance = arg() if callable(arg) else arg
|
|
95
|
+
if not isinstance(instance, State):
|
|
96
|
+
raise TypeError(
|
|
97
|
+
"`pulse.state` expects a State instance or a callable returning a State instance"
|
|
98
|
+
)
|
|
99
|
+
return instance
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _state_factory():
|
|
103
|
+
return StateHookState()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _frame_offset(frame: FrameType) -> int:
|
|
107
|
+
offset = frame.f_lasti
|
|
108
|
+
if offset < 0:
|
|
109
|
+
offset = frame.f_lineno
|
|
110
|
+
return offset
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def collect_component_identity(
|
|
114
|
+
frame: FrameType,
|
|
115
|
+
) -> tuple[tuple[CodeType, int], ...]:
|
|
116
|
+
identity: list[tuple[CodeType, int]] = []
|
|
117
|
+
cursor: FrameType | None = frame
|
|
118
|
+
while cursor is not None:
|
|
119
|
+
identity.append((cursor.f_code, _frame_offset(cursor)))
|
|
120
|
+
if is_component_code(cursor.f_code):
|
|
121
|
+
return tuple(identity)
|
|
122
|
+
cursor = cursor.f_back
|
|
123
|
+
return tuple(identity[:1])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_state_hook = hooks.create(
|
|
127
|
+
"pulse:core.state",
|
|
128
|
+
_state_factory,
|
|
129
|
+
metadata=HookMetadata(
|
|
130
|
+
owner="pulse.core",
|
|
131
|
+
description="Internal storage for pulse.state hook",
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def state(
|
|
137
|
+
arg: S | Callable[[], S],
|
|
138
|
+
*,
|
|
139
|
+
key: str | None = None,
|
|
140
|
+
) -> S:
|
|
141
|
+
"""Get or create a state instance associated with a key or callsite.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
arg: A State instance or a callable that returns a State instance.
|
|
145
|
+
key: Optional key to disambiguate multiple calls from the same location.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The same State instance on subsequent renders with the same key.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: If key is empty.
|
|
152
|
+
RuntimeError: If called more than once per render with the same key.
|
|
153
|
+
TypeError: If arg is not a State or callable returning a State.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
def counter():
|
|
159
|
+
s = ps.state("counter", lambda: CounterState())
|
|
160
|
+
return m.Button(f"Count: {s.count}", on_click=lambda: s.increment())
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Notes:
|
|
164
|
+
- Key must be non-empty string
|
|
165
|
+
- Can only be called once per render with the same key
|
|
166
|
+
- Factory is only called on first render; subsequent renders return cached instance
|
|
167
|
+
- State is disposed when component unmounts
|
|
168
|
+
"""
|
|
169
|
+
if key is not None and not isinstance(key, str):
|
|
170
|
+
raise TypeError("state() key must be a string")
|
|
171
|
+
|
|
172
|
+
if key == "":
|
|
173
|
+
raise ValueError("state() requires a non-empty string key")
|
|
174
|
+
|
|
175
|
+
resolved_key = key
|
|
176
|
+
resolved_arg = arg
|
|
177
|
+
|
|
178
|
+
identity: Any
|
|
179
|
+
if resolved_key is None:
|
|
180
|
+
frame = inspect.currentframe()
|
|
181
|
+
assert frame is not None
|
|
182
|
+
caller = frame.f_back
|
|
183
|
+
assert caller is not None
|
|
184
|
+
identity = collect_component_identity(caller)
|
|
185
|
+
else:
|
|
186
|
+
identity = resolved_key
|
|
187
|
+
|
|
188
|
+
hook_state = _state_hook()
|
|
189
|
+
return hook_state.get_or_create_state(identity, resolved_key, resolved_arg) # pyright: ignore[reportReturnType]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
__all__ = ["state", "StateHookState"]
|
pulse/js/__init__.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""JavaScript module bindings for use in @javascript decorated functions (transpiler).
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
Import JS classes (for constructors and static methods):
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pulse.js import Set, Number, Array, Date, Promise, Map, Error
|
|
9
|
+
Set([1, 2, 3]) # -> new Set([1, 2, 3])
|
|
10
|
+
Number.isFinite(42) # -> Number.isFinite(42)
|
|
11
|
+
Array.isArray(x) # -> Array.isArray(x)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Import JS namespace objects (function-only modules):
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from pulse.js import Math, JSON, console, window, document, navigator
|
|
18
|
+
Math.floor(3.7) # -> Math.floor(3.7)
|
|
19
|
+
JSON.stringify(obj) # -> JSON.stringify(obj)
|
|
20
|
+
console.log("hi") # -> console.log("hi")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Alternative: import namespace modules for namespace access:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import pulse.js.json as JSON
|
|
27
|
+
JSON.stringify(obj) # -> JSON.stringify(obj)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Statement functions:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pulse.js import throw
|
|
34
|
+
throw(Error("message")) # -> throw Error("message");
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Object literals (plain JS objects instead of Map):
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pulse.js import obj
|
|
41
|
+
obj(a=1, b=2) # -> { a: 1, b: 2 }
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import importlib as _importlib
|
|
46
|
+
from typing import Any as _Any
|
|
47
|
+
from typing import NoReturn as _NoReturn
|
|
48
|
+
|
|
49
|
+
from pulse.js.obj import obj as obj
|
|
50
|
+
from pulse.transpiler.nodes import EXPR_REGISTRY as _EXPR_REGISTRY
|
|
51
|
+
from pulse.transpiler.nodes import UNDEFINED as _UNDEFINED
|
|
52
|
+
|
|
53
|
+
# Namespace modules - return JsModule from registry (handles both builtins and external)
|
|
54
|
+
_MODULE_EXPORTS_NAMESPACE: dict[str, str] = {
|
|
55
|
+
"JSON": "pulse.js.json",
|
|
56
|
+
"Math": "pulse.js.math",
|
|
57
|
+
"React": "pulse.js.react",
|
|
58
|
+
"console": "pulse.js.console",
|
|
59
|
+
"window": "pulse.js.window",
|
|
60
|
+
"document": "pulse.js.document",
|
|
61
|
+
"navigator": "pulse.js.navigator",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Class modules - return via getattr to get Class wrapper (emits `new ...`)
|
|
65
|
+
_MODULE_EXPORTS_ATTRIBUTE: dict[str, str] = {
|
|
66
|
+
"Array": "pulse.js.array",
|
|
67
|
+
"Date": "pulse.js.date",
|
|
68
|
+
"Error": "pulse.js.error",
|
|
69
|
+
"Map": "pulse.js.map",
|
|
70
|
+
"Object": "pulse.js.object",
|
|
71
|
+
"Promise": "pulse.js.promise",
|
|
72
|
+
"React": "pulse.js.react",
|
|
73
|
+
"RegExp": "pulse.js.regexp",
|
|
74
|
+
"Set": "pulse.js.set",
|
|
75
|
+
"String": "pulse.js.string",
|
|
76
|
+
"WeakMap": "pulse.js.weakmap",
|
|
77
|
+
"WeakSet": "pulse.js.weakset",
|
|
78
|
+
"Number": "pulse.js.number",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Statement-like functions (not classes/objects, but callable transformers)
|
|
83
|
+
# Note: throw needs special handling in the transpiler to convert from expression to statement
|
|
84
|
+
class _ThrowExpr:
|
|
85
|
+
"""Wrapper for throw that can be detected and converted to a statement."""
|
|
86
|
+
|
|
87
|
+
def __call__(self, x: _Any) -> _NoReturn:
|
|
88
|
+
# This will be replaced during transpilation
|
|
89
|
+
# The transpiler should detect this and emit as a Throw statement
|
|
90
|
+
raise RuntimeError("throw() can only be used in @javascript functions")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
throw = _ThrowExpr()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# JS primitive values
|
|
97
|
+
undefined = _UNDEFINED
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Cache for exported values
|
|
101
|
+
_export_cache: dict[str, _Any] = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def __getattr__(name: str) -> _Any:
|
|
105
|
+
"""Lazily import and return JS builtin modules.
|
|
106
|
+
|
|
107
|
+
Allows: from pulse.js import Set, Number, Array, etc.
|
|
108
|
+
"""
|
|
109
|
+
# Return cached export if already imported
|
|
110
|
+
if name in _export_cache:
|
|
111
|
+
return _export_cache[name]
|
|
112
|
+
|
|
113
|
+
# Namespace modules: return JsModule (handles attribute access via transpile_getattr)
|
|
114
|
+
if name in _MODULE_EXPORTS_NAMESPACE:
|
|
115
|
+
module = _importlib.import_module(_MODULE_EXPORTS_NAMESPACE[name])
|
|
116
|
+
export = _EXPR_REGISTRY[id(module)]
|
|
117
|
+
# Class modules: return Class wrapper via getattr (emits `new ...()`)
|
|
118
|
+
elif name in _MODULE_EXPORTS_ATTRIBUTE:
|
|
119
|
+
module = _importlib.import_module(_MODULE_EXPORTS_ATTRIBUTE[name])
|
|
120
|
+
export = getattr(module, name)
|
|
121
|
+
else:
|
|
122
|
+
raise AttributeError(f"module 'pulse.js' has no attribute '{name}'")
|
|
123
|
+
|
|
124
|
+
_export_cache[name] = export
|
|
125
|
+
return export
|
pulse/js/__init__.pyi
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Type stubs for pulse.js module exports.
|
|
2
|
+
|
|
3
|
+
This file provides type hints for direct imports from pulse.js:
|
|
4
|
+
from pulse.js import Set, Number, Array, Math, Date, Promise, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any as _Any
|
|
8
|
+
from typing import NoReturn as _NoReturn
|
|
9
|
+
|
|
10
|
+
import pulse.js.console
|
|
11
|
+
import pulse.js.document
|
|
12
|
+
import pulse.js.json
|
|
13
|
+
import pulse.js.math
|
|
14
|
+
import pulse.js.navigator
|
|
15
|
+
import pulse.js.window
|
|
16
|
+
|
|
17
|
+
# Re-export type definitions for use in user code
|
|
18
|
+
from pulse.js._types import (
|
|
19
|
+
Clipboard as Clipboard,
|
|
20
|
+
)
|
|
21
|
+
from pulse.js._types import (
|
|
22
|
+
ClipboardItem as ClipboardItem,
|
|
23
|
+
)
|
|
24
|
+
from pulse.js._types import (
|
|
25
|
+
CSSStyleDeclaration as CSSStyleDeclaration,
|
|
26
|
+
)
|
|
27
|
+
from pulse.js._types import (
|
|
28
|
+
Element as Element,
|
|
29
|
+
)
|
|
30
|
+
from pulse.js._types import (
|
|
31
|
+
Event as Event,
|
|
32
|
+
)
|
|
33
|
+
from pulse.js._types import (
|
|
34
|
+
HTMLCollection as HTMLCollection,
|
|
35
|
+
)
|
|
36
|
+
from pulse.js._types import (
|
|
37
|
+
HTMLElement as HTMLElement,
|
|
38
|
+
)
|
|
39
|
+
from pulse.js._types import (
|
|
40
|
+
JSIterable as JSIterable,
|
|
41
|
+
)
|
|
42
|
+
from pulse.js._types import (
|
|
43
|
+
JSIterator as JSIterator,
|
|
44
|
+
)
|
|
45
|
+
from pulse.js._types import (
|
|
46
|
+
JSIteratorResult as JSIteratorResult,
|
|
47
|
+
)
|
|
48
|
+
from pulse.js._types import (
|
|
49
|
+
JSONReplacer as JSONReplacer,
|
|
50
|
+
)
|
|
51
|
+
from pulse.js._types import (
|
|
52
|
+
JSONReviver as JSONReviver,
|
|
53
|
+
)
|
|
54
|
+
from pulse.js._types import (
|
|
55
|
+
JSONValue as JSONValue,
|
|
56
|
+
)
|
|
57
|
+
from pulse.js._types import (
|
|
58
|
+
NodeList as NodeList,
|
|
59
|
+
)
|
|
60
|
+
from pulse.js._types import (
|
|
61
|
+
Range as Range,
|
|
62
|
+
)
|
|
63
|
+
from pulse.js._types import (
|
|
64
|
+
Selection as Selection,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Re-export classes with proper generic types
|
|
68
|
+
from pulse.js.array import Array as Array
|
|
69
|
+
from pulse.js.date import Date as Date
|
|
70
|
+
from pulse.js.error import Error as Error
|
|
71
|
+
from pulse.js.error import EvalError as EvalError
|
|
72
|
+
from pulse.js.error import RangeError as RangeError
|
|
73
|
+
from pulse.js.error import ReferenceError as ReferenceError
|
|
74
|
+
from pulse.js.error import SyntaxError as SyntaxError
|
|
75
|
+
from pulse.js.error import TypeError as TypeError
|
|
76
|
+
from pulse.js.error import URIError as URIError
|
|
77
|
+
from pulse.js.map import Map as Map
|
|
78
|
+
from pulse.js.number import Number as Number
|
|
79
|
+
from pulse.js.object import Object as Object
|
|
80
|
+
from pulse.js.object import PropertyDescriptor as PropertyDescriptor
|
|
81
|
+
from pulse.js.promise import Promise as Promise
|
|
82
|
+
from pulse.js.promise import PromiseWithResolvers as PromiseWithResolvers
|
|
83
|
+
from pulse.js.regexp import RegExp as RegExp
|
|
84
|
+
from pulse.js.set import Set as Set
|
|
85
|
+
from pulse.js.string import String as String
|
|
86
|
+
from pulse.js.weakmap import WeakMap as WeakMap
|
|
87
|
+
from pulse.js.weakset import WeakSet as WeakSet
|
|
88
|
+
from pulse.transpiler.nodes import Undefined
|
|
89
|
+
|
|
90
|
+
# Re-export namespace modules
|
|
91
|
+
console = pulse.js.console
|
|
92
|
+
document = pulse.js.document
|
|
93
|
+
JSON = pulse.js.json
|
|
94
|
+
Math = pulse.js.math
|
|
95
|
+
navigator = pulse.js.navigator
|
|
96
|
+
window = pulse.js.window
|
|
97
|
+
|
|
98
|
+
# Statement-like functions
|
|
99
|
+
def throw(x: _Any) -> _NoReturn:
|
|
100
|
+
"""Throw a JavaScript error."""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def obj(**kwargs: _Any) -> _Any:
|
|
104
|
+
"""Create a plain JavaScript object literal.
|
|
105
|
+
|
|
106
|
+
Use this instead of dict() when you need a plain JS object (e.g., for React style prop).
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
style=obj(display="block", color="red")
|
|
110
|
+
# Transpiles to: style={{ display: "block", color: "red" }}
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
# Primitive values
|
|
115
|
+
undefined: Undefined
|