pulse-framework 0.1.53__py3-none-any.whl → 0.1.55__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 +3 -3
- pulse/app.py +34 -20
- pulse/code_analysis.py +38 -0
- pulse/codegen/codegen.py +18 -50
- pulse/codegen/templates/route.py +100 -56
- pulse/component.py +24 -6
- pulse/components/for_.py +17 -2
- pulse/cookies.py +38 -2
- pulse/env.py +4 -4
- pulse/hooks/init.py +174 -14
- pulse/hooks/state.py +105 -0
- pulse/js/__init__.py +12 -9
- pulse/js/obj.py +79 -0
- pulse/js/pulse.py +112 -0
- pulse/js/react.py +457 -0
- pulse/messages.py +13 -13
- pulse/proxy.py +18 -5
- pulse/render_session.py +282 -266
- pulse/renderer.py +36 -73
- pulse/serializer.py +5 -2
- pulse/transpiler/__init__.py +13 -0
- pulse/transpiler/assets.py +66 -0
- pulse/transpiler/builtins.py +0 -20
- pulse/transpiler/dynamic_import.py +131 -0
- pulse/transpiler/emit_context.py +49 -0
- pulse/transpiler/errors.py +29 -11
- pulse/transpiler/function.py +36 -5
- pulse/transpiler/imports.py +33 -27
- pulse/transpiler/js_module.py +73 -20
- pulse/transpiler/modules/pulse/tags.py +35 -15
- pulse/transpiler/nodes.py +121 -36
- pulse/transpiler/py_module.py +1 -1
- pulse/transpiler/react_component.py +4 -11
- pulse/transpiler/transpiler.py +32 -26
- pulse/user_session.py +10 -0
- pulse_framework-0.1.55.dist-info/METADATA +196 -0
- {pulse_framework-0.1.53.dist-info → pulse_framework-0.1.55.dist-info}/RECORD +39 -32
- pulse/hooks/states.py +0 -285
- pulse_framework-0.1.53.dist-info/METADATA +0 -18
- {pulse_framework-0.1.53.dist-info → pulse_framework-0.1.55.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.53.dist-info → pulse_framework-0.1.55.dist-info}/entry_points.txt +0 -0
pulse/transpiler/nodes.py
CHANGED
|
@@ -88,19 +88,23 @@ class Expr(ABC):
|
|
|
88
88
|
def transpile_call(
|
|
89
89
|
self,
|
|
90
90
|
args: list[ast.expr],
|
|
91
|
-
|
|
91
|
+
keywords: list[ast.keyword],
|
|
92
92
|
ctx: Transpiler,
|
|
93
93
|
) -> Expr:
|
|
94
94
|
"""Called when this expression is used as a function: expr(args).
|
|
95
95
|
|
|
96
96
|
Override to customize call behavior.
|
|
97
|
-
Default
|
|
97
|
+
Default emits a Call expression with args transpiled.
|
|
98
98
|
|
|
99
|
-
Args and
|
|
99
|
+
Args and keywords are raw Python AST nodes (not yet transpiled).
|
|
100
100
|
Use ctx.emit_expr() to convert them to Expr as needed.
|
|
101
|
+
Keywords with kw.arg=None are **spread syntax.
|
|
101
102
|
"""
|
|
102
|
-
if
|
|
103
|
-
|
|
103
|
+
if keywords:
|
|
104
|
+
has_spread = any(kw.arg is None for kw in keywords)
|
|
105
|
+
if has_spread:
|
|
106
|
+
raise TranspileError("Spread (**expr) not supported in this call")
|
|
107
|
+
raise TranspileError("Keyword arguments not supported in call")
|
|
104
108
|
return Call(self, [ctx.emit_expr(a) for a in args])
|
|
105
109
|
|
|
106
110
|
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
@@ -341,10 +345,10 @@ class ExprWrapper(Expr):
|
|
|
341
345
|
def transpile_call(
|
|
342
346
|
self,
|
|
343
347
|
args: list[ast.expr],
|
|
344
|
-
|
|
348
|
+
keywords: list[ast.keyword],
|
|
345
349
|
ctx: Transpiler,
|
|
346
350
|
) -> Expr:
|
|
347
|
-
return self.expr.transpile_call(args,
|
|
351
|
+
return self.expr.transpile_call(args, keywords, ctx)
|
|
348
352
|
|
|
349
353
|
@override
|
|
350
354
|
def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
|
|
@@ -395,28 +399,33 @@ class Jsx(ExprWrapper):
|
|
|
395
399
|
def transpile_call(
|
|
396
400
|
self,
|
|
397
401
|
args: list[ast.expr],
|
|
398
|
-
|
|
402
|
+
keywords: list[ast.keyword],
|
|
399
403
|
ctx: "Transpiler",
|
|
400
404
|
) -> Expr:
|
|
401
405
|
"""Transpile a call to this JSX wrapper into an Element.
|
|
402
406
|
|
|
403
407
|
Positional args become children, keyword args become props.
|
|
404
|
-
The `key` kwarg is extracted specially.
|
|
408
|
+
The `key` kwarg is extracted specially. Spread (**expr) is supported.
|
|
405
409
|
"""
|
|
406
410
|
children: list[Node] = [ctx.emit_expr(a) for a in args]
|
|
407
411
|
|
|
408
|
-
props:
|
|
412
|
+
props: list[tuple[str, Prop] | Spread] = []
|
|
409
413
|
key: str | Expr | None = None
|
|
410
|
-
for
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if isinstance(v, Literal) and isinstance(v.value, str):
|
|
415
|
-
key = v.value # Optimize string literals
|
|
416
|
-
else:
|
|
417
|
-
key = v # Keep as expression
|
|
414
|
+
for kw in keywords:
|
|
415
|
+
if kw.arg is None:
|
|
416
|
+
# **spread syntax
|
|
417
|
+
props.append(spread_dict(ctx.emit_expr(kw.value)))
|
|
418
418
|
else:
|
|
419
|
-
|
|
419
|
+
k = kw.arg
|
|
420
|
+
v = ctx.emit_expr(kw.value)
|
|
421
|
+
if k == "key":
|
|
422
|
+
# Accept any expression as key for transpilation
|
|
423
|
+
if isinstance(v, Literal) and isinstance(v.value, str):
|
|
424
|
+
key = v.value # Optimize string literals
|
|
425
|
+
else:
|
|
426
|
+
key = v # Keep as expression
|
|
427
|
+
else:
|
|
428
|
+
props.append((k, v))
|
|
420
429
|
|
|
421
430
|
return Element(
|
|
422
431
|
tag=self.expr,
|
|
@@ -435,7 +444,7 @@ class Jsx(ExprWrapper):
|
|
|
435
444
|
"""
|
|
436
445
|
|
|
437
446
|
# Normal call: build Element
|
|
438
|
-
props: dict[str,
|
|
447
|
+
props: dict[str, Any] = {}
|
|
439
448
|
key: str | None = None
|
|
440
449
|
children: list[Node] = list(args)
|
|
441
450
|
|
|
@@ -490,7 +499,9 @@ class Value(Expr):
|
|
|
490
499
|
|
|
491
500
|
@override
|
|
492
501
|
def render(self) -> VDOMNode:
|
|
493
|
-
raise TypeError(
|
|
502
|
+
raise TypeError(
|
|
503
|
+
"Value cannot be rendered as VDOMExpr; unwrap with .value instead"
|
|
504
|
+
)
|
|
494
505
|
|
|
495
506
|
|
|
496
507
|
class Element(Expr):
|
|
@@ -501,19 +512,23 @@ class Element(Expr):
|
|
|
501
512
|
- "div", "span", etc.: HTML element
|
|
502
513
|
- "$$ComponentId": Client component (registered in JS registry)
|
|
503
514
|
- Expr (Import, Member, etc.): Direct component reference for transpilation
|
|
515
|
+
|
|
516
|
+
Props can be either:
|
|
517
|
+
- tuple[str, Prop]: key-value pair
|
|
518
|
+
- Spread: spread expression (...expr)
|
|
504
519
|
"""
|
|
505
520
|
|
|
506
521
|
__slots__: tuple[str, ...] = ("tag", "props", "children", "key")
|
|
507
522
|
|
|
508
523
|
tag: str | Expr
|
|
509
|
-
props: dict[str, Any] | None
|
|
524
|
+
props: Sequence[tuple[str, Prop] | Spread] | dict[str, Any] | None
|
|
510
525
|
children: Sequence[Node] | None
|
|
511
526
|
key: str | Expr | None
|
|
512
527
|
|
|
513
528
|
def __init__(
|
|
514
529
|
self,
|
|
515
530
|
tag: str | Expr,
|
|
516
|
-
props: dict[str, Any] | None = None,
|
|
531
|
+
props: Sequence[tuple[str, Prop] | Spread] | dict[str, Any] | None = None,
|
|
517
532
|
children: Sequence[Node] | None = None,
|
|
518
533
|
key: str | Expr | None = None,
|
|
519
534
|
) -> None:
|
|
@@ -534,8 +549,6 @@ class Element(Expr):
|
|
|
534
549
|
warn_stacklevel=5,
|
|
535
550
|
)
|
|
536
551
|
self.key = key
|
|
537
|
-
if self.key is None and self.props:
|
|
538
|
-
self.key = self.props.pop("key", None)
|
|
539
552
|
|
|
540
553
|
def _emit_key(self, out: list[str]) -> None:
|
|
541
554
|
"""Emit key prop (string or expression)."""
|
|
@@ -583,10 +596,24 @@ class Element(Expr):
|
|
|
583
596
|
if self.key is not None:
|
|
584
597
|
self._emit_key(props_out)
|
|
585
598
|
if self.props:
|
|
586
|
-
|
|
599
|
+
# Handle both dict (from render path) and sequence (from transpilation)
|
|
600
|
+
# Dict case: items() yields tuple[str, Any], never Spread
|
|
601
|
+
# Sequence case: already list[tuple[str, Prop] | Spread]
|
|
602
|
+
props_iter: Iterable[tuple[str, Any]] | Sequence[tuple[str, Prop] | Spread]
|
|
603
|
+
if isinstance(self.props, dict):
|
|
604
|
+
props_iter = self.props.items()
|
|
605
|
+
else:
|
|
606
|
+
props_iter = self.props
|
|
607
|
+
for prop in props_iter:
|
|
587
608
|
if props_out:
|
|
588
609
|
props_out.append(" ")
|
|
589
|
-
|
|
610
|
+
if isinstance(prop, Spread):
|
|
611
|
+
props_out.append("{...")
|
|
612
|
+
prop.expr.emit(props_out)
|
|
613
|
+
props_out.append("}")
|
|
614
|
+
else:
|
|
615
|
+
name, value = prop
|
|
616
|
+
_emit_jsx_prop(name, value, props_out)
|
|
590
617
|
|
|
591
618
|
# Build children into a separate buffer to check if empty
|
|
592
619
|
children_out: list[str] = []
|
|
@@ -682,6 +709,27 @@ class Element(Expr):
|
|
|
682
709
|
key=self.key,
|
|
683
710
|
)
|
|
684
711
|
|
|
712
|
+
def props_dict(self) -> dict[str, Any]:
|
|
713
|
+
"""Convert props to dict for rendering.
|
|
714
|
+
|
|
715
|
+
Raises TypeError if props contain Spread (only valid in transpilation).
|
|
716
|
+
"""
|
|
717
|
+
if not self.props:
|
|
718
|
+
return {}
|
|
719
|
+
# Already a dict (from renderer reconciliation)
|
|
720
|
+
if isinstance(self.props, dict):
|
|
721
|
+
return self.props
|
|
722
|
+
# Sequence of (key, value) pairs or Spread
|
|
723
|
+
result: dict[str, Any] = {}
|
|
724
|
+
for prop in self.props:
|
|
725
|
+
if isinstance(prop, Spread):
|
|
726
|
+
raise TypeError(
|
|
727
|
+
"Element with spread props cannot be rendered; spread is only valid during transpilation"
|
|
728
|
+
)
|
|
729
|
+
k, v = prop
|
|
730
|
+
result[k] = v
|
|
731
|
+
return result
|
|
732
|
+
|
|
685
733
|
@override
|
|
686
734
|
def render(self) -> VDOMNode:
|
|
687
735
|
"""Element rendering is handled by Renderer.render_node(), not render().
|
|
@@ -938,25 +986,40 @@ class Array(Expr):
|
|
|
938
986
|
|
|
939
987
|
@dataclass(slots=True)
|
|
940
988
|
class Object(Expr):
|
|
941
|
-
"""JS object: { key: value }
|
|
989
|
+
"""JS object: { key: value, ...spread }
|
|
990
|
+
|
|
991
|
+
Props can be either:
|
|
992
|
+
- tuple[str, Expr]: key-value pair
|
|
993
|
+
- Spread: spread expression (...expr)
|
|
994
|
+
"""
|
|
942
995
|
|
|
943
|
-
props: Sequence[tuple[str, Expr]]
|
|
996
|
+
props: Sequence[tuple[str, Expr] | Spread]
|
|
944
997
|
|
|
945
998
|
@override
|
|
946
999
|
def emit(self, out: list[str]) -> None:
|
|
947
1000
|
out.append("{")
|
|
948
|
-
for i,
|
|
1001
|
+
for i, prop in enumerate(self.props):
|
|
949
1002
|
if i > 0:
|
|
950
1003
|
out.append(", ")
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1004
|
+
if isinstance(prop, Spread):
|
|
1005
|
+
prop.emit(out)
|
|
1006
|
+
else:
|
|
1007
|
+
k, v = prop
|
|
1008
|
+
out.append('"')
|
|
1009
|
+
out.append(_escape_string(k))
|
|
1010
|
+
out.append('": ')
|
|
1011
|
+
v.emit(out)
|
|
955
1012
|
out.append("}")
|
|
956
1013
|
|
|
957
1014
|
@override
|
|
958
1015
|
def render(self) -> VDOMNode:
|
|
959
|
-
|
|
1016
|
+
rendered_props: dict[str, VDOMNode] = {}
|
|
1017
|
+
for prop in self.props:
|
|
1018
|
+
if isinstance(prop, Spread):
|
|
1019
|
+
raise TypeError("Object spread cannot be rendered to VDOM")
|
|
1020
|
+
k, v = prop
|
|
1021
|
+
rendered_props[k] = v.render()
|
|
1022
|
+
return {"t": "object", "props": rendered_props}
|
|
960
1023
|
|
|
961
1024
|
|
|
962
1025
|
@dataclass(slots=True)
|
|
@@ -1214,6 +1277,21 @@ class Spread(Expr):
|
|
|
1214
1277
|
raise TypeError("Spread cannot be rendered as VDOMExpr directly")
|
|
1215
1278
|
|
|
1216
1279
|
|
|
1280
|
+
def spread_dict(expr: Expr) -> Spread:
|
|
1281
|
+
"""Wrap a spread expression with Map-to-object conversion.
|
|
1282
|
+
|
|
1283
|
+
Python dicts transpile to Map, which has no enumerable own properties.
|
|
1284
|
+
This wraps the spread with an IIFE that converts Map to object:
|
|
1285
|
+
(...expr) -> ...($s => $s instanceof Map ? Object.fromEntries($s) : $s)(expr)
|
|
1286
|
+
|
|
1287
|
+
The IIFE ensures expr is evaluated only once.
|
|
1288
|
+
"""
|
|
1289
|
+
s = Identifier("$s")
|
|
1290
|
+
is_map = Binary(s, "instanceof", Identifier("Map"))
|
|
1291
|
+
as_obj = Call(Member(Identifier("Object"), "fromEntries"), [s])
|
|
1292
|
+
return Spread(Call(Arrow(["$s"], Ternary(is_map, as_obj, s)), [expr]))
|
|
1293
|
+
|
|
1294
|
+
|
|
1217
1295
|
@dataclass(slots=True)
|
|
1218
1296
|
class New(Expr):
|
|
1219
1297
|
"""JS new expression: new Ctor(args)"""
|
|
@@ -1272,9 +1350,16 @@ class Transformer(Expr, Generic[_F]):
|
|
|
1272
1350
|
def transpile_call(
|
|
1273
1351
|
self,
|
|
1274
1352
|
args: list[ast.expr],
|
|
1275
|
-
|
|
1353
|
+
keywords: list[ast.keyword],
|
|
1276
1354
|
ctx: Transpiler,
|
|
1277
1355
|
) -> Expr:
|
|
1356
|
+
# Convert keywords to dict, reject spreads
|
|
1357
|
+
kwargs: dict[str, ast.expr] = {}
|
|
1358
|
+
for kw in keywords:
|
|
1359
|
+
if kw.arg is None:
|
|
1360
|
+
label = self.name or "Function"
|
|
1361
|
+
raise TranspileError(f"{label} does not support **spread")
|
|
1362
|
+
kwargs[kw.arg] = kw.value
|
|
1278
1363
|
if kwargs:
|
|
1279
1364
|
return self.fn(*args, ctx=ctx, **kwargs)
|
|
1280
1365
|
return self.fn(*args, ctx=ctx)
|
pulse/transpiler/py_module.py
CHANGED
|
@@ -22,17 +22,15 @@ def default_signature(
|
|
|
22
22
|
) -> Element: ...
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def react_component(
|
|
26
|
-
expr: Expr,
|
|
27
|
-
*,
|
|
28
|
-
lazy: bool = False,
|
|
29
|
-
):
|
|
25
|
+
def react_component(expr: Expr):
|
|
30
26
|
"""Decorator that uses the decorated function solely as a typed signature.
|
|
31
27
|
|
|
32
28
|
Returns a Jsx(expr) that preserves the function's type signature for type
|
|
33
29
|
checkers and produces Element nodes when called in transpiled code.
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
For lazy loading, use Import(lazy=True) directly:
|
|
32
|
+
LazyChart = Import("Chart", "./Chart", kind="default", lazy=True)
|
|
33
|
+
React.lazy(LazyChart) # LazyChart is already a factory
|
|
36
34
|
"""
|
|
37
35
|
|
|
38
36
|
def decorator(fn: Callable[P, Any]) -> Callable[P, Element]:
|
|
@@ -41,11 +39,6 @@ def react_component(
|
|
|
41
39
|
|
|
42
40
|
# Wrap expr: Jsx provides Element generation
|
|
43
41
|
jsx_wrapper = expr if isinstance(expr, Jsx) else Jsx(expr)
|
|
44
|
-
|
|
45
|
-
# Note: lazy flag is not currently wired into codegen
|
|
46
|
-
# Could store it via a separate side-registry if needed in future
|
|
47
|
-
_ = lazy # Suppress unused variable warning
|
|
48
|
-
|
|
49
42
|
return jsx_wrapper
|
|
50
43
|
|
|
51
44
|
return decorator
|
pulse/transpiler/transpiler.py
CHANGED
|
@@ -10,7 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
import ast
|
|
11
11
|
import re
|
|
12
12
|
from collections.abc import Callable, Mapping
|
|
13
|
-
from
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, cast
|
|
14
15
|
|
|
15
16
|
from pulse.transpiler.builtins import BUILTINS, emit_method
|
|
16
17
|
from pulse.transpiler.errors import TranspileError
|
|
@@ -31,6 +32,7 @@ from pulse.transpiler.nodes import (
|
|
|
31
32
|
If,
|
|
32
33
|
Literal,
|
|
33
34
|
Member,
|
|
35
|
+
New,
|
|
34
36
|
Return,
|
|
35
37
|
Spread,
|
|
36
38
|
Stmt,
|
|
@@ -93,6 +95,7 @@ class Transpiler:
|
|
|
93
95
|
deps: Mapping[str, Expr]
|
|
94
96
|
locals: set[str]
|
|
95
97
|
jsx: bool
|
|
98
|
+
source_file: Path | None
|
|
96
99
|
_temp_counter: int
|
|
97
100
|
|
|
98
101
|
def __init__(
|
|
@@ -101,8 +104,10 @@ class Transpiler:
|
|
|
101
104
|
deps: Mapping[str, Expr],
|
|
102
105
|
*,
|
|
103
106
|
jsx: bool = False,
|
|
107
|
+
source_file: Path | None = None,
|
|
104
108
|
) -> None:
|
|
105
109
|
self.fndef = fndef
|
|
110
|
+
self.source_file = source_file
|
|
106
111
|
# Collect all argument names (regular, vararg, kwonly, kwarg)
|
|
107
112
|
args: list[str] = [arg.arg for arg in fndef.args.args]
|
|
108
113
|
if fndef.args.vararg:
|
|
@@ -465,7 +470,7 @@ class Transpiler:
|
|
|
465
470
|
return self._emit_dict(node)
|
|
466
471
|
|
|
467
472
|
if isinstance(node, ast.Set):
|
|
468
|
-
return
|
|
473
|
+
return New(
|
|
469
474
|
Identifier("Set"),
|
|
470
475
|
[Array([self.emit_expr(e) for e in node.elts])],
|
|
471
476
|
)
|
|
@@ -515,14 +520,14 @@ class Transpiler:
|
|
|
515
520
|
arr = self._emit_comprehension_chain(
|
|
516
521
|
node.generators, lambda: self.emit_expr(node.elt)
|
|
517
522
|
)
|
|
518
|
-
return
|
|
523
|
+
return New(Identifier("Set"), [arr])
|
|
519
524
|
|
|
520
525
|
if isinstance(node, ast.DictComp):
|
|
521
526
|
pairs = self._emit_comprehension_chain(
|
|
522
527
|
node.generators,
|
|
523
528
|
lambda: Array([self.emit_expr(node.key), self.emit_expr(node.value)]),
|
|
524
529
|
)
|
|
525
|
-
return
|
|
530
|
+
return New(Identifier("Map"), [pairs])
|
|
526
531
|
|
|
527
532
|
if isinstance(node, ast.Lambda):
|
|
528
533
|
return self._emit_lambda(node)
|
|
@@ -596,7 +601,7 @@ class Transpiler:
|
|
|
596
601
|
key_expr = self.emit_expr(k)
|
|
597
602
|
val_expr = self.emit_expr(v)
|
|
598
603
|
entries.append(Array([key_expr, val_expr]))
|
|
599
|
-
return
|
|
604
|
+
return New(Identifier("Map"), [Array(entries)])
|
|
600
605
|
|
|
601
606
|
def _emit_binop(self, node: ast.BinOp) -> Expr:
|
|
602
607
|
"""Emit a binary operation."""
|
|
@@ -720,37 +725,38 @@ class Transpiler:
|
|
|
720
725
|
|
|
721
726
|
def _emit_call(self, node: ast.Call) -> Expr:
|
|
722
727
|
"""Emit a function call."""
|
|
723
|
-
# Collect args and kwargs as raw AST values
|
|
724
|
-
args_raw = list(node.args)
|
|
725
|
-
kwargs_raw: dict[str, Any] = {}
|
|
726
|
-
for kw in node.keywords:
|
|
727
|
-
if kw.arg is None:
|
|
728
|
-
raise TranspileError(
|
|
729
|
-
"Spread props (**kwargs) not yet supported in v2 transpiler"
|
|
730
|
-
)
|
|
731
|
-
kwargs_raw[kw.arg] = kw.value
|
|
732
|
-
|
|
733
728
|
# Method call: obj.method(args) - try builtin method dispatch
|
|
734
729
|
if isinstance(node.func, ast.Attribute):
|
|
730
|
+
# Check for spreads - if present, skip builtin method handling
|
|
731
|
+
# (let transpile_call decide on spread support)
|
|
732
|
+
has_spread = any(kw.arg is None for kw in node.keywords)
|
|
733
|
+
|
|
735
734
|
obj = self.emit_expr(node.func.value)
|
|
736
735
|
method = node.func.attr
|
|
737
|
-
args: list[Expr] = [self.emit_expr(a) for a in args_raw]
|
|
738
|
-
kwargs: dict[str, Expr] = {
|
|
739
|
-
k: self.emit_expr(v) for k, v in kwargs_raw.items()
|
|
740
|
-
}
|
|
741
736
|
|
|
742
|
-
# Try builtin method handling
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
737
|
+
# Try builtin method handling only if no spreads
|
|
738
|
+
if not has_spread:
|
|
739
|
+
# Safe to cast: has_spread=False means all kw.arg are str (not None)
|
|
740
|
+
kwargs_raw: dict[str, Any] = {
|
|
741
|
+
cast(str, kw.arg): kw.value for kw in node.keywords
|
|
742
|
+
}
|
|
743
|
+
args: list[Expr] = [self.emit_expr(a) for a in node.args]
|
|
744
|
+
kwargs: dict[str, Expr] = {
|
|
745
|
+
k: self.emit_expr(v) for k, v in kwargs_raw.items()
|
|
746
|
+
}
|
|
747
|
+
result = emit_method(obj, method, args, kwargs)
|
|
748
|
+
if result is not None:
|
|
749
|
+
return result
|
|
746
750
|
|
|
747
751
|
# IMPORTANT: derive method expr via transpile_getattr
|
|
748
752
|
method_expr = obj.transpile_getattr(method, self)
|
|
749
|
-
return method_expr.transpile_call(
|
|
753
|
+
return method_expr.transpile_call(
|
|
754
|
+
list(node.args), list(node.keywords), self
|
|
755
|
+
)
|
|
750
756
|
|
|
751
|
-
# Function call (
|
|
757
|
+
# Function call - pass raw keywords (let callee decide on spread support)
|
|
752
758
|
callee = self.emit_expr(node.func)
|
|
753
|
-
return callee.transpile_call(
|
|
759
|
+
return callee.transpile_call(list(node.args), list(node.keywords), self)
|
|
754
760
|
|
|
755
761
|
def _emit_attribute(self, node: ast.Attribute) -> Expr:
|
|
756
762
|
"""Emit an attribute access."""
|
pulse/user_session.py
CHANGED
|
@@ -61,6 +61,10 @@ class UserSession(Disposable):
|
|
|
61
61
|
# unwrap subscribes the effect to all signals in the session ReactiveDict
|
|
62
62
|
data = unwrap(self.data)
|
|
63
63
|
signed_cookie = app.session_store.encode(self.sid, data)
|
|
64
|
+
if app.cookie.secure is None:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Cookie.secure is not resolved. This is likely an internal error. Ensure App.setup() ran before sessions."
|
|
67
|
+
)
|
|
64
68
|
self.set_cookie(
|
|
65
69
|
name=app.cookie.name,
|
|
66
70
|
value=signed_cookie,
|
|
@@ -83,6 +87,12 @@ class UserSession(Disposable):
|
|
|
83
87
|
self._queued_cookies.clear()
|
|
84
88
|
self.scheduled_cookie_refresh = False
|
|
85
89
|
|
|
90
|
+
def get_cookie_value(self, name: str) -> str | None:
|
|
91
|
+
cookie = self._queued_cookies.get(name)
|
|
92
|
+
if cookie is None:
|
|
93
|
+
return None
|
|
94
|
+
return cookie.value
|
|
95
|
+
|
|
86
96
|
def set_cookie(
|
|
87
97
|
self,
|
|
88
98
|
name: str,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pulse-framework
|
|
3
|
+
Version: 0.1.55
|
|
4
|
+
Summary: Pulse - Full-stack framework for building real-time React applications in Python
|
|
5
|
+
Requires-Dist: websockets>=12.0
|
|
6
|
+
Requires-Dist: fastapi>=0.104.0
|
|
7
|
+
Requires-Dist: uvicorn>=0.24.0
|
|
8
|
+
Requires-Dist: mako>=1.3.10
|
|
9
|
+
Requires-Dist: typer>=0.16.0
|
|
10
|
+
Requires-Dist: python-socketio>=5.13.0
|
|
11
|
+
Requires-Dist: rich>=13.7.1
|
|
12
|
+
Requires-Dist: python-multipart>=0.0.20
|
|
13
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
14
|
+
Requires-Dist: watchfiles>=1.1.0
|
|
15
|
+
Requires-Dist: httpx>=0.28.1
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Pulse Python
|
|
20
|
+
|
|
21
|
+
Core Python framework for building full-stack reactive web apps with React frontends.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
Server-driven UI model: Python components render to VDOM, synced to React via WebSocket. State changes trigger re-renders; diffs are sent to client.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
29
|
+
│ Python Server │
|
|
30
|
+
│ ┌──────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
|
|
31
|
+
│ │ App │──│ RenderSession │──│ VDOM Renderer │ │
|
|
32
|
+
│ │ (FastAPI)│ │ (per browser) │ │ (diff & serialize) │ │
|
|
33
|
+
│ └──────────┘ └───────────────┘ └──────────────────────────┘ │
|
|
34
|
+
│ │ │ │ │
|
|
35
|
+
│ │ ┌──────┴───────┐ │ │
|
|
36
|
+
│ │ │ Hooks │ │ │
|
|
37
|
+
│ │ │ (state/setup)│ │ │
|
|
38
|
+
│ │ └──────────────┘ │ │
|
|
39
|
+
└───────┼───────────────────────────────────────┼─────────────────┘
|
|
40
|
+
│ Socket.IO │ VDOM updates
|
|
41
|
+
▼ ▼
|
|
42
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
43
|
+
│ Browser (React) │
|
|
44
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Folder Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/pulse/
|
|
51
|
+
├── app.py # Main App class, FastAPI + Socket.IO setup
|
|
52
|
+
├── channel.py # Bidirectional real-time channels
|
|
53
|
+
├── routing.py # Route/Layout definitions, URL matching
|
|
54
|
+
├── vdom.py # VDOM node types (Element, Component, Node)
|
|
55
|
+
├── renderer.py # VDOM rendering and diffing
|
|
56
|
+
├── render_session.py # Per-browser session, manages mounted routes
|
|
57
|
+
├── reactive.py # Signal/Computed/Effect primitives
|
|
58
|
+
├── reactive_extensions.py # ReactiveList, ReactiveDict, ReactiveSet
|
|
59
|
+
├── state.py # State management
|
|
60
|
+
├── serializer.py # Python<->JSON serialization
|
|
61
|
+
├── middleware.py # Request middleware (prerender, connect, message)
|
|
62
|
+
├── plugin.py # Plugin interface for extensions
|
|
63
|
+
├── form.py # Form handling
|
|
64
|
+
├── context.py # PulseContext (request/session context)
|
|
65
|
+
├── cookies.py # Cookie management
|
|
66
|
+
├── request.py # PulseRequest abstraction
|
|
67
|
+
├── user_session.py # User session storage
|
|
68
|
+
├── helpers.py # Utilities (CSSProperties, later, repeat)
|
|
69
|
+
├── decorators.py # @computed, @effect decorators
|
|
70
|
+
├── messages.py # Client<->server message types
|
|
71
|
+
├── react_component.py # ReactComponent wrapper for JS libraries
|
|
72
|
+
│
|
|
73
|
+
├── hooks/ # Server-side hooks (like React hooks)
|
|
74
|
+
│ ├── core.py # Hook registry, HooksAPI
|
|
75
|
+
│ ├── runtime.py # session(), route(), navigate(), redirect()
|
|
76
|
+
│ ├── states.py # Reactive state hook
|
|
77
|
+
│ ├── effects.py # Side effects hook
|
|
78
|
+
│ ├── setup.py # Initialization hook
|
|
79
|
+
│ ├── init.py # One-time setup hook
|
|
80
|
+
│ └── stable.py # Memoization hook
|
|
81
|
+
│
|
|
82
|
+
├── queries/ # Data fetching (like TanStack Query)
|
|
83
|
+
│ ├── query.py # @query decorator
|
|
84
|
+
│ ├── mutation.py # @mutation decorator
|
|
85
|
+
│ ├── infinite_query.py # Pagination support
|
|
86
|
+
│ ├── client.py # QueryClient for cache management
|
|
87
|
+
│ └── store.py # Query state store
|
|
88
|
+
│
|
|
89
|
+
├── components/ # Built-in components
|
|
90
|
+
│ ├── for_.py # <For> loop component
|
|
91
|
+
│ ├── if_.py # <If> conditional component
|
|
92
|
+
│ └── react_router.py # Link, Outlet for routing
|
|
93
|
+
│
|
|
94
|
+
├── html/ # HTML element bindings
|
|
95
|
+
│ ├── tags.py # div, span, button, etc.
|
|
96
|
+
│ ├── props.py # Typed props for HTML elements
|
|
97
|
+
│ ├── events.py # Event types (MouseEvent, etc.)
|
|
98
|
+
│ └── elements.py # Element type definitions
|
|
99
|
+
│
|
|
100
|
+
├── transpiler/ # Python->JS transpilation
|
|
101
|
+
│ ├── function.py # JsFunction, @javascript decorator
|
|
102
|
+
│ └── imports.py # Import/CssImport for client-side JS
|
|
103
|
+
│
|
|
104
|
+
├── codegen/ # Code generation for React Router
|
|
105
|
+
│ ├── codegen.py # Generates routes.ts, loaders
|
|
106
|
+
│ └── templates/ # Mako templates for generated code
|
|
107
|
+
│
|
|
108
|
+
├── cli/ # Command-line interface
|
|
109
|
+
│ ├── cmd.py # pulse run, pulse build
|
|
110
|
+
│ └── processes.py # Dev server process management
|
|
111
|
+
│
|
|
112
|
+
└── js/ # JS API stubs for transpilation
|
|
113
|
+
├── window.py, document.py, navigator.py
|
|
114
|
+
├── array.py, object.py, string.py
|
|
115
|
+
└── ...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Key Concepts
|
|
119
|
+
|
|
120
|
+
### App
|
|
121
|
+
|
|
122
|
+
Entry point defining routes, middleware, plugins.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import pulse as ps
|
|
126
|
+
|
|
127
|
+
app = ps.App(routes=[
|
|
128
|
+
ps.Route("/", home),
|
|
129
|
+
ps.Layout("/dashboard", layout, children=[
|
|
130
|
+
ps.Route("/", dashboard),
|
|
131
|
+
]),
|
|
132
|
+
])
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Components
|
|
136
|
+
|
|
137
|
+
Functions returning VDOM. Use `@ps.component` for stateful components.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
def greeting(name: str):
|
|
141
|
+
return ps.div(f"Hello, {name}!")
|
|
142
|
+
|
|
143
|
+
@ps.component
|
|
144
|
+
def counter():
|
|
145
|
+
count = ps.states.use(0)
|
|
146
|
+
return ps.button(f"Count: {count()}", onClick=lambda _: count.set(count() + 1))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Reactivity
|
|
150
|
+
|
|
151
|
+
- `Signal[T]` - reactive value
|
|
152
|
+
- `Computed[T]` - derived value
|
|
153
|
+
- `Effect` - side effect on change
|
|
154
|
+
|
|
155
|
+
### Hooks
|
|
156
|
+
|
|
157
|
+
Server-side hooks via `ps.states`, `ps.effects`, `ps.setup`:
|
|
158
|
+
- `states.use(initial)` - reactive state
|
|
159
|
+
- `effects.use(fn, deps)` - side effects
|
|
160
|
+
- `setup.use(fn)` - one-time initialization
|
|
161
|
+
|
|
162
|
+
### Queries
|
|
163
|
+
|
|
164
|
+
Data fetching with caching:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
@ps.query
|
|
168
|
+
async def fetch_user(id: str):
|
|
169
|
+
return await db.get_user(id)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Channels
|
|
173
|
+
|
|
174
|
+
Bidirectional real-time messaging:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
ch = ps.channel("chat")
|
|
178
|
+
|
|
179
|
+
@ch.on("message")
|
|
180
|
+
def handle_message(data):
|
|
181
|
+
ch.broadcast("new_message", data)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Main Exports
|
|
185
|
+
|
|
186
|
+
- `App`, `Route`, `Layout` - app/routing
|
|
187
|
+
- `component` - server-side component decorator
|
|
188
|
+
- `states`, `effects`, `setup`, `init` - hooks
|
|
189
|
+
- `query`, `mutation`, `infinite_query` - data fetching
|
|
190
|
+
- `channel` - real-time channels
|
|
191
|
+
- `State`, `@computed`, `@effect` - reactivity
|
|
192
|
+
- `ReactiveList`, `ReactiveDict`, `ReactiveSet` - reactive containers
|
|
193
|
+
- `div`, `span`, `button`, ... - HTML elements
|
|
194
|
+
- `For`, `If`, `Link`, `Outlet` - built-in components
|
|
195
|
+
- `@react_component` - wrap JS components
|
|
196
|
+
- `@javascript` - transpile Python to JS
|