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.
Files changed (85) hide show
  1. pulse/__init__.py +542 -562
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +0 -14
  4. pulse/cli/cmd.py +96 -80
  5. pulse/cli/dependencies.py +10 -41
  6. pulse/cli/folder_lock.py +3 -3
  7. pulse/cli/helpers.py +40 -67
  8. pulse/cli/logging.py +102 -0
  9. pulse/cli/packages.py +16 -0
  10. pulse/cli/processes.py +40 -23
  11. pulse/codegen/codegen.py +70 -35
  12. pulse/codegen/js.py +2 -4
  13. pulse/codegen/templates/route.py +94 -146
  14. pulse/component.py +115 -0
  15. pulse/components/for_.py +1 -1
  16. pulse/components/if_.py +1 -1
  17. pulse/components/react_router.py +16 -22
  18. pulse/{html → dom}/events.py +1 -1
  19. pulse/{html → dom}/props.py +6 -6
  20. pulse/{html → dom}/tags.py +11 -11
  21. pulse/dom/tags.pyi +480 -0
  22. pulse/form.py +7 -6
  23. pulse/hooks/init.py +1 -13
  24. pulse/js/__init__.py +37 -41
  25. pulse/js/__init__.pyi +22 -2
  26. pulse/js/_types.py +5 -3
  27. pulse/js/array.py +121 -38
  28. pulse/js/console.py +9 -9
  29. pulse/js/date.py +22 -19
  30. pulse/js/document.py +8 -4
  31. pulse/js/error.py +12 -14
  32. pulse/js/json.py +4 -3
  33. pulse/js/map.py +17 -7
  34. pulse/js/math.py +2 -2
  35. pulse/js/navigator.py +4 -4
  36. pulse/js/number.py +8 -8
  37. pulse/js/object.py +9 -13
  38. pulse/js/promise.py +25 -9
  39. pulse/js/regexp.py +6 -6
  40. pulse/js/set.py +20 -8
  41. pulse/js/string.py +7 -7
  42. pulse/js/weakmap.py +6 -6
  43. pulse/js/weakset.py +6 -6
  44. pulse/js/window.py +17 -14
  45. pulse/messages.py +1 -4
  46. pulse/react_component.py +3 -999
  47. pulse/render_session.py +74 -66
  48. pulse/renderer.py +311 -238
  49. pulse/routing.py +1 -10
  50. pulse/serializer.py +11 -1
  51. pulse/transpiler/__init__.py +84 -114
  52. pulse/transpiler/builtins.py +661 -343
  53. pulse/transpiler/errors.py +78 -2
  54. pulse/transpiler/function.py +463 -133
  55. pulse/transpiler/id.py +18 -0
  56. pulse/transpiler/imports.py +230 -325
  57. pulse/transpiler/js_module.py +218 -209
  58. pulse/transpiler/modules/__init__.py +16 -13
  59. pulse/transpiler/modules/asyncio.py +45 -26
  60. pulse/transpiler/modules/json.py +12 -8
  61. pulse/transpiler/modules/math.py +161 -216
  62. pulse/transpiler/modules/pulse/__init__.py +5 -0
  63. pulse/transpiler/modules/pulse/tags.py +231 -0
  64. pulse/transpiler/modules/typing.py +33 -28
  65. pulse/transpiler/nodes.py +1607 -923
  66. pulse/transpiler/py_module.py +118 -95
  67. pulse/transpiler/react_component.py +51 -0
  68. pulse/transpiler/transpiler.py +593 -437
  69. pulse/transpiler/vdom.py +255 -0
  70. {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
  71. pulse_framework-0.1.52.dist-info/RECORD +120 -0
  72. pulse/html/tags.pyi +0 -470
  73. pulse/transpiler/constants.py +0 -110
  74. pulse/transpiler/context.py +0 -26
  75. pulse/transpiler/ids.py +0 -16
  76. pulse/transpiler/modules/re.py +0 -466
  77. pulse/transpiler/modules/tags.py +0 -268
  78. pulse/transpiler/utils.py +0 -4
  79. pulse/vdom.py +0 -667
  80. pulse_framework-0.1.50.dist-info/RECORD +0 -119
  81. /pulse/{html → dom}/__init__.py +0 -0
  82. /pulse/{html → dom}/elements.py +0 -0
  83. /pulse/{html → dom}/svg.py +0 -0
  84. {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
  85. {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
@@ -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 types import FunctionType, ModuleType
14
- from typing import Any, ClassVar, Literal, TypeVar, override
13
+ from typing import Literal, override
15
14
 
16
- from pulse.transpiler.errors import JSCompilationError
15
+ from pulse.transpiler.errors import TranspileError
17
16
  from pulse.transpiler.imports import Import
18
- from pulse.transpiler.nodes import JSExpr, JSIdentifier, JSMember, JSNew
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
- # Track functions marked as constructors (by id, since we delete them)
21
- CONSTRUCTORS: set[int] = set()
54
+ @override
55
+ def emit(self, out: list[str]) -> None:
56
+ self.ctor.emit(out)
22
57
 
23
- F = TypeVar("F", bound=Callable[..., object])
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
- """Configuration for a JavaScript module binding.
81
+ class JsModule(Expr):
82
+ """Expr representing a JavaScript module binding.
29
83
 
30
84
  Attributes:
31
- name: The JavaScript identifier (e.g., "Math", "lodash")
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
- global_scope: bool = False
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 to_js_expr(self) -> JSIdentifier | Import:
53
- """Generate the appropriate JSExpr for this module.
136
+ def to_expr(self) -> Identifier | Import:
137
+ """Generate the appropriate Expr for this module.
54
138
 
55
- Returns JSIdentifier for builtins, Import for external modules.
139
+ Returns Identifier for builtins, Import for external modules.
56
140
 
57
- Raises JSCompilationError if global_scope=True (module imports are disallowed).
141
+ Raises TranspileError if name is None (module imports are disallowed).
58
142
  """
59
- if self.global_scope:
60
- module_name_lower = self.name.lower()
61
- msg = (
62
- f"Cannot import module '{self.name}' directly. "
63
- + f"Use 'from pulse.js.{module_name_lower} import ...' instead."
64
- )
65
- raise JSCompilationError(msg)
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 JSIdentifier(self.name)
157
+ return Identifier(self.name)
69
158
 
70
- if self.kind == "default":
71
- return Import.default(self.name, self.src)
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) -> JSMember | JSConstructor | JSIdentifier | Import:
75
- """Get a member of this module as a JS expression.
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 global_scope modules: returns JSIdentifier(name) directly (e.g., Set -> Set)
78
- For builtins: returns JSMember (e.g., Math.sin), or JSIdentifier if name
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 JSMember (e.g., React.useState)
81
- For external modules with "named_import" style: returns a named Import (e.g., import { useState } from "react")
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, wraps the result in JSConstructor.
171
+ If name is in constructors, returns a Class that emits `new ...(...)`.
84
172
  """
85
- expr: JSMember | JSIdentifier | Import
86
- if self.global_scope:
87
- # Global scope: members are just identifiers, not members of a module
88
- expr = JSIdentifier(name)
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 = JSIdentifier(name)
184
+ expr = Identifier(js_name)
93
185
  else:
94
- expr = JSMember(JSIdentifier(self.name), name)
186
+ expr = Member(Identifier(self.name), js_name)
95
187
  elif self.values == "named_import":
96
- expr = Import.named(name, self.src)
188
+ expr = Import(js_name, self.src)
97
189
  else:
98
- expr = JSMember(self.to_js_expr(), name)
190
+ expr = Member(self.to_expr(), js_name)
99
191
 
100
192
  if name in self.constructors:
101
- return JSConstructor(expr)
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
- def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
121
- if kwargs:
122
- raise JSCompilationError(
123
- "Keyword arguments not supported in constructor call"
124
- )
125
- return JSNew(self.ctor, [JSExpr.of(a) for a in args])
126
-
127
-
128
- # Registry: Python module -> JsModule config
129
- JS_MODULES: dict[ModuleType, JsModule] = {}
130
-
131
-
132
- def register_js_module(
133
- *,
134
- name: str,
135
- src: str | None = None,
136
- kind: Literal["default", "namespace"] = "namespace",
137
- values: Literal["member", "named_import"] = "named_import",
138
- global_scope: bool = False,
139
- ) -> None:
140
- """Register the calling Python module as a JavaScript module binding.
141
-
142
- Must be called from within the module being registered. The module is
143
- automatically detected from the call stack.
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
- This function:
146
- 1. Creates a JsModule config and adds it to JS_MODULES
147
- 2. Sets up __getattr__ on the module for dynamic attribute access
238
+ # Collect locally defined names and clean up module namespace
239
+ constructors: set[str] = set()
240
+ local_names: set[str] = set()
148
241
 
149
- Args:
150
- name: The JavaScript identifier (e.g., "Math")
151
- src: Import source path. None for builtins.
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
- Example (inside pulse/js/math.py):
161
- register_js_module(name="Math") # builtin
246
+ obj = getattr(module, attr_name)
247
+ is_local = not hasattr(obj, "__module__") or obj.__module__ == module_name
162
248
 
163
- Example (inside pulse/js/react.py):
164
- register_js_module(name="React", src="react") # namespace + named imports (default)
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
- # Collect constant names from annotations (constants are just type annotations)
240
- # before clearing them
241
- constant_names: set[str] = set()
242
- if hasattr(module, "__annotations__"):
243
- for ann_name in module.__annotations__:
244
- if not ann_name.startswith("_") and ann_name not in locally_defined_names:
245
- constant_names.add(ann_name)
246
- # Clear annotations (they're just for IDE hints, not runtime values)
247
- module.__annotations__.clear()
248
-
249
- js_module = JsModule(
250
- name=name,
251
- src=src,
252
- kind=kind,
253
- values=values,
254
- constructors=frozenset(ctor_names),
255
- global_scope=global_scope,
256
- )
257
- JS_MODULES[module] = js_module
258
-
259
- # Include constants in locally_defined_names so they're accessible via __getattr__
260
- locally_defined_names.update(constant_names)
261
-
262
- # Set up __getattr__ - all attribute access now goes through here
263
- # Capture locally_defined_names in closure
264
- _defined_names = locally_defined_names
265
-
266
- def __getattr__(name: str) -> JSMember | JSConstructor | JSIdentifier | Import:
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 and JavaScript modules for transpilation.
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.html.tags as pulse_tags
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.re import PyRe
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 register_module
19
+ from pulse.transpiler.py_module import PyModule
21
20
 
22
21
  # Register built-in Python modules
23
- register_module(asyncio_builtin, PyAsyncio)
24
- register_module(json_builtin, PyJson)
25
- register_module(math_builtin, PyMath)
26
- register_module(re_builtin, PyRe)
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 HTML tags for JSX transpilation
30
- register_module(pulse_tags, PyTags)
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
- This module provides transpilation from Python's `asyncio` module to JavaScript's `Promise` methods.
4
- """
5
-
6
- from pulse.transpiler.nodes import JSArray, JSExpr, JSIdentifier, JSMemberCall
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: JSExpr, **kwargs: JSExpr) -> JSExpr:
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
- **kwargs: Keyword arguments, including return_exceptions
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
- # Convert coros to array
25
- promises = JSArray(list(coros))
26
-
27
- # Check return_exceptions keyword argument
28
- return_exceptions = kwargs.get("return_exceptions")
29
- if return_exceptions is not None:
30
- # Check if it's a boolean true (JSBoolean(True))
31
- from pulse.transpiler.nodes import JSBoolean
32
-
33
- if isinstance(return_exceptions, JSBoolean) and return_exceptions.value:
34
- # Promise.allSettled returns results with status
35
- return JSMemberCall(JSIdentifier("Promise"), "allSettled", [promises])
36
-
37
- # Default: Promise.all fails fast on first rejection
38
- return JSMemberCall(JSIdentifier("Promise"), "all", [promises])
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
+ )
@@ -1,20 +1,24 @@
1
1
  """Python json module transpilation to JavaScript JSON."""
2
2
 
3
- from pulse.transpiler.nodes import JSExpr, JSIdentifier, JSMemberCall
4
- from pulse.transpiler.py_module import PyModule
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
- def JSONCall(name: str, *args: JSExpr) -> JSExpr:
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: JSExpr) -> JSExpr:
16
- return JSONCall("stringify", obj)
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: JSExpr) -> JSExpr:
20
- return JSONCall("parse", s)
23
+ def loads(s: Any, *, ctx: Transpiler) -> Expr:
24
+ return Call(Member(_JSON, "parse"), [ctx.emit_expr(s)])