pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__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 (73) hide show
  1. pulse/__init__.py +9 -23
  2. pulse/app.py +6 -25
  3. pulse/cli/processes.py +1 -0
  4. pulse/codegen/codegen.py +43 -88
  5. pulse/codegen/js.py +35 -5
  6. pulse/codegen/templates/route.py +341 -254
  7. pulse/form.py +1 -1
  8. pulse/helpers.py +51 -27
  9. pulse/hooks/core.py +2 -2
  10. pulse/hooks/effects.py +1 -1
  11. pulse/hooks/init.py +2 -1
  12. pulse/hooks/setup.py +1 -1
  13. pulse/hooks/stable.py +2 -2
  14. pulse/hooks/states.py +2 -2
  15. pulse/html/props.py +3 -2
  16. pulse/html/tags.py +135 -0
  17. pulse/html/tags.pyi +4 -0
  18. pulse/js/__init__.py +110 -0
  19. pulse/js/__init__.pyi +95 -0
  20. pulse/js/_types.py +297 -0
  21. pulse/js/array.py +253 -0
  22. pulse/js/console.py +47 -0
  23. pulse/js/date.py +113 -0
  24. pulse/js/document.py +138 -0
  25. pulse/js/error.py +139 -0
  26. pulse/js/json.py +62 -0
  27. pulse/js/map.py +84 -0
  28. pulse/js/math.py +66 -0
  29. pulse/js/navigator.py +76 -0
  30. pulse/js/number.py +54 -0
  31. pulse/js/object.py +173 -0
  32. pulse/js/promise.py +150 -0
  33. pulse/js/regexp.py +54 -0
  34. pulse/js/set.py +109 -0
  35. pulse/js/string.py +35 -0
  36. pulse/js/weakmap.py +50 -0
  37. pulse/js/weakset.py +45 -0
  38. pulse/js/window.py +199 -0
  39. pulse/messages.py +22 -3
  40. pulse/proxy.py +21 -8
  41. pulse/react_component.py +167 -14
  42. pulse/reactive_extensions.py +5 -5
  43. pulse/render_session.py +144 -34
  44. pulse/renderer.py +80 -115
  45. pulse/routing.py +1 -18
  46. pulse/transpiler/__init__.py +131 -0
  47. pulse/transpiler/builtins.py +731 -0
  48. pulse/transpiler/constants.py +110 -0
  49. pulse/transpiler/context.py +26 -0
  50. pulse/transpiler/errors.py +2 -0
  51. pulse/transpiler/function.py +250 -0
  52. pulse/transpiler/ids.py +16 -0
  53. pulse/transpiler/imports.py +409 -0
  54. pulse/transpiler/js_module.py +274 -0
  55. pulse/transpiler/modules/__init__.py +30 -0
  56. pulse/transpiler/modules/asyncio.py +38 -0
  57. pulse/transpiler/modules/json.py +20 -0
  58. pulse/transpiler/modules/math.py +320 -0
  59. pulse/transpiler/modules/re.py +466 -0
  60. pulse/transpiler/modules/tags.py +268 -0
  61. pulse/transpiler/modules/typing.py +59 -0
  62. pulse/transpiler/nodes.py +1216 -0
  63. pulse/transpiler/py_module.py +119 -0
  64. pulse/transpiler/transpiler.py +938 -0
  65. pulse/transpiler/utils.py +4 -0
  66. pulse/vdom.py +112 -6
  67. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
  68. pulse_framework-0.1.48.dist-info/RECORD +119 -0
  69. pulse/codegen/imports.py +0 -204
  70. pulse/css.py +0 -155
  71. pulse_framework-0.1.46.dist-info/RECORD +0 -80
  72. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
  73. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,466 @@
1
+ """Python re module transpilation to JavaScript RegExp.
2
+
3
+ This module provides transpilation from Python's `re` module to JavaScript's RegExp.
4
+ For direct JavaScript RegExp bindings, use `pulse.js.regexp` instead.
5
+
6
+ Supported features:
7
+ - re.match, re.search, re.fullmatch, re.sub, re.split, re.findall, re.compile
8
+ - Flags: re.I, re.M, re.S, re.U (re.VERBOSE/re.X is NOT supported)
9
+ - Named groups: (?P<name>...) → (?<name>...)
10
+ - Named backrefs: (?P=name) → \\k<name>
11
+ - Replacement backrefs: \\g<name> → $<name>, \\1 → $1
12
+
13
+ Limitations:
14
+ - re.VERBOSE (re.X) is not supported - no JS equivalent
15
+ - Conditional patterns (?(id)yes|no) are not supported
16
+ - \\A and \\Z have different semantics with multiline mode
17
+ """
18
+
19
+ # pyright: reportUnannotatedClassAttribute=false
20
+
21
+ import re as re_module
22
+
23
+ from pulse.transpiler.constants import jsify
24
+ from pulse.transpiler.errors import JSCompilationError
25
+ from pulse.transpiler.nodes import (
26
+ JSArray,
27
+ JSArrowFunction,
28
+ JSExpr,
29
+ JSIdentifier,
30
+ JSMemberCall,
31
+ JSNew,
32
+ JSNumber,
33
+ JSSpread,
34
+ JSString,
35
+ JSSubscript,
36
+ )
37
+ from pulse.transpiler.py_module import PyModule
38
+
39
+
40
+ def _convert_pattern(pattern: str) -> str:
41
+ """Convert Python regex syntax to JavaScript regex syntax.
42
+
43
+ Handles:
44
+ - (?P<name>...) → (?<name>...)
45
+ - (?P=name) → \\k<name>
46
+ """
47
+ import re
48
+
49
+ # Convert named groups: (?P<name>...) → (?<name>...)
50
+ result = re.sub(r"\(\?P<([^>]+)>", r"(?<\1>", pattern)
51
+
52
+ # Convert named backreferences: (?P=name) → \k<name>
53
+ result = re.sub(r"\(\?P=([^)]+)\)", r"\\k<\1>", result)
54
+
55
+ return result
56
+
57
+
58
+ def _convert_replacement(replacement: str) -> str:
59
+ """Convert Python replacement string syntax to JavaScript.
60
+
61
+ Handles:
62
+ - \\g<name> → $<name>
63
+ - \\g<0>, \\g<1>, etc. → $0, $1, etc.
64
+ - \\1, \\2, etc. → $1, $2, etc. (already handled by JS)
65
+ """
66
+ import re
67
+
68
+ # Convert named group references: \g<name> → $<name>
69
+ result = re.sub(r"\\g<([^>]+)>", r"$<\1>", replacement)
70
+
71
+ # Convert numeric group references: \1 → $1 (for replacement strings)
72
+ # Note: In Python replacement, \1 means group 1. In JS, it's $1.
73
+ result = re.sub(r"\\([1-9][0-9]*)", r"$\1", result)
74
+
75
+ return result
76
+
77
+
78
+ def _get_pattern_string(pattern: JSExpr) -> str | None:
79
+ """Extract the string value from a JSString pattern, or None if dynamic."""
80
+ if isinstance(pattern, JSString):
81
+ return pattern.value
82
+ return None
83
+
84
+
85
+ def _make_regexp(
86
+ pattern: JSExpr,
87
+ flags: str = "",
88
+ *,
89
+ anchor_start: bool = False,
90
+ anchor_both: bool = False,
91
+ ) -> JSExpr:
92
+ """Create a new RegExp expression, optionally converting the pattern."""
93
+ pattern_str = _get_pattern_string(pattern)
94
+
95
+ if pattern_str is not None:
96
+ # Static pattern - convert at transpile time
97
+ converted = _convert_pattern(pattern_str)
98
+ if anchor_start and not converted.startswith("^"):
99
+ converted = "^" + converted
100
+ if anchor_both:
101
+ if not converted.startswith("^"):
102
+ converted = "^" + converted
103
+ if not converted.endswith("$"):
104
+ converted = converted + "$"
105
+ pattern = JSString(converted)
106
+
107
+ args: list[JSExpr] = [pattern]
108
+ if flags:
109
+ args.append(JSString(flags))
110
+
111
+ return JSNew(JSIdentifier("RegExp"), args)
112
+
113
+
114
+ # Flag constants - these emit their JS equivalent flag character
115
+ class _FlagExpr(JSExpr):
116
+ """A regex flag that emits as a string character."""
117
+
118
+ def __init__(self, flag: str, name: str):
119
+ self.flag = flag
120
+ self.name = name
121
+
122
+ def emit(self) -> str: # pyright: ignore[reportImplicitOverride]
123
+ return f'"{self.flag}"'
124
+
125
+
126
+ class PyRe(PyModule):
127
+ """Provides transpilation for Python re functions to JavaScript RegExp."""
128
+
129
+ # Flag constants
130
+ I = _FlagExpr("i", "IGNORECASE") # noqa: E741
131
+ IGNORECASE = _FlagExpr("i", "IGNORECASE")
132
+ M = _FlagExpr("m", "MULTILINE")
133
+ MULTILINE = _FlagExpr("m", "MULTILINE")
134
+ S = _FlagExpr("s", "DOTALL")
135
+ DOTALL = _FlagExpr("s", "DOTALL")
136
+ U = _FlagExpr("u", "UNICODE")
137
+ UNICODE = _FlagExpr("u", "UNICODE")
138
+
139
+ # Unsupported flags - will error at transpile time
140
+ @staticmethod
141
+ def _unsupported_flag(name: str) -> JSExpr:
142
+ raise JSCompilationError(
143
+ f"re.{name} (VERBOSE) flag is not supported - no JavaScript equivalent"
144
+ )
145
+
146
+ X = property(lambda self: PyRe._unsupported_flag("X"))
147
+ VERBOSE = property(lambda self: PyRe._unsupported_flag("VERBOSE"))
148
+ A = property(lambda self: PyRe._unsupported_flag("A"))
149
+ ASCII = property(lambda self: PyRe._unsupported_flag("ASCII"))
150
+
151
+ @staticmethod
152
+ def compile(
153
+ pattern: str | JSExpr,
154
+ flags: int | JSExpr = 0,
155
+ ) -> JSExpr:
156
+ """Compile a pattern into a RegExp object.
157
+
158
+ re.compile(pattern) → new RegExp(pattern)
159
+ re.compile(pattern, re.I) → new RegExp(pattern, "i")
160
+ """
161
+ pattern_expr = jsify(pattern)
162
+ flag_str = ""
163
+
164
+ if isinstance(flags, _FlagExpr):
165
+ flag_str = flags.flag
166
+ elif isinstance(flags, int) and flags != 0:
167
+ # Handle Python flag integers at transpile time
168
+ if flags & re_module.IGNORECASE:
169
+ flag_str += "i"
170
+ if flags & re_module.MULTILINE:
171
+ flag_str += "m"
172
+ if flags & re_module.DOTALL:
173
+ flag_str += "s"
174
+ if flags & re_module.UNICODE:
175
+ flag_str += "u"
176
+ if flags & re_module.VERBOSE:
177
+ raise JSCompilationError(
178
+ "re.VERBOSE flag is not supported - no JavaScript equivalent"
179
+ )
180
+ if flags & re_module.ASCII:
181
+ raise JSCompilationError(
182
+ "re.ASCII flag is not supported in JavaScript RegExp"
183
+ )
184
+
185
+ return _make_regexp(pattern_expr, flag_str)
186
+
187
+ @staticmethod
188
+ def match(
189
+ pattern: str | JSExpr,
190
+ string: str | JSExpr,
191
+ flags: int | JSExpr = 0,
192
+ ) -> JSExpr:
193
+ """Match pattern at the beginning of string.
194
+
195
+ re.match(pattern, string) → string.match(new RegExp("^" + pattern))
196
+
197
+ Returns match array or null.
198
+ """
199
+ pattern_expr = jsify(pattern)
200
+ string_expr = jsify(string)
201
+
202
+ flag_str = ""
203
+ if isinstance(flags, _FlagExpr):
204
+ flag_str = flags.flag
205
+ elif isinstance(flags, int) and flags != 0:
206
+ if flags & re_module.IGNORECASE:
207
+ flag_str += "i"
208
+ if flags & re_module.MULTILINE:
209
+ flag_str += "m"
210
+ if flags & re_module.DOTALL:
211
+ flag_str += "s"
212
+
213
+ regexp = _make_regexp(pattern_expr, flag_str, anchor_start=True)
214
+ return JSMemberCall(string_expr, "match", [regexp])
215
+
216
+ @staticmethod
217
+ def fullmatch(
218
+ pattern: str | JSExpr,
219
+ string: str | JSExpr,
220
+ flags: int | JSExpr = 0,
221
+ ) -> JSExpr:
222
+ """Match pattern against the entire string.
223
+
224
+ re.fullmatch(pattern, string) → string.match(new RegExp("^pattern$"))
225
+ """
226
+ pattern_expr = jsify(pattern)
227
+ string_expr = jsify(string)
228
+
229
+ flag_str = ""
230
+ if isinstance(flags, _FlagExpr):
231
+ flag_str = flags.flag
232
+ elif isinstance(flags, int) and flags != 0:
233
+ if flags & re_module.IGNORECASE:
234
+ flag_str += "i"
235
+ if flags & re_module.MULTILINE:
236
+ flag_str += "m"
237
+ if flags & re_module.DOTALL:
238
+ flag_str += "s"
239
+
240
+ regexp = _make_regexp(pattern_expr, flag_str, anchor_both=True)
241
+ return JSMemberCall(string_expr, "match", [regexp])
242
+
243
+ @staticmethod
244
+ def search(
245
+ pattern: str | JSExpr,
246
+ string: str | JSExpr,
247
+ flags: int | JSExpr = 0,
248
+ ) -> JSExpr:
249
+ """Search for pattern anywhere in string.
250
+
251
+ re.search(pattern, string) → new RegExp(pattern).exec(string)
252
+
253
+ Returns match array or null.
254
+ """
255
+ pattern_expr = jsify(pattern)
256
+ string_expr = jsify(string)
257
+
258
+ flag_str = ""
259
+ if isinstance(flags, _FlagExpr):
260
+ flag_str = flags.flag
261
+ elif isinstance(flags, int) and flags != 0:
262
+ if flags & re_module.IGNORECASE:
263
+ flag_str += "i"
264
+ if flags & re_module.MULTILINE:
265
+ flag_str += "m"
266
+ if flags & re_module.DOTALL:
267
+ flag_str += "s"
268
+
269
+ regexp = _make_regexp(pattern_expr, flag_str)
270
+ return JSMemberCall(regexp, "exec", [string_expr])
271
+
272
+ @staticmethod
273
+ def sub(
274
+ pattern: str | JSExpr,
275
+ repl: str | JSExpr,
276
+ string: str | JSExpr,
277
+ count: int | JSExpr = 0,
278
+ flags: int | JSExpr = 0,
279
+ ) -> JSExpr:
280
+ """Replace occurrences of pattern in string.
281
+
282
+ re.sub(pattern, repl, string) → string.replace(new RegExp(pattern, "g"), repl)
283
+ re.sub(pattern, repl, string, count=1) → string.replace(new RegExp(pattern), repl)
284
+ """
285
+ pattern_expr = jsify(pattern)
286
+ string_expr = jsify(string)
287
+ repl_expr = jsify(repl)
288
+
289
+ # Convert replacement string if it's a literal
290
+ if isinstance(repl_expr, JSString):
291
+ repl_expr = JSString(_convert_replacement(repl_expr.value))
292
+
293
+ # Determine flags
294
+ flag_str = ""
295
+ if isinstance(flags, _FlagExpr):
296
+ flag_str = flags.flag
297
+ elif isinstance(flags, int) and flags != 0:
298
+ if flags & re_module.IGNORECASE:
299
+ flag_str += "i"
300
+ if flags & re_module.MULTILINE:
301
+ flag_str += "m"
302
+ if flags & re_module.DOTALL:
303
+ flag_str += "s"
304
+
305
+ # Handle count parameter
306
+ use_global = True
307
+ count_val: int | None = None
308
+ if isinstance(count, int):
309
+ count_val = count
310
+ elif isinstance(count, JSNumber) and isinstance(count.value, int):
311
+ count_val = count.value
312
+
313
+ if count_val is not None:
314
+ if count_val == 1:
315
+ use_global = False
316
+ elif count_val > 1:
317
+ raise JSCompilationError(
318
+ "re.sub with count > 1 is not directly supported in JavaScript. Use count=0 (replace all) or count=1 (replace first)."
319
+ )
320
+ elif isinstance(count, JSExpr):
321
+ # Dynamic count - need runtime handling
322
+ raise JSCompilationError(
323
+ "Dynamic count in re.sub is not supported. Use literal 0 or 1."
324
+ )
325
+
326
+ if use_global and "g" not in flag_str:
327
+ flag_str = "g" + flag_str
328
+
329
+ regexp = _make_regexp(pattern_expr, flag_str)
330
+ return JSMemberCall(string_expr, "replace", [regexp, repl_expr])
331
+
332
+ @staticmethod
333
+ def split(
334
+ pattern: str | JSExpr,
335
+ string: str | JSExpr,
336
+ maxsplit: int | JSExpr = 0,
337
+ flags: int | JSExpr = 0,
338
+ ) -> JSExpr:
339
+ """Split string by pattern.
340
+
341
+ re.split(pattern, string) → string.split(new RegExp(pattern))
342
+
343
+ Note: maxsplit > 0 requires runtime handling and is limited.
344
+ """
345
+ pattern_expr = jsify(pattern)
346
+ string_expr = jsify(string)
347
+
348
+ flag_str = ""
349
+ if isinstance(flags, _FlagExpr):
350
+ flag_str = flags.flag
351
+ elif isinstance(flags, int) and flags != 0:
352
+ if flags & re_module.IGNORECASE:
353
+ flag_str += "i"
354
+ if flags & re_module.MULTILINE:
355
+ flag_str += "m"
356
+ if flags & re_module.DOTALL:
357
+ flag_str += "s"
358
+
359
+ regexp = _make_regexp(pattern_expr, flag_str)
360
+
361
+ # Extract maxsplit value
362
+ maxsplit_val: int | None = None
363
+ if isinstance(maxsplit, int):
364
+ maxsplit_val = maxsplit
365
+ elif isinstance(maxsplit, JSNumber) and isinstance(maxsplit.value, int):
366
+ maxsplit_val = maxsplit.value
367
+
368
+ if maxsplit_val is not None:
369
+ if maxsplit_val == 0:
370
+ # No limit
371
+ return JSMemberCall(string_expr, "split", [regexp])
372
+ elif maxsplit_val > 0:
373
+ # JS split limit is different from Python's maxsplit
374
+ # Python: maxsplit=2 means at most 2 splits, resulting in 3 parts
375
+ # JS: limit=3 means at most 3 parts
376
+ # So we add 1 to convert
377
+ return JSMemberCall(
378
+ string_expr, "split", [regexp, JSNumber(maxsplit_val + 1)]
379
+ )
380
+
381
+ raise JSCompilationError(
382
+ "Dynamic maxsplit in re.split is not supported. Use literal value."
383
+ )
384
+
385
+ @staticmethod
386
+ def findall(
387
+ pattern: str | JSExpr,
388
+ string: str | JSExpr,
389
+ flags: int | JSExpr = 0,
390
+ ) -> JSExpr:
391
+ """Find all matches of pattern in string.
392
+
393
+ re.findall(pattern, string) → [...string.matchAll(new RegExp(pattern, "g"))].map(m => m[0])
394
+
395
+ Note: Returns array of matched strings (not groups).
396
+ For patterns with groups, this returns the full match, not the groups.
397
+ """
398
+ pattern_expr = jsify(pattern)
399
+ string_expr = jsify(string)
400
+
401
+ flag_str = "g" # Always need global for matchAll
402
+ if isinstance(flags, _FlagExpr):
403
+ flag_str += flags.flag
404
+ elif isinstance(flags, int) and flags != 0:
405
+ if flags & re_module.IGNORECASE:
406
+ flag_str += "i"
407
+ if flags & re_module.MULTILINE:
408
+ flag_str += "m"
409
+ if flags & re_module.DOTALL:
410
+ flag_str += "s"
411
+
412
+ regexp = _make_regexp(pattern_expr, flag_str)
413
+
414
+ # [...string.matchAll(regexp)].map(m => m[0])
415
+ match_all = JSMemberCall(string_expr, "matchAll", [regexp])
416
+ spread_array = JSArray([JSSpread(match_all)])
417
+ # Arrow: m => m[0]
418
+ arrow = JSArrowFunction("m", JSSubscript(JSIdentifier("m"), JSNumber(0)))
419
+ return JSMemberCall(spread_array, "map", [arrow])
420
+
421
+ @staticmethod
422
+ def test(
423
+ pattern: str | JSExpr,
424
+ string: str | JSExpr,
425
+ flags: int | JSExpr = 0,
426
+ ) -> JSExpr:
427
+ """Test if pattern matches anywhere in string.
428
+
429
+ This is a convenience method (not in Python's re module) that maps to RegExp.test().
430
+
431
+ re.test(pattern, string) → new RegExp(pattern).test(string)
432
+
433
+ Returns boolean.
434
+ """
435
+ pattern_expr = jsify(pattern)
436
+ string_expr = jsify(string)
437
+
438
+ flag_str = ""
439
+ if isinstance(flags, _FlagExpr):
440
+ flag_str = flags.flag
441
+ elif isinstance(flags, int) and flags != 0:
442
+ if flags & re_module.IGNORECASE:
443
+ flag_str += "i"
444
+ if flags & re_module.MULTILINE:
445
+ flag_str += "m"
446
+ if flags & re_module.DOTALL:
447
+ flag_str += "s"
448
+
449
+ regexp = _make_regexp(pattern_expr, flag_str)
450
+ return JSMemberCall(regexp, "test", [string_expr])
451
+
452
+ @staticmethod
453
+ def escape(pattern: str | JSExpr) -> JSExpr:
454
+ """Escape special regex characters in pattern.
455
+
456
+ re.escape(string) → string.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")
457
+ """
458
+ pattern_expr = jsify(pattern)
459
+
460
+ # The escape regex pattern for JS
461
+ escape_regexp = JSNew(
462
+ JSIdentifier("RegExp"),
463
+ [JSString("[.*+?^${}()|[\\]\\\\]"), JSString("g")],
464
+ )
465
+
466
+ return JSMemberCall(pattern_expr, "replace", [escape_regexp, JSString("\\$&")])