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 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
- raise ValueError(f"Unexpected prop(s) for component '{comp_tag}': {bad}")
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 '{comp_tag}': {'; '.join(errors)}"
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, float, str)):
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: "Sequence[Element] | None"
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
- raise ValueError(f"{self.tag} cannot have children")
138
- # Dev-only validation for JSON-unsafe values
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 _clean_parent_name_for_warning(parent_name: str) -> str:
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
- ) -> Sequence[Element]:
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 = _clean_parent_name_for_warning(parent_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 {parent_name}. "
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 tuple(flat) if return_tuple else flat
488
+ return flat
557
489
 
558
490
 
559
491
  def _short_args(args: tuple[Any, ...], max_items: int = 4) -> list[str] | str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.50
3
+ Version: 0.1.51
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: websockets>=12.0
6
6
  Requires-Dist: fastapi>=0.104.0
@@ -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=m2WJwrCvzaHDC_o4PRvZ3pD3nwy9QshxtVn10vMRDQg,30603
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=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
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=_7O8gvNSBASNnN5aqo1q9kZDImmjascQjB4Rn0IqekU,18614
114
+ pulse/vdom.py,sha256=BQov3TqjrEoLwgMTYHHiB4hhbAxeNHxeOFZPv6eKB1o,16075
115
115
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
116
- pulse_framework-0.1.50.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
117
- pulse_framework-0.1.50.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
118
- pulse_framework-0.1.50.dist-info/METADATA,sha256=Nmcyg63-aWVx4jfs-NrtxyEnksYVkmpUu3pgCz-iq0Q,580
119
- pulse_framework-0.1.50.dist-info/RECORD,,
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,,