pulse-framework 0.1.50__py3-none-any.whl → 0.1.51__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/react_component.py +5 -3
- pulse/serializer.py +11 -1
- pulse/vdom.py +9 -77
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.51.dist-info}/METADATA +1 -1
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.51.dist-info}/RECORD +7 -7
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.51.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.51.dist-info}/entry_points.txt +0 -0
pulse/react_component.py
CHANGED
|
@@ -37,7 +37,7 @@ from pulse.transpiler.nodes import (
|
|
|
37
37
|
JSXProp,
|
|
38
38
|
JSXSpreadProp,
|
|
39
39
|
)
|
|
40
|
-
from pulse.vdom import Child, Element, Node
|
|
40
|
+
from pulse.vdom import Child, Element, Node, clean_element_name
|
|
41
41
|
|
|
42
42
|
T = TypeVar("T")
|
|
43
43
|
P = ParamSpec("P")
|
|
@@ -205,7 +205,8 @@ class PropSpec:
|
|
|
205
205
|
unknown_keys = props.keys() - known_keys - {"key"}
|
|
206
206
|
if not self.allow_unspecified and unknown_keys:
|
|
207
207
|
bad = ", ".join(repr(k) for k in sorted(unknown_keys))
|
|
208
|
-
|
|
208
|
+
clean_tag = clean_element_name(comp_tag)
|
|
209
|
+
raise ValueError(f"Unexpected prop(s) for component '{clean_tag}': {bad}")
|
|
209
210
|
if self.allow_unspecified:
|
|
210
211
|
for k in unknown_keys:
|
|
211
212
|
v = props[k]
|
|
@@ -290,8 +291,9 @@ class PropSpec:
|
|
|
290
291
|
errors.append(
|
|
291
292
|
f"Multiple props map to '{js_key}': {', '.join(py_keys)}"
|
|
292
293
|
)
|
|
294
|
+
clean_tag = clean_element_name(comp_tag)
|
|
293
295
|
raise ValueError(
|
|
294
|
-
f"Invalid props for component '{
|
|
296
|
+
f"Invalid props for component '{clean_tag}': {'; '.join(errors)}"
|
|
295
297
|
)
|
|
296
298
|
|
|
297
299
|
return result or None
|
pulse/serializer.py
CHANGED
|
@@ -29,6 +29,7 @@ containing primitives, lists/tuples, ``dict``/plain objects, ``set`` and
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
31
|
import datetime as dt
|
|
32
|
+
import math
|
|
32
33
|
import types
|
|
33
34
|
from dataclasses import fields, is_dataclass
|
|
34
35
|
from typing import Any
|
|
@@ -56,7 +57,16 @@ def serialize(data: Any) -> Serialized:
|
|
|
56
57
|
|
|
57
58
|
def process(value: Any) -> PlainJSON:
|
|
58
59
|
nonlocal global_index
|
|
59
|
-
if value is None or isinstance(value, (bool, int,
|
|
60
|
+
if value is None or isinstance(value, (bool, int, str)):
|
|
61
|
+
return value
|
|
62
|
+
if isinstance(value, float):
|
|
63
|
+
if math.isnan(value):
|
|
64
|
+
return None # NaN → None (matches pandas None ↔ NaN semantics)
|
|
65
|
+
if math.isinf(value):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Cannot serialize {value}: Infinity is not valid JSON. "
|
|
68
|
+
+ "Replace with None or a sentinel value."
|
|
69
|
+
)
|
|
60
70
|
return value
|
|
61
71
|
|
|
62
72
|
idx = global_index
|
pulse/vdom.py
CHANGED
|
@@ -6,7 +6,6 @@ the TypeScript UINode format exactly, eliminating the need for translation.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import functools
|
|
9
|
-
import math
|
|
10
9
|
import re
|
|
11
10
|
import warnings
|
|
12
11
|
from collections.abc import Callable, Iterable, Sequence
|
|
@@ -30,56 +29,6 @@ from pulse.env import env
|
|
|
30
29
|
from pulse.hooks.core import HookContext
|
|
31
30
|
from pulse.hooks.init import rewrite_init_blocks
|
|
32
31
|
|
|
33
|
-
# ============================================================================
|
|
34
|
-
# Validation helpers (dev mode only)
|
|
35
|
-
# ============================================================================
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _check_json_safe_float(value: float, context: str) -> None:
|
|
39
|
-
"""Raise ValueError if a float is NaN or Infinity."""
|
|
40
|
-
if math.isnan(value):
|
|
41
|
-
raise ValueError(
|
|
42
|
-
f"Cannot use nan in {context}. "
|
|
43
|
-
+ "NaN and Infinity are not supported in Pulse because they cannot be serialized to JSON. "
|
|
44
|
-
+ "Replace with None or a sentinel value before passing to components."
|
|
45
|
-
)
|
|
46
|
-
if math.isinf(value):
|
|
47
|
-
kind = "inf" if value > 0 else "-inf"
|
|
48
|
-
raise ValueError(
|
|
49
|
-
f"Cannot use {kind} in {context}. "
|
|
50
|
-
+ "NaN and Infinity are not supported in Pulse because they cannot be serialized to JSON. "
|
|
51
|
-
+ "Replace with None or a sentinel value before passing to components."
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _validate_value(value: Any, context: str) -> None:
|
|
56
|
-
"""Recursively validate a value for JSON-unsafe floats (NaN, Infinity)."""
|
|
57
|
-
if isinstance(value, float):
|
|
58
|
-
_check_json_safe_float(value, context)
|
|
59
|
-
elif isinstance(value, dict):
|
|
60
|
-
for v in value.values():
|
|
61
|
-
_validate_value(v, context)
|
|
62
|
-
elif isinstance(value, (list, tuple)):
|
|
63
|
-
for item in value:
|
|
64
|
-
_validate_value(item, context)
|
|
65
|
-
# Skip other types - they'll be handled by the serializer
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _validate_props(props: dict[str, Any] | None, parent_name: str) -> None:
|
|
69
|
-
"""Validate all props for JSON-unsafe values."""
|
|
70
|
-
if not props:
|
|
71
|
-
return
|
|
72
|
-
for key, value in props.items():
|
|
73
|
-
_validate_value(value, f"{parent_name} prop '{key}'")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _validate_children(children: "Sequence[Element]", parent_name: str) -> None:
|
|
77
|
-
"""Validate primitive children for JSON-unsafe values."""
|
|
78
|
-
for child in children:
|
|
79
|
-
if isinstance(child, float):
|
|
80
|
-
_check_json_safe_float(child, f"{parent_name} children")
|
|
81
|
-
|
|
82
|
-
|
|
83
32
|
# ============================================================================
|
|
84
33
|
# Core VDOM
|
|
85
34
|
# ============================================================================
|
|
@@ -109,7 +58,7 @@ class Node:
|
|
|
109
58
|
|
|
110
59
|
tag: str
|
|
111
60
|
props: dict[str, Any] | None
|
|
112
|
-
children: "
|
|
61
|
+
children: "list[Element] | None"
|
|
113
62
|
allow_children: bool
|
|
114
63
|
key: str | None
|
|
115
64
|
|
|
@@ -122,7 +71,6 @@ class Node:
|
|
|
122
71
|
allow_children: bool = True,
|
|
123
72
|
):
|
|
124
73
|
self.tag = tag
|
|
125
|
-
# Normalize to None
|
|
126
74
|
self.props = props or None
|
|
127
75
|
self.children = (
|
|
128
76
|
_flatten_children(children, parent_name=f"<{self.tag}>")
|
|
@@ -134,13 +82,8 @@ class Node:
|
|
|
134
82
|
if key is not None and not isinstance(key, str):
|
|
135
83
|
raise ValueError("key must be a string or None")
|
|
136
84
|
if not self.allow_children and children:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if env.pulse_env == "dev":
|
|
140
|
-
parent_name = f"<{self.tag}>"
|
|
141
|
-
_validate_props(self.props, parent_name)
|
|
142
|
-
if self.children:
|
|
143
|
-
_validate_children(self.children, parent_name)
|
|
85
|
+
clean_tag = clean_element_name(self.tag)
|
|
86
|
+
raise ValueError(f"{clean_tag} cannot have children")
|
|
144
87
|
|
|
145
88
|
# --- Pretty printing helpers -------------------------------------------------
|
|
146
89
|
@override
|
|
@@ -332,17 +275,6 @@ class ComponentNode:
|
|
|
332
275
|
# Used for rendering
|
|
333
276
|
self.contents = None
|
|
334
277
|
self.hooks = HookContext()
|
|
335
|
-
# Dev-only validation for JSON-unsafe values
|
|
336
|
-
if env.pulse_env == "dev":
|
|
337
|
-
parent_name = f"<{self.name}>"
|
|
338
|
-
# Validate kwargs (props)
|
|
339
|
-
_validate_props(self.kwargs, parent_name)
|
|
340
|
-
# Validate args (children passed positionally)
|
|
341
|
-
for arg in self.args:
|
|
342
|
-
if isinstance(arg, float):
|
|
343
|
-
_check_json_safe_float(arg, f"{parent_name} children")
|
|
344
|
-
elif isinstance(arg, (dict, list, tuple)):
|
|
345
|
-
_validate_value(arg, f"{parent_name} children")
|
|
346
278
|
|
|
347
279
|
def __getitem__(self, children_arg: "Child | tuple[Child, ...]"):
|
|
348
280
|
if not self.takes_children:
|
|
@@ -485,7 +417,7 @@ VDOMOperation: TypeAlias = (
|
|
|
485
417
|
# ----------------------------------------------------------------------------
|
|
486
418
|
|
|
487
419
|
|
|
488
|
-
def
|
|
420
|
+
def clean_element_name(parent_name: str) -> str:
|
|
489
421
|
"""Strip $$ prefix and hexadecimal suffix from ReactComponent tags in warning messages.
|
|
490
422
|
|
|
491
423
|
ReactComponent tags are in the format <$$ComponentName_1a2b> or <$$ComponentName_1a2b.prop>.
|
|
@@ -499,7 +431,7 @@ def _clean_parent_name_for_warning(parent_name: str) -> str:
|
|
|
499
431
|
|
|
500
432
|
def _flatten_children(
|
|
501
433
|
children: Children, *, parent_name: str, warn_stacklevel: int = 5
|
|
502
|
-
) ->
|
|
434
|
+
) -> list[Element]:
|
|
503
435
|
"""Flatten children and emit warnings for unkeyed iterables (dev mode only).
|
|
504
436
|
|
|
505
437
|
Args:
|
|
@@ -510,7 +442,6 @@ def _flatten_children(
|
|
|
510
442
|
- 4 for ComponentNode.__getitem__ or Component.__call__ (user -> method -> _flatten_children -> visit -> warn)
|
|
511
443
|
"""
|
|
512
444
|
flat: list[Element] = []
|
|
513
|
-
return_tuple = isinstance(children, tuple)
|
|
514
445
|
is_dev = env.pulse_env == "dev"
|
|
515
446
|
|
|
516
447
|
def visit(item: Child) -> None:
|
|
@@ -528,7 +459,7 @@ def _flatten_children(
|
|
|
528
459
|
visit(sub)
|
|
529
460
|
if missing_key:
|
|
530
461
|
# Warn once per iterable without keys on its elements.
|
|
531
|
-
clean_name =
|
|
462
|
+
clean_name = clean_element_name(parent_name)
|
|
532
463
|
warnings.warn(
|
|
533
464
|
(
|
|
534
465
|
f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
|
|
@@ -547,13 +478,14 @@ def _flatten_children(
|
|
|
547
478
|
for child in flat:
|
|
548
479
|
if isinstance(child, (Node, ComponentNode)) and child.key is not None:
|
|
549
480
|
if child.key in seen_keys:
|
|
481
|
+
clean_name = clean_element_name(parent_name)
|
|
550
482
|
raise ValueError(
|
|
551
|
-
f"[Pulse] Duplicate key '{child.key}' found among children of {
|
|
483
|
+
f"[Pulse] Duplicate key '{child.key}' found among children of {clean_name}. "
|
|
552
484
|
+ "Keys must be unique per sibling set."
|
|
553
485
|
)
|
|
554
486
|
seen_keys.add(child.key)
|
|
555
487
|
|
|
556
|
-
return
|
|
488
|
+
return flat
|
|
557
489
|
|
|
558
490
|
|
|
559
491
|
def _short_args(args: tuple[Any, ...], max_items: int = 4) -> list[str] | str:
|
|
@@ -79,14 +79,14 @@ pulse/queries/mutation.py,sha256=px1fprFL-RxNfbRSoRtdsOLkEbjSsMrJxGHKBIPYQTM,495
|
|
|
79
79
|
pulse/queries/protocol.py,sha256=R8n238Ex9DbYIAVKB83a8FAPtnCiPNhWar-F01K2fTo,3345
|
|
80
80
|
pulse/queries/query.py,sha256=G8eXCaT5wuvVcstlqWU8VBxuuUUS7K1R5Y-VtDpMIG0,35065
|
|
81
81
|
pulse/queries/store.py,sha256=Ct7a-h1-Cq07zEfe9vw-LM85Fm7jIJx7CLAIlsiznlU,3444
|
|
82
|
-
pulse/react_component.py,sha256=
|
|
82
|
+
pulse/react_component.py,sha256=7_7gNYiWnLqwta5TBG8GhWN29IH1aH9wRmGCm4pRJzI,30713
|
|
83
83
|
pulse/reactive.py,sha256=v8a9IttkabeWwYrrHAx33zqzW9WC4WlS4iXbIh2KQkU,24374
|
|
84
84
|
pulse/reactive_extensions.py,sha256=T1V3AasHtvJkmGO55miC9RVPxDFIj7qrooMsn89x5SI,32076
|
|
85
85
|
pulse/render_session.py,sha256=kGskm6NNhQ2u4vBRXOeIeCoXsyP81O2Q4Fr93nA6q-4,18222
|
|
86
86
|
pulse/renderer.py,sha256=kUwrI5v9XMerVCjBmnYZSyfkDu8B8cW8jFjVnM93TS4,15702
|
|
87
87
|
pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
|
|
88
88
|
pulse/routing.py,sha256=XZdq4gjfYeuz1wKtjPza6YA8ya76_cQ58b2l4dBDbr4,13243
|
|
89
|
-
pulse/serializer.py,sha256=
|
|
89
|
+
pulse/serializer.py,sha256=7YF8ZjDZYgA5Wmxd9CT-lPvakSb4zjUI1YbTd9m4jQM,5733
|
|
90
90
|
pulse/state.py,sha256=ikQbK4R8PieV96qd4uWREUvs0jXo9sCapawY7i6oCYo,10776
|
|
91
91
|
pulse/transpiler/__init__.py,sha256=sgfHxLwZEPj3rBBMtzD4997qx7GTr5Wt22-296e-uC8,6492
|
|
92
92
|
pulse/transpiler/builtins.py,sha256=V_H3bpgU22Yb_GzM6YvOutZ65O36xHLzRANl7uHRqUI,22401
|
|
@@ -111,9 +111,9 @@ pulse/transpiler/utils.py,sha256=W5PAOWvmYdJCEw1eY7QEJRQFmNLVjFTdlCyWzmnTrCc,94
|
|
|
111
111
|
pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
112
|
pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
|
|
113
113
|
pulse/user_session.py,sha256=FITxLSEl3JU-jod6UWuUYC6EpnPG2rbaLCnIOdkQPtg,7803
|
|
114
|
-
pulse/vdom.py,sha256=
|
|
114
|
+
pulse/vdom.py,sha256=BQov3TqjrEoLwgMTYHHiB4hhbAxeNHxeOFZPv6eKB1o,16075
|
|
115
115
|
pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
|
|
116
|
-
pulse_framework-0.1.
|
|
117
|
-
pulse_framework-0.1.
|
|
118
|
-
pulse_framework-0.1.
|
|
119
|
-
pulse_framework-0.1.
|
|
116
|
+
pulse_framework-0.1.51.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
117
|
+
pulse_framework-0.1.51.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
|
|
118
|
+
pulse_framework-0.1.51.dist-info/METADATA,sha256=eHobxGSJSHVYV0jn6LtVABYkSnhxxcBHiFvi3N2HTSI,580
|
|
119
|
+
pulse_framework-0.1.51.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|