pulse-framework 0.1.51__py3-none-any.whl → 0.1.53__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 (84) 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 -1001
  47. pulse/render_session.py +74 -66
  48. pulse/renderer.py +311 -238
  49. pulse/routing.py +1 -10
  50. pulse/transpiler/__init__.py +84 -114
  51. pulse/transpiler/builtins.py +661 -343
  52. pulse/transpiler/errors.py +78 -2
  53. pulse/transpiler/function.py +463 -133
  54. pulse/transpiler/id.py +18 -0
  55. pulse/transpiler/imports.py +230 -325
  56. pulse/transpiler/js_module.py +218 -209
  57. pulse/transpiler/modules/__init__.py +16 -13
  58. pulse/transpiler/modules/asyncio.py +45 -26
  59. pulse/transpiler/modules/json.py +12 -8
  60. pulse/transpiler/modules/math.py +161 -216
  61. pulse/transpiler/modules/pulse/__init__.py +5 -0
  62. pulse/transpiler/modules/pulse/tags.py +231 -0
  63. pulse/transpiler/modules/typing.py +33 -28
  64. pulse/transpiler/nodes.py +1607 -923
  65. pulse/transpiler/py_module.py +118 -95
  66. pulse/transpiler/react_component.py +51 -0
  67. pulse/transpiler/transpiler.py +593 -437
  68. pulse/transpiler/vdom.py +255 -0
  69. {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/METADATA +1 -1
  70. pulse_framework-0.1.53.dist-info/RECORD +120 -0
  71. pulse/html/tags.pyi +0 -470
  72. pulse/transpiler/constants.py +0 -110
  73. pulse/transpiler/context.py +0 -26
  74. pulse/transpiler/ids.py +0 -16
  75. pulse/transpiler/modules/re.py +0 -466
  76. pulse/transpiler/modules/tags.py +0 -268
  77. pulse/transpiler/utils.py +0 -4
  78. pulse/vdom.py +0 -599
  79. pulse_framework-0.1.51.dist-info/RECORD +0 -119
  80. /pulse/{html → dom}/__init__.py +0 -0
  81. /pulse/{html → dom}/elements.py +0 -0
  82. /pulse/{html → dom}/svg.py +0 -0
  83. {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/WHEEL +0 -0
  84. {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/entry_points.txt +0 -0
pulse/vdom.py DELETED
@@ -1,599 +0,0 @@
1
- """
2
- HTML library that generates UI tree nodes directly.
3
-
4
- This library provides a Python API for building UI trees that match
5
- the TypeScript UINode format exactly, eliminating the need for translation.
6
- """
7
-
8
- import functools
9
- import re
10
- import warnings
11
- from collections.abc import Callable, Iterable, Sequence
12
- from inspect import Parameter, signature
13
- from types import NoneType
14
- from typing import (
15
- Any,
16
- Generic,
17
- Literal,
18
- NamedTuple,
19
- NotRequired,
20
- ParamSpec,
21
- TypeAlias,
22
- TypedDict,
23
- final,
24
- overload,
25
- override,
26
- )
27
-
28
- from pulse.env import env
29
- from pulse.hooks.core import HookContext
30
- from pulse.hooks.init import rewrite_init_blocks
31
-
32
- # ============================================================================
33
- # Core VDOM
34
- # ============================================================================
35
-
36
-
37
- class VDOMNode(TypedDict):
38
- tag: str
39
- key: NotRequired[str]
40
- props: NotRequired[dict[str, Any]] # does not include callbacks
41
- children: "NotRequired[Sequence[VDOMNode | Primitive] | None]"
42
-
43
-
44
- class Callback(NamedTuple):
45
- fn: Callable[..., Any]
46
- n_args: int
47
-
48
-
49
- @final
50
- class Node:
51
- __slots__ = (
52
- "tag",
53
- "props",
54
- "children",
55
- "allow_children",
56
- "key",
57
- )
58
-
59
- tag: str
60
- props: dict[str, Any] | None
61
- children: "list[Element] | None"
62
- allow_children: bool
63
- key: str | None
64
-
65
- def __init__(
66
- self,
67
- tag: str,
68
- props: dict[str, Any] | None | None = None,
69
- children: "Children | None" = None,
70
- key: str | None = None,
71
- allow_children: bool = True,
72
- ):
73
- self.tag = tag
74
- self.props = props or None
75
- self.children = (
76
- _flatten_children(children, parent_name=f"<{self.tag}>")
77
- if children
78
- else None
79
- )
80
- self.allow_children = allow_children
81
- self.key = key or None
82
- if key is not None and not isinstance(key, str):
83
- raise ValueError("key must be a string or None")
84
- if not self.allow_children and children:
85
- clean_tag = clean_element_name(self.tag)
86
- raise ValueError(f"{clean_tag} cannot have children")
87
-
88
- # --- Pretty printing helpers -------------------------------------------------
89
- @override
90
- def __repr__(self) -> str:
91
- return (
92
- f"Node(tag={self.tag!r}, key={self.key!r}, props={_short_props(self.props)}, "
93
- f"children={_short_children(self.children)})"
94
- )
95
-
96
- def __getitem__(
97
- self,
98
- children_arg: "Child | tuple[Child, ...]",
99
- ):
100
- """Support indexing syntax: div()[children] or div()["text"]
101
-
102
- Children may include iterables (lists, generators) of nodes, which will
103
- be flattened during render.
104
- """
105
- if self.children:
106
- raise ValueError(f"Node already has children: {self.children}")
107
-
108
- if isinstance(children_arg, tuple):
109
- new_children = list(children_arg)
110
- else:
111
- new_children = [children_arg]
112
-
113
- return Node(
114
- tag=self.tag,
115
- props=self.props,
116
- children=new_children,
117
- key=self.key,
118
- allow_children=self.allow_children,
119
- )
120
-
121
- @staticmethod
122
- def from_vdom(
123
- vdom: "VDOM",
124
- callbacks: "Callbacks | None" = None,
125
- *,
126
- path: str = "",
127
- ) -> "Node | Primitive":
128
- """Create a Node tree from a VDOM structure.
129
-
130
- - Primitive values are returned as-is
131
- - Callbacks can be reattached by providing both `callbacks` (the
132
- callable registry) and `callback_props` (props per VDOM path)
133
- """
134
-
135
- if isinstance(vdom, (str, int, float, bool, NoneType)):
136
- return vdom
137
-
138
- tag = vdom.get("tag")
139
- props = vdom.get("props") or {}
140
- key_value = vdom.get("key")
141
-
142
- callbacks = callbacks or {}
143
- prefix = f"{path}." if path else ""
144
- prop_names: list[str] = []
145
- for key in callbacks.keys():
146
- if path:
147
- if not key.startswith(prefix):
148
- continue
149
- remainder = key[len(prefix) :]
150
- else:
151
- remainder = key
152
- if "." in remainder:
153
- continue
154
- prop_names.append(remainder)
155
- if prop_names:
156
- props = props.copy()
157
- for name in prop_names:
158
- callback_key = f"{path}.{name}" if path else name
159
- callback = callbacks.get(callback_key)
160
- if not callback:
161
- raise ValueError(f"Missing callback '{callback_key}'")
162
- props[name] = callback.fn
163
-
164
- children_value: list[Element] | None = None
165
- raw_children = vdom.get("children")
166
- if raw_children is not None:
167
- children_value = []
168
- for idx, raw_child in enumerate(raw_children):
169
- child_path = f"{path}.{idx}" if path else str(idx)
170
- children_value.append(
171
- Node.from_vdom(
172
- raw_child,
173
- callbacks=callbacks,
174
- path=child_path,
175
- )
176
- )
177
-
178
- return Node(
179
- tag=tag,
180
- props=props or None,
181
- children=children_value,
182
- key=key_value,
183
- )
184
-
185
-
186
- # ============================================================================
187
- # Tag Definition Functions
188
- # ============================================================================
189
-
190
-
191
- # --- Components ---
192
-
193
- P = ParamSpec("P")
194
-
195
-
196
- class Component(Generic[P]):
197
- fn: "Callable[P, Element]"
198
- name: str
199
- _takes_children: bool
200
-
201
- def __init__(self, fn: "Callable[P, Element]", name: str | None = None) -> None:
202
- self.fn = fn
203
- self.name = name or _infer_component_name(fn)
204
- self._takes_children = _takes_children(fn)
205
-
206
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "ComponentNode":
207
- key = kwargs.get("key")
208
- if key is not None and not isinstance(key, str):
209
- raise ValueError("key must be a string or None")
210
-
211
- # Flatten children if component accepts them via `*children` parameter
212
- if self._takes_children and args:
213
- flattened = _flatten_children(
214
- args, # pyright: ignore[reportArgumentType]
215
- parent_name=f"<{self.name}>",
216
- warn_stacklevel=4,
217
- )
218
- args = tuple(flattened) # pyright: ignore[reportAssignmentType]
219
-
220
- return ComponentNode(
221
- fn=self.fn,
222
- key=key,
223
- args=args,
224
- kwargs=kwargs,
225
- name=self.name,
226
- takes_children=self._takes_children,
227
- )
228
-
229
- @override
230
- def __repr__(self) -> str:
231
- return f"Component(name={self.name!r}, fn={_callable_qualname(self.fn)!r})"
232
-
233
- @override
234
- def __str__(self) -> str:
235
- return self.name
236
-
237
-
238
- @final
239
- class ComponentNode:
240
- __slots__ = (
241
- "fn",
242
- "args",
243
- "kwargs",
244
- "key",
245
- "name",
246
- "takes_children",
247
- "hooks",
248
- "contents",
249
- )
250
-
251
- fn: Callable[..., Any]
252
- args: tuple[Any, ...]
253
- kwargs: dict[str, Any]
254
- key: str | None
255
- name: str
256
- takes_children: bool
257
- hooks: HookContext
258
- contents: "Element | None"
259
-
260
- def __init__(
261
- self,
262
- fn: Callable[..., Any],
263
- args: tuple[Any, ...],
264
- kwargs: dict[str, Any],
265
- name: str | None = None,
266
- key: str | None = None,
267
- takes_children: bool = True,
268
- ) -> None:
269
- self.fn = fn
270
- self.args = args
271
- self.kwargs = kwargs
272
- self.key = key
273
- self.name = name or _infer_component_name(fn)
274
- self.takes_children = takes_children
275
- # Used for rendering
276
- self.contents = None
277
- self.hooks = HookContext()
278
-
279
- def __getitem__(self, children_arg: "Child | tuple[Child, ...]"):
280
- if not self.takes_children:
281
- raise TypeError(
282
- f"Component {self.name} does not accept children. "
283
- + "Update the component signature to include '*children' to allow children."
284
- )
285
- if self.args:
286
- raise ValueError(
287
- f"Component {self.name} already received positional arguments. Pass all arguments as keyword arguments in order to pass children using brackets."
288
- )
289
- if not isinstance(children_arg, tuple):
290
- children_arg = (children_arg,)
291
- # Flatten children when component accepts them via `*children` parameter
292
- flattened_children = _flatten_children(
293
- children_arg, parent_name=f"<{self.name}>", warn_stacklevel=4
294
- )
295
- return ComponentNode(
296
- fn=self.fn,
297
- args=tuple(flattened_children),
298
- kwargs=self.kwargs,
299
- name=self.name,
300
- key=self.key,
301
- takes_children=self.takes_children,
302
- )
303
-
304
- @override
305
- def __repr__(self) -> str:
306
- return (
307
- f"ComponentNode(name={self.name!r}, key={self.key!r}, "
308
- f"args={_short_args(self.args)}, kwargs={_short_props(self.kwargs)})"
309
- )
310
-
311
-
312
- @overload
313
- def component(fn: "Callable[P, Element]") -> Component[P]: ...
314
- @overload
315
- def component(
316
- fn: None = None, *, name: str | None = None
317
- ) -> "Callable[[Callable[P, Element]], Component[P]]": ...
318
-
319
-
320
- # The explicit return type is necessary for the type checker to be happy
321
- def component(
322
- fn: "Callable[P, Element] | None" = None, *, name: str | None = None
323
- ) -> "Component[P] | Callable[[Callable[P, Element]], Component[P]]":
324
- def decorator(fn: Callable[P, Element]):
325
- rewritten = rewrite_init_blocks(fn)
326
- return Component(rewritten, name)
327
-
328
- if fn is not None:
329
- return decorator(fn)
330
- return decorator
331
-
332
-
333
- Primitive = str | int | float | None
334
- Element = Node | ComponentNode | Primitive
335
- # A child can be an Element or any iterable yielding children (e.g., generators)
336
- Child: TypeAlias = Element | Iterable[Element]
337
- Children: TypeAlias = Sequence[Child]
338
-
339
- Callbacks = dict[str, Callback]
340
- VDOM: TypeAlias = VDOMNode | Primitive
341
- Props = dict[str, Any]
342
-
343
-
344
- # ============================================================================
345
- # VDOM Operations (updates sent from server to client)
346
- # ============================================================================
347
-
348
-
349
- class ReplaceOperation(TypedDict):
350
- type: Literal["replace"]
351
- path: str
352
- data: VDOM
353
-
354
-
355
- # This payload makes it easy for the client to rebuild an array of React nodes
356
- # from the previous children array:
357
- # - Allocate array of size N
358
- # - For i in 0..N-1, check the following scenarios
359
- # - i matches the next index in `new` -> use provided tree
360
- # - i matches the next index in `reuse` -> reuse previous child
361
- # - otherwise, reuse the element at the same index
362
- class ReconciliationOperation(TypedDict):
363
- type: Literal["reconciliation"]
364
- path: str
365
- N: int
366
- new: tuple[list[int], list[VDOM]]
367
- reuse: tuple[list[int], list[int]]
368
-
369
-
370
- class UpdatePropsDelta(TypedDict, total=False):
371
- # Only send changed/new keys under `set` and removed keys under `remove`
372
- set: Props
373
- remove: list[str]
374
-
375
-
376
- class UpdatePropsOperation(TypedDict):
377
- type: Literal["update_props"]
378
- path: str
379
- data: UpdatePropsDelta
380
-
381
-
382
- class PathDelta(TypedDict, total=False):
383
- add: list[str]
384
- remove: list[str]
385
-
386
-
387
- class UpdateCallbacksOperation(TypedDict):
388
- type: Literal["update_callbacks"]
389
- path: str
390
- data: PathDelta
391
-
392
-
393
- class UpdateRenderPropsOperation(TypedDict):
394
- type: Literal["update_render_props"]
395
- path: str
396
- data: PathDelta
397
-
398
-
399
- class UpdateJsExprPathsOperation(TypedDict):
400
- type: Literal["update_jsexpr_paths"]
401
- path: str
402
- data: PathDelta
403
-
404
-
405
- VDOMOperation: TypeAlias = (
406
- ReplaceOperation
407
- | UpdatePropsOperation
408
- | ReconciliationOperation
409
- | UpdateCallbacksOperation
410
- | UpdateRenderPropsOperation
411
- | UpdateJsExprPathsOperation
412
- )
413
-
414
-
415
- # ----------------------------------------------------------------------------
416
- # Component naming heuristics
417
- # ----------------------------------------------------------------------------
418
-
419
-
420
- def clean_element_name(parent_name: str) -> str:
421
- """Strip $$ prefix and hexadecimal suffix from ReactComponent tags in warning messages.
422
-
423
- ReactComponent tags are in the format <$$ComponentName_1a2b> or <$$ComponentName_1a2b.prop>.
424
- This function strips the $$ prefix and _1a2b suffix to show just the component name.
425
- """
426
-
427
- # Match ReactComponent tags: <$$ComponentName_hex> or <$$ComponentName_hex.prop>
428
- # Strip the $$ prefix and _hex suffix but keep the rest (hex digits are 0-9, a-f)
429
- return re.sub(r"\$\$([^_]+)_[0-9a-f]+", r"\1", parent_name)
430
-
431
-
432
- def _flatten_children(
433
- children: Children, *, parent_name: str, warn_stacklevel: int = 5
434
- ) -> list[Element]:
435
- """Flatten children and emit warnings for unkeyed iterables (dev mode only).
436
-
437
- Args:
438
- children: The children sequence to flatten.
439
- parent_name: Name of the parent element for error messages.
440
- warn_stacklevel: Stack level for warnings. Adjust based on call site:
441
- - 5 for Node.__init__ via tag factory (user -> tag factory -> Node.__init__ -> _flatten_children -> visit -> warn)
442
- - 4 for ComponentNode.__getitem__ or Component.__call__ (user -> method -> _flatten_children -> visit -> warn)
443
- """
444
- flat: list[Element] = []
445
- is_dev = env.pulse_env == "dev"
446
-
447
- def visit(item: Child) -> None:
448
- if isinstance(item, Iterable) and not isinstance(item, str):
449
- # If any Node/ComponentNode yielded by this iterable lacks a key,
450
- # emit a single warning for this iterable (dev mode only).
451
- missing_key = False
452
- for sub in item:
453
- if (
454
- is_dev
455
- and isinstance(sub, (Node, ComponentNode))
456
- and sub.key is None
457
- ):
458
- missing_key = True
459
- visit(sub)
460
- if missing_key:
461
- # Warn once per iterable without keys on its elements.
462
- clean_name = clean_element_name(parent_name)
463
- warnings.warn(
464
- (
465
- f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
466
- "Add a stable 'key' to each element inside iterables to improve reconciliation."
467
- ),
468
- stacklevel=warn_stacklevel,
469
- )
470
- else:
471
- # Not an iterable child: must be a Element or primitive
472
- flat.append(item)
473
-
474
- for child in children:
475
- visit(child)
476
-
477
- seen_keys: set[str] = set()
478
- for child in flat:
479
- if isinstance(child, (Node, ComponentNode)) and child.key is not None:
480
- if child.key in seen_keys:
481
- clean_name = clean_element_name(parent_name)
482
- raise ValueError(
483
- f"[Pulse] Duplicate key '{child.key}' found among children of {clean_name}. "
484
- + "Keys must be unique per sibling set."
485
- )
486
- seen_keys.add(child.key)
487
-
488
- return flat
489
-
490
-
491
- def _short_args(args: tuple[Any, ...], max_items: int = 4) -> list[str] | str:
492
- if not args:
493
- return []
494
- out: list[str] = []
495
- for a in args[: max_items - 1]:
496
- s = repr(a)
497
- if len(s) > 32:
498
- s = s[:29] + "…" + s[-1]
499
- out.append(s)
500
- if len(args) > (max_items - 1):
501
- out.append(f"…(+{len(args) - (max_items - 1)})")
502
- return out
503
-
504
-
505
- def _infer_component_name(fn: Callable[..., Any]) -> str:
506
- # Unwrap partials and single-level wrappers
507
- original = fn
508
- if isinstance(original, functools.partial):
509
- original = original.func # type: ignore[attr-defined]
510
-
511
- name: str | None = getattr(original, "__name__", None)
512
- if name and name != "<lambda>":
513
- return name
514
-
515
- qualname: str | None = getattr(original, "__qualname__", None)
516
- if qualname and "<locals>" not in qualname:
517
- # Best-effort: take the last path component
518
- return qualname.split(".")[-1]
519
-
520
- # Callable instances (classes defining __call__)
521
- cls = getattr(original, "__class__", None)
522
- if cls and getattr(cls, "__name__", None):
523
- return cls.__name__
524
-
525
- # Fallback
526
- return "Component"
527
-
528
-
529
- def _callable_qualname(fn: Callable[..., Any]) -> str:
530
- mod = getattr(fn, "__module__", None) or "__main__"
531
- qual = (
532
- getattr(fn, "__qualname__", None)
533
- or getattr(fn, "__name__", None)
534
- or "<callable>"
535
- )
536
- return f"{mod}.{qual}"
537
-
538
-
539
- def _takes_children(fn: Callable[..., Any]) -> bool:
540
- """Return True if function accepts children via `*children` parameter.
541
-
542
- Convention: A component accepts children if and only if it has a VAR_POSITIONAL
543
- parameter named "children". This convention should be documented in user-facing docs.
544
- """
545
- try:
546
- sig = signature(fn)
547
- except (ValueError, TypeError):
548
- # Builtins or callables without inspectable signature: assume no children
549
- return False
550
- for p in sig.parameters.values():
551
- if p.kind is Parameter.VAR_POSITIONAL and p.name == "children":
552
- return True
553
- return False
554
-
555
-
556
- # ----------------------------------------------------------------------------
557
- # Formatting helpers (internal)
558
- # ----------------------------------------------------------------------------
559
-
560
-
561
- def _pretty_repr(node: Element):
562
- if isinstance(node, Node):
563
- return f"<{node.tag}>"
564
- if isinstance(node, ComponentNode):
565
- return f"<{node.name}"
566
- return repr(node)
567
-
568
-
569
- def _short_props(
570
- props: dict[str, Any] | None, max_items: int = 6
571
- ) -> dict[str, Any] | str:
572
- if not props:
573
- return {}
574
- items = list(props.items())
575
- if len(items) <= max_items:
576
- return props
577
- head = dict(items[: max_items - 1])
578
- return {**head, "…": f"+{len(items) - (max_items - 1)} more"}
579
-
580
-
581
- def _short_children(
582
- children: Sequence[Child] | None, max_items: int = 4
583
- ) -> list[str] | str:
584
- if not children:
585
- return []
586
- out: list[str] = []
587
- i = 0
588
- while i < len(children) and len(out) < max_items:
589
- child = children[i]
590
- i += 1
591
- if isinstance(child, Iterable) and not isinstance(child, str):
592
- child = list(child)
593
- n_items = min(len(child), max_items - len(out))
594
- out.extend(_pretty_repr(c) for c in child[:n_items])
595
- else:
596
- out.append(_pretty_repr(child))
597
- if len(children) > (max_items - 1):
598
- out.append(f"…(+{len(children) - (max_items - 1)})")
599
- return out