pulse-framework 0.1.50__py3-none-any.whl → 0.1.52__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 +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -999
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/serializer.py +11 -1
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
- pulse_framework-0.1.52.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -667
- pulse_framework-0.1.50.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/transpiler/js_module.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Core infrastructure for JavaScript module bindings.
|
|
1
|
+
"""Core infrastructure for JavaScript module bindings in transpiler.
|
|
2
2
|
|
|
3
3
|
JS modules are Python modules that map to JavaScript modules/builtins.
|
|
4
4
|
Registration is done by calling register_js_module() from within the module itself.
|
|
@@ -6,269 +6,278 @@ Registration is done by calling register_js_module() from within the module itse
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import ast
|
|
9
10
|
import inspect
|
|
10
11
|
import sys
|
|
11
|
-
from collections.abc import Callable
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
|
-
from
|
|
14
|
-
from typing import Any, ClassVar, Literal, TypeVar, override
|
|
13
|
+
from typing import Literal, override
|
|
15
14
|
|
|
16
|
-
from pulse.transpiler.errors import
|
|
15
|
+
from pulse.transpiler.errors import TranspileError
|
|
17
16
|
from pulse.transpiler.imports import Import
|
|
18
|
-
from pulse.transpiler.nodes import
|
|
17
|
+
from pulse.transpiler.nodes import (
|
|
18
|
+
Expr,
|
|
19
|
+
Identifier,
|
|
20
|
+
Member,
|
|
21
|
+
New,
|
|
22
|
+
)
|
|
23
|
+
from pulse.transpiler.transpiler import Transpiler
|
|
24
|
+
from pulse.transpiler.vdom import VDOMNode
|
|
25
|
+
|
|
26
|
+
_MODULE_DUNDERS = frozenset(
|
|
27
|
+
{
|
|
28
|
+
"__name__",
|
|
29
|
+
"__file__",
|
|
30
|
+
"__doc__",
|
|
31
|
+
"__package__",
|
|
32
|
+
"__path__",
|
|
33
|
+
"__cached__",
|
|
34
|
+
"__loader__",
|
|
35
|
+
"__spec__",
|
|
36
|
+
"__builtins__",
|
|
37
|
+
"__annotations__",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(slots=True)
|
|
43
|
+
class Class(Expr):
|
|
44
|
+
"""Expr wrapper that emits calls as `new ...(...)`.
|
|
45
|
+
|
|
46
|
+
Must also behave like the wrapped expression for attribute access,
|
|
47
|
+
so patterns like `Promise.resolve(...)` work even if `Promise(...)` emits
|
|
48
|
+
as `new Promise(...)`.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
ctor: Expr
|
|
52
|
+
name: str = ""
|
|
19
53
|
|
|
20
|
-
|
|
21
|
-
|
|
54
|
+
@override
|
|
55
|
+
def emit(self, out: list[str]) -> None:
|
|
56
|
+
self.ctor.emit(out)
|
|
22
57
|
|
|
23
|
-
|
|
58
|
+
@override
|
|
59
|
+
def render(self) -> VDOMNode:
|
|
60
|
+
return self.ctor.render()
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
def transpile_call(
|
|
64
|
+
self,
|
|
65
|
+
args: list[ast.expr],
|
|
66
|
+
kwargs: dict[str, ast.expr],
|
|
67
|
+
ctx: Transpiler,
|
|
68
|
+
) -> Expr:
|
|
69
|
+
if kwargs:
|
|
70
|
+
raise TranspileError("Keyword arguments not supported in constructor call")
|
|
71
|
+
return New(self.ctor, [ctx.emit_expr(a) for a in args])
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
75
|
+
# Convention: trailing underscore escapes Python keywords (e.g. from_ -> from, is_ -> is)
|
|
76
|
+
js_attr = attr[:-1] if attr.endswith("_") else attr
|
|
77
|
+
return Member(self.ctor, js_attr)
|
|
24
78
|
|
|
25
79
|
|
|
26
80
|
@dataclass(frozen=True)
|
|
27
|
-
class JsModule:
|
|
28
|
-
"""
|
|
81
|
+
class JsModule(Expr):
|
|
82
|
+
"""Expr representing a JavaScript module binding.
|
|
29
83
|
|
|
30
84
|
Attributes:
|
|
31
|
-
name: The JavaScript identifier (e.g., "Math", "
|
|
85
|
+
name: The JavaScript identifier for the module binding (e.g., "Math", "React"),
|
|
86
|
+
or None for "global identifier" modules with no module expression.
|
|
87
|
+
py_name: Python module name for error messages (e.g., "pulse.js.math")
|
|
32
88
|
src: Import source path. None for builtins.
|
|
33
89
|
kind: Import kind - "default" or "namespace"
|
|
34
90
|
values: How attribute access is expressed:
|
|
35
91
|
- "member": Access as property (e.g., React.useState)
|
|
36
92
|
- "named_import": Each attribute is a named import (e.g., import { useState } from "react")
|
|
37
93
|
constructors: Set of names that are constructors (emit with 'new')
|
|
38
|
-
global_scope: If True, members are registered in global scope. Module imports are disallowed.
|
|
39
94
|
"""
|
|
40
95
|
|
|
41
|
-
name: str
|
|
96
|
+
name: str | None
|
|
97
|
+
py_name: str = ""
|
|
42
98
|
src: str | None = None
|
|
43
99
|
kind: Literal["default", "namespace"] = "namespace"
|
|
44
100
|
values: Literal["member", "named_import"] = "named_import"
|
|
45
101
|
constructors: frozenset[str] = field(default_factory=frozenset)
|
|
46
|
-
|
|
102
|
+
|
|
103
|
+
@override
|
|
104
|
+
def emit(self, out: list[str]) -> None:
|
|
105
|
+
label = self.py_name or self.name or "JsModule"
|
|
106
|
+
raise TypeError(f"{label} cannot be emitted directly - access an attribute")
|
|
107
|
+
|
|
108
|
+
@override
|
|
109
|
+
def render(self) -> VDOMNode:
|
|
110
|
+
label = self.py_name or self.name or "JsModule"
|
|
111
|
+
raise TypeError(f"{label} cannot be rendered directly - access an attribute")
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
def transpile_call(
|
|
115
|
+
self,
|
|
116
|
+
args: list[ast.expr],
|
|
117
|
+
kwargs: dict[str, ast.expr],
|
|
118
|
+
ctx: Transpiler,
|
|
119
|
+
) -> Expr:
|
|
120
|
+
label = self.py_name or self.name or "JsModule"
|
|
121
|
+
raise TypeError(f"{label} cannot be called directly - access an attribute")
|
|
122
|
+
|
|
123
|
+
@override
|
|
124
|
+
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
125
|
+
return self.get_value(attr)
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
|
|
129
|
+
label = self.py_name or self.name or "JsModule"
|
|
130
|
+
raise TypeError(f"{label} cannot be subscripted")
|
|
47
131
|
|
|
48
132
|
@property
|
|
49
133
|
def is_builtin(self) -> bool:
|
|
50
134
|
return self.src is None
|
|
51
135
|
|
|
52
|
-
def
|
|
53
|
-
"""Generate the appropriate
|
|
136
|
+
def to_expr(self) -> Identifier | Import:
|
|
137
|
+
"""Generate the appropriate Expr for this module.
|
|
54
138
|
|
|
55
|
-
Returns
|
|
139
|
+
Returns Identifier for builtins, Import for external modules.
|
|
56
140
|
|
|
57
|
-
Raises
|
|
141
|
+
Raises TranspileError if name is None (module imports are disallowed).
|
|
58
142
|
"""
|
|
59
|
-
if self.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
143
|
+
if self.name is None:
|
|
144
|
+
label = self.py_name or "JS global module"
|
|
145
|
+
# If a module has no JS module expression, importing it as a module value is meaningless.
|
|
146
|
+
# Users should import members from the Python module instead.
|
|
147
|
+
if self.py_name:
|
|
148
|
+
msg = (
|
|
149
|
+
f"Cannot import module '{label}' directly. "
|
|
150
|
+
+ f"Use 'from {self.py_name} import ...' instead."
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
msg = f"Cannot import module '{label}' directly."
|
|
154
|
+
raise TranspileError(msg)
|
|
66
155
|
|
|
67
156
|
if self.src is None:
|
|
68
|
-
return
|
|
157
|
+
return Identifier(self.name)
|
|
69
158
|
|
|
70
|
-
if self.kind == "default"
|
|
71
|
-
|
|
72
|
-
return Import.namespace(self.name, self.src)
|
|
159
|
+
import_kind = "default" if self.kind == "default" else "named"
|
|
160
|
+
return Import(self.name, self.src, kind=import_kind)
|
|
73
161
|
|
|
74
|
-
def get_value(self, name: str) ->
|
|
75
|
-
"""Get a member of this module as
|
|
162
|
+
def get_value(self, name: str) -> Member | Class | Identifier | Import:
|
|
163
|
+
"""Get a member of this module as an expression.
|
|
76
164
|
|
|
77
|
-
For
|
|
78
|
-
For builtins: returns
|
|
165
|
+
For global-identifier modules (name=None): returns Identifier(name) directly (e.g., Set -> Set)
|
|
166
|
+
For builtins: returns Member (e.g., Math.sin), or Identifier if name
|
|
79
167
|
matches the module name (e.g., Set -> Set, not Set.Set)
|
|
80
|
-
For external modules with "member" style: returns
|
|
81
|
-
For external modules with "named_import" style: returns a named Import
|
|
168
|
+
For external modules with "member" style: returns Member (e.g., React.useState)
|
|
169
|
+
For external modules with "named_import" style: returns a named Import
|
|
82
170
|
|
|
83
|
-
If name is in constructors,
|
|
171
|
+
If name is in constructors, returns a Class that emits `new ...(...)`.
|
|
84
172
|
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
173
|
+
# Convention: trailing underscore escapes Python keywords (e.g. from_ -> from, is_ -> is).
|
|
174
|
+
# We keep the original `name` for constructor detection, but emit the JS name.
|
|
175
|
+
js_name = name[:-1] if name.endswith("_") else name
|
|
176
|
+
|
|
177
|
+
expr: Member | Identifier | Import
|
|
178
|
+
if self.name is None:
|
|
179
|
+
# No module expression: members are just identifiers, not members of a module
|
|
180
|
+
expr = Identifier(js_name)
|
|
89
181
|
elif self.src is None:
|
|
90
182
|
# Builtins: use identifier when name matches module name (Set.Set -> Set)
|
|
91
183
|
if name == self.name:
|
|
92
|
-
expr =
|
|
184
|
+
expr = Identifier(js_name)
|
|
93
185
|
else:
|
|
94
|
-
expr =
|
|
186
|
+
expr = Member(Identifier(self.name), js_name)
|
|
95
187
|
elif self.values == "named_import":
|
|
96
|
-
expr = Import
|
|
188
|
+
expr = Import(js_name, self.src)
|
|
97
189
|
else:
|
|
98
|
-
expr =
|
|
190
|
+
expr = Member(self.to_expr(), js_name)
|
|
99
191
|
|
|
100
192
|
if name in self.constructors:
|
|
101
|
-
return
|
|
193
|
+
return Class(expr, name=name)
|
|
102
194
|
return expr
|
|
103
195
|
|
|
104
|
-
|
|
105
|
-
@dataclass
|
|
106
|
-
class JSConstructor(JSExpr):
|
|
107
|
-
"""Wrapper that emits constructor calls with 'new' keyword.
|
|
108
|
-
|
|
109
|
-
When this expression is called, it produces JSNew instead of JSCall.
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
ctor: JSExpr
|
|
113
|
-
is_primary: ClassVar[bool] = True
|
|
114
|
-
|
|
115
|
-
@override
|
|
116
|
-
def emit(self) -> str:
|
|
117
|
-
return self.ctor.emit()
|
|
118
|
-
|
|
119
196
|
@override
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
197
|
+
@staticmethod
|
|
198
|
+
def register( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
199
|
+
*,
|
|
200
|
+
name: str | None,
|
|
201
|
+
src: str | None = None,
|
|
202
|
+
kind: Literal["default", "namespace"] = "namespace",
|
|
203
|
+
values: Literal["member", "named_import"] = "named_import",
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Register the calling Python module as a JavaScript module binding.
|
|
206
|
+
|
|
207
|
+
Must be called from within the module being registered. The module is
|
|
208
|
+
automatically detected from the call stack.
|
|
209
|
+
|
|
210
|
+
This function sets up __getattr__ on the module for dynamic attribute access,
|
|
211
|
+
and registers the module object in EXPR_REGISTRY so it can be used as a
|
|
212
|
+
dependency (e.g., `import pulse.js.math as Math`) during transpilation.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
name: The JavaScript identifier for the module binding (e.g., "Math"), or None
|
|
216
|
+
for modules that expose only global identifiers and cannot be imported as a whole.
|
|
217
|
+
src: Import source path. None for builtins.
|
|
218
|
+
kind: Import kind - "default" or "namespace"
|
|
219
|
+
values: How attribute access works:
|
|
220
|
+
- "member": Access as property (e.g., Math.sin, React.useState)
|
|
221
|
+
- "named_import": Each attribute is a named import
|
|
222
|
+
|
|
223
|
+
Example (inside pulse/js/math.py):
|
|
224
|
+
JsModule.register(name="Math") # builtin
|
|
225
|
+
|
|
226
|
+
Example (inside pulse/js/react.py):
|
|
227
|
+
JsModule.register(name="React", src="react") # namespace + named imports
|
|
228
|
+
|
|
229
|
+
Example (inside pulse/js/set.py):
|
|
230
|
+
JsModule.register(name=None) # global identifier builtin (no module binding)
|
|
231
|
+
"""
|
|
232
|
+
# Get the calling module from the stack frame
|
|
233
|
+
frame = inspect.currentframe()
|
|
234
|
+
assert frame is not None and frame.f_back is not None
|
|
235
|
+
module_name = frame.f_back.f_globals["__name__"]
|
|
236
|
+
module = sys.modules[module_name]
|
|
144
237
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
238
|
+
# Collect locally defined names and clean up module namespace
|
|
239
|
+
constructors: set[str] = set()
|
|
240
|
+
local_names: set[str] = set()
|
|
148
241
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
kind: Import kind - "default" or "namespace"
|
|
153
|
-
values: How attribute access works:
|
|
154
|
-
- "member": Access as property (e.g., Math.sin, React.useState)
|
|
155
|
-
- "named_import": Each attribute is a named import (e.g., import { useState } from "react")
|
|
156
|
-
global_scope: If True, members are registered in global scope. Module imports
|
|
157
|
-
(e.g., `import pulse.js.set as Set`) are disallowed. Members are transpiled
|
|
158
|
-
as direct identifiers (e.g., `Set` -> `Set`, not `Set.Set`).
|
|
242
|
+
for attr_name in list(vars(module)):
|
|
243
|
+
if attr_name in _MODULE_DUNDERS:
|
|
244
|
+
continue
|
|
159
245
|
|
|
160
|
-
|
|
161
|
-
|
|
246
|
+
obj = getattr(module, attr_name)
|
|
247
|
+
is_local = not hasattr(obj, "__module__") or obj.__module__ == module_name
|
|
162
248
|
|
|
163
|
-
|
|
164
|
-
|
|
249
|
+
if is_local and not attr_name.startswith("_"):
|
|
250
|
+
local_names.add(attr_name)
|
|
251
|
+
if inspect.isclass(obj):
|
|
252
|
+
constructors.add(attr_name)
|
|
165
253
|
|
|
166
|
-
Example (inside pulse/js/set.py):
|
|
167
|
-
register_js_module(name="Set", global_scope=True) # global scope builtin
|
|
168
|
-
"""
|
|
169
|
-
# Get the calling module from the stack frame
|
|
170
|
-
frame = inspect.currentframe()
|
|
171
|
-
assert frame is not None and frame.f_back is not None
|
|
172
|
-
module_name = frame.f_back.f_globals["__name__"]
|
|
173
|
-
module = sys.modules[module_name]
|
|
174
|
-
|
|
175
|
-
# Collect constructor names before deleting functions/classes
|
|
176
|
-
# Classes are automatically treated as constructors
|
|
177
|
-
# Only items defined in this module are considered (not imported ones)
|
|
178
|
-
# Track locally defined names so __getattr__ can distinguish them from imported ones
|
|
179
|
-
|
|
180
|
-
def is_defined_in_module(obj: Any) -> bool:
|
|
181
|
-
"""Check if an object is defined in the current module (not imported)."""
|
|
182
|
-
return hasattr(obj, "__module__") and obj.__module__ == module_name
|
|
183
|
-
|
|
184
|
-
ctor_names: set[str] = set()
|
|
185
|
-
locally_defined_names: set[str] = set()
|
|
186
|
-
|
|
187
|
-
for attr_name in list(vars(module)):
|
|
188
|
-
# Skip special module attributes
|
|
189
|
-
if attr_name in (
|
|
190
|
-
"__name__",
|
|
191
|
-
"__file__",
|
|
192
|
-
"__doc__",
|
|
193
|
-
"__package__",
|
|
194
|
-
"__path__",
|
|
195
|
-
"__cached__",
|
|
196
|
-
"__loader__",
|
|
197
|
-
"__spec__",
|
|
198
|
-
):
|
|
199
|
-
continue
|
|
200
|
-
attr = getattr(module, attr_name)
|
|
201
|
-
# Check if this is an imported name (has __module__ that doesn't match)
|
|
202
|
-
is_imported = hasattr(attr, "__module__") and attr.__module__ != module_name
|
|
203
|
-
if isinstance(attr, FunctionType):
|
|
204
|
-
# Functions without __module__ are assumed to be locally defined (stub functions)
|
|
205
|
-
if not is_imported and (
|
|
206
|
-
not hasattr(attr, "__module__") or is_defined_in_module(attr)
|
|
207
|
-
):
|
|
208
|
-
# Only track non-underscore-prefixed names for exports
|
|
209
|
-
if not attr_name.startswith("_"):
|
|
210
|
-
locally_defined_names.add(attr_name)
|
|
211
|
-
if id(attr) in CONSTRUCTORS:
|
|
212
|
-
ctor_names.add(attr_name)
|
|
213
|
-
delattr(module, attr_name)
|
|
214
|
-
else:
|
|
215
|
-
# Delete imported functions (including underscore-prefixed)
|
|
216
|
-
delattr(module, attr_name)
|
|
217
|
-
elif inspect.isclass(attr):
|
|
218
|
-
# Only consider classes defined in this module (not imported)
|
|
219
|
-
if not is_imported and is_defined_in_module(attr):
|
|
220
|
-
# Only track non-underscore-prefixed names for exports
|
|
221
|
-
if not attr_name.startswith("_"):
|
|
222
|
-
locally_defined_names.add(attr_name)
|
|
223
|
-
ctor_names.add(attr_name)
|
|
224
|
-
delattr(module, attr_name)
|
|
225
|
-
else:
|
|
226
|
-
# Delete imported classes (including underscore-prefixed)
|
|
227
|
-
delattr(module, attr_name)
|
|
228
|
-
elif is_imported:
|
|
229
|
-
# Delete all imported objects (TypeVar, Generic, etc.) including underscore-prefixed
|
|
230
|
-
delattr(module, attr_name)
|
|
231
|
-
else:
|
|
232
|
-
# For objects without __module__, assume they're locally defined
|
|
233
|
-
# (constants, etc.) - but constants are usually just annotations
|
|
234
|
-
# so they won't be in vars(module) anyway
|
|
235
|
-
if not attr_name.startswith("_"):
|
|
236
|
-
locally_defined_names.add(attr_name)
|
|
237
254
|
delattr(module, attr_name)
|
|
238
255
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if name.startswith("_"):
|
|
268
|
-
raise AttributeError(name)
|
|
269
|
-
# Only allow access to locally defined names (functions, classes, constants)
|
|
270
|
-
if name not in _defined_names:
|
|
271
|
-
raise AttributeError(name)
|
|
272
|
-
return js_module.get_value(name)
|
|
273
|
-
|
|
274
|
-
module.__getattr__ = __getattr__ # type: ignore[method-assign]
|
|
256
|
+
# Add annotated constants to local_names
|
|
257
|
+
if hasattr(module, "__annotations__"):
|
|
258
|
+
for ann_name in module.__annotations__:
|
|
259
|
+
if not ann_name.startswith("_"):
|
|
260
|
+
local_names.add(ann_name)
|
|
261
|
+
module.__annotations__.clear()
|
|
262
|
+
|
|
263
|
+
# Invariants: a module without a JS module binding cannot be imported from a JS source.
|
|
264
|
+
if name is None and src is not None:
|
|
265
|
+
raise ValueError("name=None is only supported for builtins (src=None)")
|
|
266
|
+
|
|
267
|
+
js_module = JsModule(
|
|
268
|
+
name=name,
|
|
269
|
+
py_name=module.__name__,
|
|
270
|
+
src=src,
|
|
271
|
+
kind=kind,
|
|
272
|
+
values=values,
|
|
273
|
+
constructors=frozenset(constructors),
|
|
274
|
+
)
|
|
275
|
+
# Register the module object itself so `import pulse.js.math as Math` resolves via EXPR_REGISTRY.
|
|
276
|
+
Expr.register(module, js_module)
|
|
277
|
+
|
|
278
|
+
def __getattr__(name: str) -> Member | Class | Identifier | Import:
|
|
279
|
+
if name.startswith("_") or name not in local_names:
|
|
280
|
+
raise AttributeError(name)
|
|
281
|
+
return js_module.get_value(name)
|
|
282
|
+
|
|
283
|
+
module.__getattr__ = __getattr__ # type: ignore[method-assign]
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
"""Central registration point for all module transpilers.
|
|
2
2
|
|
|
3
|
-
This module registers all built-in Python
|
|
3
|
+
This module registers all built-in Python modules for transpilation.
|
|
4
4
|
Import this module to ensure all transpilers are available.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio as asyncio_builtin
|
|
8
8
|
import json as json_builtin
|
|
9
9
|
import math as math_builtin
|
|
10
|
-
import re as re_builtin
|
|
11
10
|
import typing as typing_builtin
|
|
12
11
|
|
|
13
|
-
import pulse
|
|
12
|
+
import pulse as pulse_module
|
|
13
|
+
import pulse.dom.tags as pulseTags
|
|
14
14
|
from pulse.transpiler.modules.asyncio import PyAsyncio
|
|
15
15
|
from pulse.transpiler.modules.json import PyJson
|
|
16
16
|
from pulse.transpiler.modules.math import PyMath
|
|
17
|
-
from pulse.transpiler.modules.
|
|
18
|
-
from pulse.transpiler.modules.tags import PyTags
|
|
17
|
+
from pulse.transpiler.modules.pulse.tags import PulseTags
|
|
19
18
|
from pulse.transpiler.modules.typing import PyTyping
|
|
20
|
-
from pulse.transpiler.py_module import
|
|
19
|
+
from pulse.transpiler.py_module import PyModule
|
|
21
20
|
|
|
22
21
|
# Register built-in Python modules
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
register_module(typing_builtin, PyTyping)
|
|
22
|
+
PyModule.register(math_builtin, PyMath)
|
|
23
|
+
PyModule.register(json_builtin, PyJson)
|
|
24
|
+
PyModule.register(asyncio_builtin, PyAsyncio)
|
|
25
|
+
PyModule.register(typing_builtin, PyTyping)
|
|
28
26
|
|
|
29
|
-
# Register Pulse
|
|
30
|
-
|
|
27
|
+
# Register Pulse DOM tags for JSX transpilation
|
|
28
|
+
# This covers `from pulse.dom import tags; tags.div(...)`
|
|
29
|
+
PyModule.register(pulseTags, PulseTags)
|
|
30
|
+
|
|
31
|
+
# Register main pulse module for transpilation
|
|
32
|
+
# This covers `import pulse as ps; ps.div(...)`
|
|
33
|
+
PyModule.register(pulse_module, PulseTags)
|
|
@@ -1,38 +1,57 @@
|
|
|
1
|
-
"""Python asyncio module transpilation to JavaScript Promise operations.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
"""Python asyncio module transpilation to JavaScript Promise operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast, final
|
|
6
|
+
|
|
7
|
+
from pulse.transpiler.nodes import (
|
|
8
|
+
Array,
|
|
9
|
+
Binary,
|
|
10
|
+
Call,
|
|
11
|
+
Expr,
|
|
12
|
+
Identifier,
|
|
13
|
+
Literal,
|
|
14
|
+
Member,
|
|
15
|
+
Ternary,
|
|
16
|
+
)
|
|
7
17
|
from pulse.transpiler.py_module import PyModule
|
|
18
|
+
from pulse.transpiler.transpiler import Transpiler
|
|
8
19
|
|
|
20
|
+
_Promise = Identifier("Promise")
|
|
9
21
|
|
|
22
|
+
|
|
23
|
+
@final
|
|
10
24
|
class PyAsyncio(PyModule):
|
|
11
25
|
"""Provides transpilation for Python asyncio functions to JavaScript Promise methods."""
|
|
12
26
|
|
|
13
27
|
@staticmethod
|
|
14
|
-
def gather(*coros:
|
|
28
|
+
def gather(*coros: Any, return_exceptions: Any = False, ctx: Transpiler) -> Expr:
|
|
15
29
|
"""Transpile asyncio.gather to Promise.all or Promise.allSettled.
|
|
16
30
|
|
|
17
31
|
Args:
|
|
18
32
|
*coros: Variable number of coroutine/promise expressions
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
JSExpr representing Promise.all([...]) or Promise.allSettled([...])
|
|
33
|
+
return_exceptions: If True, use Promise.allSettled
|
|
34
|
+
ctx: Transpiler context
|
|
23
35
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if return_exceptions is
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
promises = Array([ctx.emit_expr(c) for c in coros])
|
|
37
|
+
all_call = Call(Member(_Promise, "all"), [promises])
|
|
38
|
+
all_settled_call = Call(Member(_Promise, "allSettled"), [promises])
|
|
39
|
+
|
|
40
|
+
# Optimized: literal True -> allSettled
|
|
41
|
+
if return_exceptions is True or (
|
|
42
|
+
isinstance(return_exceptions, Literal) and return_exceptions.value is True
|
|
43
|
+
):
|
|
44
|
+
return all_settled_call
|
|
45
|
+
|
|
46
|
+
# Optimized: literal False or default -> all
|
|
47
|
+
if return_exceptions is False or (
|
|
48
|
+
isinstance(return_exceptions, Literal) and return_exceptions.value is False
|
|
49
|
+
):
|
|
50
|
+
return all_call
|
|
51
|
+
|
|
52
|
+
# General case: emit ternary on the expression
|
|
53
|
+
return Ternary(
|
|
54
|
+
Binary(ctx.emit_expr(cast(Any, return_exceptions)), "===", Literal(True)),
|
|
55
|
+
all_settled_call,
|
|
56
|
+
all_call,
|
|
57
|
+
)
|
pulse/transpiler/modules/json.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"""Python json module transpilation to JavaScript JSON."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, final
|
|
5
6
|
|
|
7
|
+
from pulse.transpiler.nodes import Call, Expr, Identifier, Member
|
|
8
|
+
from pulse.transpiler.py_module import PyModule
|
|
9
|
+
from pulse.transpiler.transpiler import Transpiler
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
return JSMemberCall(JSIdentifier("JSON"), name, list(args))
|
|
11
|
+
_JSON = Identifier("JSON")
|
|
9
12
|
|
|
10
13
|
|
|
14
|
+
@final
|
|
11
15
|
class PyJson(PyModule):
|
|
12
16
|
"""Provides transpilation for Python json functions to JavaScript."""
|
|
13
17
|
|
|
14
18
|
@staticmethod
|
|
15
|
-
def dumps(obj:
|
|
16
|
-
return
|
|
19
|
+
def dumps(obj: Any, *, ctx: Transpiler) -> Expr:
|
|
20
|
+
return Call(Member(_JSON, "stringify"), [ctx.emit_expr(obj)])
|
|
17
21
|
|
|
18
22
|
@staticmethod
|
|
19
|
-
def loads(s:
|
|
20
|
-
return
|
|
23
|
+
def loads(s: Any, *, ctx: Transpiler) -> Expr:
|
|
24
|
+
return Call(Member(_JSON, "parse"), [ctx.emit_expr(s)])
|