pulse-framework 0.1.44__py3-none-any.whl → 0.1.47__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 (80) hide show
  1. pulse/__init__.py +10 -24
  2. pulse/app.py +3 -25
  3. pulse/codegen/codegen.py +43 -88
  4. pulse/codegen/js.py +35 -5
  5. pulse/codegen/templates/route.py +341 -254
  6. pulse/form.py +1 -1
  7. pulse/helpers.py +40 -8
  8. pulse/hooks/core.py +2 -2
  9. pulse/hooks/effects.py +1 -1
  10. pulse/hooks/init.py +2 -1
  11. pulse/hooks/setup.py +1 -1
  12. pulse/hooks/stable.py +2 -2
  13. pulse/hooks/states.py +2 -2
  14. pulse/html/props.py +3 -2
  15. pulse/html/tags.py +135 -0
  16. pulse/html/tags.pyi +4 -0
  17. pulse/js/__init__.py +110 -0
  18. pulse/js/__init__.pyi +95 -0
  19. pulse/js/_types.py +297 -0
  20. pulse/js/array.py +253 -0
  21. pulse/js/console.py +47 -0
  22. pulse/js/date.py +113 -0
  23. pulse/js/document.py +138 -0
  24. pulse/js/error.py +139 -0
  25. pulse/js/json.py +62 -0
  26. pulse/js/map.py +84 -0
  27. pulse/js/math.py +66 -0
  28. pulse/js/navigator.py +76 -0
  29. pulse/js/number.py +54 -0
  30. pulse/js/object.py +173 -0
  31. pulse/js/promise.py +150 -0
  32. pulse/js/regexp.py +54 -0
  33. pulse/js/set.py +109 -0
  34. pulse/js/string.py +35 -0
  35. pulse/js/weakmap.py +50 -0
  36. pulse/js/weakset.py +45 -0
  37. pulse/js/window.py +199 -0
  38. pulse/messages.py +22 -3
  39. pulse/queries/client.py +7 -7
  40. pulse/queries/effect.py +16 -0
  41. pulse/queries/infinite_query.py +138 -29
  42. pulse/queries/mutation.py +1 -15
  43. pulse/queries/protocol.py +136 -0
  44. pulse/queries/query.py +610 -174
  45. pulse/queries/store.py +11 -14
  46. pulse/react_component.py +167 -14
  47. pulse/reactive.py +19 -1
  48. pulse/reactive_extensions.py +5 -5
  49. pulse/render_session.py +185 -59
  50. pulse/renderer.py +80 -158
  51. pulse/routing.py +1 -18
  52. pulse/transpiler/__init__.py +131 -0
  53. pulse/transpiler/builtins.py +731 -0
  54. pulse/transpiler/constants.py +110 -0
  55. pulse/transpiler/context.py +26 -0
  56. pulse/transpiler/errors.py +2 -0
  57. pulse/transpiler/function.py +250 -0
  58. pulse/transpiler/ids.py +16 -0
  59. pulse/transpiler/imports.py +409 -0
  60. pulse/transpiler/js_module.py +274 -0
  61. pulse/transpiler/modules/__init__.py +30 -0
  62. pulse/transpiler/modules/asyncio.py +38 -0
  63. pulse/transpiler/modules/json.py +20 -0
  64. pulse/transpiler/modules/math.py +320 -0
  65. pulse/transpiler/modules/re.py +466 -0
  66. pulse/transpiler/modules/tags.py +268 -0
  67. pulse/transpiler/modules/typing.py +59 -0
  68. pulse/transpiler/nodes.py +1216 -0
  69. pulse/transpiler/py_module.py +119 -0
  70. pulse/transpiler/transpiler.py +938 -0
  71. pulse/transpiler/utils.py +4 -0
  72. pulse/types/event_handler.py +3 -2
  73. pulse/vdom.py +212 -13
  74. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
  75. pulse_framework-0.1.47.dist-info/RECORD +119 -0
  76. pulse/codegen/imports.py +0 -204
  77. pulse/css.py +0 -155
  78. pulse_framework-0.1.44.dist-info/RECORD +0 -79
  79. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
  80. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,4 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ AnyCallable = Callable[..., Any]
@@ -1,9 +1,10 @@
1
- from collections.abc import Awaitable, Callable
1
+ from collections.abc import Callable
2
2
  from typing import (
3
+ Any,
3
4
  TypeVar,
4
5
  )
5
6
 
6
- EventHandlerResult = None | Awaitable[None]
7
+ EventHandlerResult = Any
7
8
 
8
9
  T1 = TypeVar("T1", contravariant=True)
9
10
  T2 = TypeVar("T2", contravariant=True)
pulse/vdom.py CHANGED
@@ -6,6 +6,8 @@ the TypeScript UINode format exactly, eliminating the need for translation.
6
6
  """
7
7
 
8
8
  import functools
9
+ import math
10
+ import re
9
11
  import warnings
10
12
  from collections.abc import Callable, Iterable, Sequence
11
13
  from inspect import Parameter, signature
@@ -13,18 +15,71 @@ from types import NoneType
13
15
  from typing import (
14
16
  Any,
15
17
  Generic,
18
+ Literal,
16
19
  NamedTuple,
17
20
  NotRequired,
18
21
  ParamSpec,
19
22
  TypeAlias,
20
23
  TypedDict,
24
+ final,
21
25
  overload,
22
26
  override,
23
27
  )
24
28
 
29
+ from pulse.env import env
25
30
  from pulse.hooks.core import HookContext
26
31
  from pulse.hooks.init import rewrite_init_blocks
27
32
 
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
+
28
83
  # ============================================================================
29
84
  # Core VDOM
30
85
  # ============================================================================
@@ -42,11 +97,16 @@ class Callback(NamedTuple):
42
97
  n_args: int
43
98
 
44
99
 
45
- def NOOP(*_args: Any):
46
- return None
47
-
48
-
100
+ @final
49
101
  class Node:
102
+ __slots__ = (
103
+ "tag",
104
+ "props",
105
+ "children",
106
+ "allow_children",
107
+ "key",
108
+ )
109
+
50
110
  tag: str
51
111
  props: dict[str, Any] | None
52
112
  children: "Sequence[Element] | None"
@@ -75,6 +135,12 @@ class Node:
75
135
  raise ValueError("key must be a string or None")
76
136
  if not self.allow_children and children:
77
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)
78
144
 
79
145
  # --- Pretty printing helpers -------------------------------------------------
80
146
  @override
@@ -199,6 +265,15 @@ class Component(Generic[P]):
199
265
  if key is not None and not isinstance(key, str):
200
266
  raise ValueError("key must be a string or None")
201
267
 
268
+ # Flatten children if component takes children (has *children parameter)
269
+ if self._takes_children and args:
270
+ flattened = _flatten_children(
271
+ args, # pyright: ignore[reportArgumentType]
272
+ parent_name=f"<{self.name}>",
273
+ warn_stacklevel=4,
274
+ )
275
+ args = tuple(flattened) # pyright: ignore[reportAssignmentType]
276
+
202
277
  return ComponentNode(
203
278
  fn=self.fn,
204
279
  key=key,
@@ -217,7 +292,19 @@ class Component(Generic[P]):
217
292
  return self.name
218
293
 
219
294
 
295
+ @final
220
296
  class ComponentNode:
297
+ __slots__ = (
298
+ "fn",
299
+ "args",
300
+ "kwargs",
301
+ "key",
302
+ "name",
303
+ "takes_children",
304
+ "hooks",
305
+ "contents",
306
+ )
307
+
221
308
  fn: Callable[..., Any]
222
309
  args: tuple[Any, ...]
223
310
  kwargs: dict[str, Any]
@@ -225,6 +312,7 @@ class ComponentNode:
225
312
  name: str
226
313
  takes_children: bool
227
314
  hooks: HookContext
315
+ contents: "Element | None"
228
316
 
229
317
  def __init__(
230
318
  self,
@@ -242,8 +330,19 @@ class ComponentNode:
242
330
  self.name = name or _infer_component_name(fn)
243
331
  self.takes_children = takes_children
244
332
  # Used for rendering
245
- self.contents: Element | None = None
333
+ self.contents = None
246
334
  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")
247
346
 
248
347
  def __getitem__(self, children_arg: "Child | tuple[Child, ...]"):
249
348
  if not self.takes_children:
@@ -259,7 +358,7 @@ class ComponentNode:
259
358
  children_arg = (children_arg,)
260
359
  # Flatten children for ComponentNode as well
261
360
  flattened_children = _flatten_children(
262
- children_arg, parent_name=f"<{self.name}>"
361
+ children_arg, parent_name=f"<{self.name}>", warn_stacklevel=4
263
362
  )
264
363
  result = ComponentNode(
265
364
  fn=self.fn,
@@ -310,33 +409,133 @@ Callbacks = dict[str, Callback]
310
409
  VDOM: TypeAlias = VDOMNode | Primitive
311
410
  Props = dict[str, Any]
312
411
 
412
+
413
+ # ============================================================================
414
+ # VDOM Operations (updates sent from server to client)
415
+ # ============================================================================
416
+
417
+
418
+ class ReplaceOperation(TypedDict):
419
+ type: Literal["replace"]
420
+ path: str
421
+ data: VDOM
422
+
423
+
424
+ # This payload makes it easy for the client to rebuild an array of React nodes
425
+ # from the previous children array:
426
+ # - Allocate array of size N
427
+ # - For i in 0..N-1, check the following scenarios
428
+ # - i matches the next index in `new` -> use provided tree
429
+ # - i matches the next index in `reuse` -> reuse previous child
430
+ # - otherwise, reuse the element at the same index
431
+ class ReconciliationOperation(TypedDict):
432
+ type: Literal["reconciliation"]
433
+ path: str
434
+ N: int
435
+ new: tuple[list[int], list[VDOM]]
436
+ reuse: tuple[list[int], list[int]]
437
+
438
+
439
+ class UpdatePropsDelta(TypedDict, total=False):
440
+ # Only send changed/new keys under `set` and removed keys under `remove`
441
+ set: Props
442
+ remove: list[str]
443
+
444
+
445
+ class UpdatePropsOperation(TypedDict):
446
+ type: Literal["update_props"]
447
+ path: str
448
+ data: UpdatePropsDelta
449
+
450
+
451
+ class PathDelta(TypedDict, total=False):
452
+ add: list[str]
453
+ remove: list[str]
454
+
455
+
456
+ class UpdateCallbacksOperation(TypedDict):
457
+ type: Literal["update_callbacks"]
458
+ path: str
459
+ data: PathDelta
460
+
461
+
462
+ class UpdateRenderPropsOperation(TypedDict):
463
+ type: Literal["update_render_props"]
464
+ path: str
465
+ data: PathDelta
466
+
467
+
468
+ class UpdateJsExprPathsOperation(TypedDict):
469
+ type: Literal["update_jsexpr_paths"]
470
+ path: str
471
+ data: PathDelta
472
+
473
+
474
+ VDOMOperation: TypeAlias = (
475
+ ReplaceOperation
476
+ | UpdatePropsOperation
477
+ | ReconciliationOperation
478
+ | UpdateCallbacksOperation
479
+ | UpdateRenderPropsOperation
480
+ | UpdateJsExprPathsOperation
481
+ )
482
+
483
+
313
484
  # ----------------------------------------------------------------------------
314
485
  # Component naming heuristics
315
486
  # ----------------------------------------------------------------------------
316
487
 
317
488
 
318
- def _flatten_children(children: Children, *, parent_name: str) -> Sequence[Element]:
319
- """Flatten children and emit warnings for unkeyed iterables."""
489
+ def _clean_parent_name_for_warning(parent_name: str) -> str:
490
+ """Strip $$ prefix and hexadecimal suffix from ReactComponent tags in warning messages.
491
+
492
+ ReactComponent tags are in the format <$$ComponentName_1a2b> or <$$ComponentName_1a2b.prop>.
493
+ This function strips the $$ prefix and _1a2b suffix to show just the component name.
494
+ """
495
+
496
+ # Match ReactComponent tags: <$$ComponentName_hex> or <$$ComponentName_hex.prop>
497
+ # Strip the $$ prefix and _hex suffix but keep the rest (hex digits are 0-9, a-f)
498
+ return re.sub(r"\$\$([^_]+)_[0-9a-f]+", r"\1", parent_name)
499
+
500
+
501
+ def _flatten_children(
502
+ children: Children, *, parent_name: str, warn_stacklevel: int = 5
503
+ ) -> Sequence[Element]:
504
+ """Flatten children and emit warnings for unkeyed iterables (dev mode only).
505
+
506
+ Args:
507
+ children: The children sequence to flatten.
508
+ parent_name: Name of the parent element for error messages.
509
+ warn_stacklevel: Stack level for warnings. Adjust based on call site:
510
+ - 5 for Node.__init__ via tag factory (user -> tag factory -> Node.__init__ -> _flatten_children -> visit -> warn)
511
+ - 4 for ComponentNode.__getitem__ or Component.__call__ (user -> method -> _flatten_children -> visit -> warn)
512
+ """
320
513
  flat: list[Element] = []
321
514
  return_tuple = isinstance(children, tuple)
515
+ is_dev = env.pulse_env == "dev"
322
516
 
323
517
  def visit(item: Child) -> None:
324
518
  if isinstance(item, Iterable) and not isinstance(item, str):
325
519
  # If any Node/ComponentNode yielded by this iterable lacks a key,
326
- # emit a single warning for this iterable.
520
+ # emit a single warning for this iterable (dev mode only).
327
521
  missing_key = False
328
522
  for sub in item:
329
- if isinstance(sub, (Node, ComponentNode)) and sub.key is None:
523
+ if (
524
+ is_dev
525
+ and isinstance(sub, (Node, ComponentNode))
526
+ and sub.key is None
527
+ ):
330
528
  missing_key = True
331
529
  visit(sub)
332
530
  if missing_key:
333
- # Warn once per iterable without keys on its elements
531
+ # Warn once per iterable without keys on its elements.
532
+ clean_name = _clean_parent_name_for_warning(parent_name)
334
533
  warnings.warn(
335
534
  (
336
- f"[Pulse] Iterable children of {parent_name} contain elements without 'key'. "
535
+ f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
337
536
  "Add a stable 'key' to each element inside iterables to improve reconciliation."
338
537
  ),
339
- stacklevel=3,
538
+ stacklevel=warn_stacklevel,
340
539
  )
341
540
  else:
342
541
  # Not an iterable child: must be a Element or primitive
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.44
3
+ Version: 0.1.47
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
@@ -0,0 +1,119 @@
1
+ pulse/__init__.py,sha256=F97Jf6i99NXZe05ICjGbBjIaT4JURdh7dC1T1-Gnb0o,32790
2
+ pulse/app.py,sha256=57o_FrJjKxMDkbxKTzeCUtBRXHfGrCbSpJUnP0Z8evg,31474
3
+ pulse/channel.py,sha256=d9eLxgyB0P9UBVkPkXV7MHkC4LWED1Cq3GKsEu_SYy4,13056
4
+ pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ pulse/cli/cmd.py,sha256=UBT7OoqWRU-idLOKkA9TDN8m8ugi1gwRMiUJTUmkVfU,14853
6
+ pulse/cli/dependencies.py,sha256=ZBqBAfMvMBQUvh4THdPDztTMQ_dyR52S1IuotP_eEZs,5623
7
+ pulse/cli/folder_lock.py,sha256=kvUmZBg869lwCTIZFoge9dhorv8qPXHTWwVv_jQg1k8,3477
8
+ pulse/cli/helpers.py,sha256=8bRlV3d7w3w-jHaFvFYt9Pzue6_CbKOq_Z3jBsBOeUk,8820
9
+ pulse/cli/models.py,sha256=NBV5byBDNoAQSk0vKwibLjoxuA85XBYIyOVJn64L8oU,858
10
+ pulse/cli/packages.py,sha256=e7ycwwJfdmB4pzrai4DHos6-JzyUgmE4DCZp0BqjdeI,6792
11
+ pulse/cli/processes.py,sha256=C1xU72oUanj-1Mkc9WmqESTsVUn_aUHG8URiPyRHSFM,7016
12
+ pulse/cli/secrets.py,sha256=dNfQe6AzSYhZuWveesjCRHIbvaPd3-F9lEJ-kZA7ROw,921
13
+ pulse/cli/uvicorn_log_config.py,sha256=f7ikDc5foXh3TmFMrnfnW8yev48ZAdlo8F4F_aMVoVk,2391
14
+ pulse/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ pulse/codegen/codegen.py,sha256=hK2gh2hX8ARMdQiQ3ILj6NMOuNiCbcuTdgNrtt4ex-8,9525
16
+ pulse/codegen/js.py,sha256=yw2RKQhiSBZr_FL-3WpZoAhcYCvuPEZXTzP5p9i7mCY,1540
17
+ pulse/codegen/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ pulse/codegen/templates/layout.py,sha256=nmWPQcO9SRXc3mCCVLCmykreSF96TqQfdDY7dvUBxRg,4737
19
+ pulse/codegen/templates/route.py,sha256=ionwEoTq0aAIE3lmnFyHLAlFnpKOT0QZm_XhTL_ZDNQ,10447
20
+ pulse/codegen/templates/routes_ts.py,sha256=nPgKCvU0gzue2k6KlOL1TJgrBqqRLmyy7K_qKAI8zAE,1129
21
+ pulse/codegen/utils.py,sha256=QoXcV-h-DLLmq_t03hDNUePS0fNnofUQLoR-TXzDFCY,539
22
+ pulse/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ pulse/components/for_.py,sha256=LUyJEUlDM6b9oPjvUFgSsddxu6b6usF4BQdXe8FIiGI,1302
24
+ pulse/components/if_.py,sha256=rQywsmdirNpkb-61ZEdF-tgzUh-37JWd4YFGblkzIdQ,1624
25
+ pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuLubk,1129
26
+ pulse/context.py,sha256=fMK6GdQY4q_3452v5DJli2f2_urVihnpzb-O-O9cJ1Q,1734
27
+ pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
28
+ pulse/decorators.py,sha256=ywNgLN6VFcKOM5fbFdUUzh-DWk4BuSXdD1BTfd1N-0U,4827
29
+ pulse/env.py,sha256=p3XI8KG1ZCcXPD3LJP7fW8JPYfyvoYY5ENwae2o0PiA,2889
30
+ pulse/form.py,sha256=UHVyp9fIZaM-Bi9_he8FBT8A2tI7bnRCn1dJkOat0zw,9022
31
+ pulse/helpers.py,sha256=sAIA_X2p6-AEg1Y87NejCZvV82_FtNpePX1Z0sFsdh8,15005
32
+ pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ pulse/hooks/core.py,sha256=QfYRz2O8-drNSQx_xnv8mK8ksWcw3LNM1H2hoInT0Rk,7457
34
+ pulse/hooks/effects.py,sha256=pVq5OndlhFLHLpM9Pn9Bp5rEpnpmJEpbIp2UaHHyJFQ,2428
35
+ pulse/hooks/init.py,sha256=iTNmEcFgZCXsLImPONbSNwc5asT7NQRz04b1Jopgzxs,11960
36
+ pulse/hooks/runtime.py,sha256=k5LZ8hnlNBMKOiEkQcAvs8BKwYxV6gwea2WCfju5K7Y,5106
37
+ pulse/hooks/setup.py,sha256=c_uVi0S0HPioEvjdWUaSdAGT9M3Cxpw8J-llvtmDOGo,4496
38
+ pulse/hooks/stable.py,sha256=mLNS6WyA4tC-65gNybPOE0DLEz1YlxOCddD9odElArU,1772
39
+ pulse/hooks/states.py,sha256=fFqN3gf7v7rY6QmieKWN1hVCQRRnL-5H4TeG9LTnKSc,6778
40
+ pulse/html/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ pulse/html/elements.py,sha256=YHXkVpfMAC4-0o61fK-E0LGTOM3KMCtBfpHHAwLx7dw,23241
42
+ pulse/html/events.py,sha256=SiZxaQV340hc5YGoKWXC5uCmbLsuijuEgnQz1hmdqYg,14700
43
+ pulse/html/props.py,sha256=FMXGHcO6GhTYp2Uw_6LpjDH5b-fVR88to3YlLvysjAM,26731
44
+ pulse/html/svg.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ pulse/html/tags.py,sha256=NEH1otY0mURAl8STVunWzBC3fnmiwGqxcPucBPmtVzU,7568
46
+ pulse/html/tags.pyi,sha256=y0zHmPDmZbkPUbO8YJ8Yaw47NicoorOU-xjCTWWB_NM,14094
47
+ pulse/js/__init__.py,sha256=nI_FQpUlLUyCTmpj5cIbpNXjN7Oqwmp5je13ixB9pWw,3104
48
+ pulse/js/__init__.pyi,sha256=Xrub3LEm4JjyNTIyehm-Bfyy2gmpJgvFsEoRfpmq7bg,2624
49
+ pulse/js/_types.py,sha256=zkXtH1B7qURL24VFBPbyH3VIO1H2ptPaU2iE9ZHLDkQ,7402
50
+ pulse/js/array.py,sha256=WC7Z_A_dsf0_uyRTjuWqkTftBWXnNDdHzZJ4pvCqjsE,7102
51
+ pulse/js/console.py,sha256=rAbKq9uMYmsAsQ4j2crYpIoFkrfGHPWdPyU62zHQyWc,1818
52
+ pulse/js/date.py,sha256=lqPpZMhOR5bLxqEe9vyCWBKv6PPKfN_Ikwc-WAmu_NQ,3392
53
+ pulse/js/document.py,sha256=nS8C6e5rMMH8dKvwfO6WkfQElqrBxK-EKhl5JUUCzSA,3015
54
+ pulse/js/error.py,sha256=8zQPumWEssBK_gIcFOQu_TZGPNwcH5N6Yd6C0COElxM,2763
55
+ pulse/js/json.py,sha256=mPvJ0meHNuHrjXOJ7PfC_g1kOldAk_w7xYKA2dhwSUs,1945
56
+ pulse/js/map.py,sha256=Q8EAbkrlg1P30nKrx0MoLsmMS25Y860-lnoPzyZ0yLQ,2056
57
+ pulse/js/math.py,sha256=EwVnFN4MfHlY-QFz4_35BIsLEjfOyisTqoOfYHnp-mM,1801
58
+ pulse/js/navigator.py,sha256=viUQGCO18R_UvWhrdWj1x1IzDd6ZZYcb_m0bI5n8fms,1539
59
+ pulse/js/number.py,sha256=d64aZ8VEJSDN8vMzLvL0LnotRhG0bLrMg1IoLf23na8,1321
60
+ pulse/js/object.py,sha256=l5OzfKS_kiUnrzRk4HAhjOOVUAJ8q-VWZv-4pADDtjc,4808
61
+ pulse/js/promise.py,sha256=OnENcz7k0V6oGRtIxp-4LbGdurqd4aFMrFfWibm26Y0,4438
62
+ pulse/js/regexp.py,sha256=vHI8xI2QOT-dDHI6Held4YdwvfGs4i0_3Ce6gMyB7L0,1123
63
+ pulse/js/set.py,sha256=trMVxWfAhV07GKC8wn5Gqm31rk1SQtrJrNzSdDyfqi0,2830
64
+ pulse/js/string.py,sha256=fBd_CKq5nhc300mRa3YgNw0jpTEgGyaXRmGBiJgeu5w,928
65
+ pulse/js/weakmap.py,sha256=Q7kgPQx6rFqYfhIDyRfhuC12JmlKmO2n-OGSpl3g9ZY,1473
66
+ pulse/js/weakset.py,sha256=FJoVR0WtaOaHL7AXzJOb29F_sqG1K2mWxvR0RJk3mS0,1333
67
+ pulse/js/window.py,sha256=ayx3lBl54hTVanlkiC2wCVGNh0IDJqzPO7OlO11YUtI,4081
68
+ pulse/messages.py,sha256=PDsb07QDKvkMitAMgLmOk2c4JDb58Cq9WWCwbQ8unvg,3979
69
+ pulse/middleware.py,sha256=9uyAhVUEGMSwqWC3WXqs7x5JMMNEcSTTu3g7DjsR8w8,9812
70
+ pulse/plugin.py,sha256=RfGl6Vtr7VRHb8bp4Ob4dOX9dVzvc4Riu7HWnStMPpk,580
71
+ pulse/proxy.py,sha256=zh4v5lmYNg5IBE_xdHHmGPwbMQNSXb2npeLXvw_O1Oc,6591
72
+ pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
+ pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ pulse/queries/client.py,sha256=GGckE0P3YCBO4Mj-08AO_I9eXVC4sIDSNw_xTLrBFuE,15224
75
+ pulse/queries/common.py,sha256=Cr_NV0dWz5DQ7Qg771jvUms1o2-EnTYqjZJe4tVeoVk,1160
76
+ pulse/queries/effect.py,sha256=7KvV_yK7OHTWhfQbZFGzg_pRhyI2mn25pKIF9AmSmcU,1471
77
+ pulse/queries/infinite_query.py,sha256=oUHWjP2OliB7h8VDJooGocefHm4m9TDy4WaJesSrsdI,40457
78
+ pulse/queries/mutation.py,sha256=px1fprFL-RxNfbRSoRtdsOLkEbjSsMrJxGHKBIPYQTM,4959
79
+ pulse/queries/protocol.py,sha256=R8n238Ex9DbYIAVKB83a8FAPtnCiPNhWar-F01K2fTo,3345
80
+ pulse/queries/query.py,sha256=G8eXCaT5wuvVcstlqWU8VBxuuUUS7K1R5Y-VtDpMIG0,35065
81
+ pulse/queries/store.py,sha256=Ct7a-h1-Cq07zEfe9vw-LM85Fm7jIJx7CLAIlsiznlU,3444
82
+ pulse/react_component.py,sha256=m2WJwrCvzaHDC_o4PRvZ3pD3nwy9QshxtVn10vMRDQg,30603
83
+ pulse/reactive.py,sha256=v8a9IttkabeWwYrrHAx33zqzW9WC4WlS4iXbIh2KQkU,24374
84
+ pulse/reactive_extensions.py,sha256=T1V3AasHtvJkmGO55miC9RVPxDFIj7qrooMsn89x5SI,32076
85
+ pulse/render_session.py,sha256=UAI65k28ysZqCp7j6BBdi7MkG2wVKJXj4hIgjK7hsaM,18120
86
+ pulse/renderer.py,sha256=kUwrI5v9XMerVCjBmnYZSyfkDu8B8cW8jFjVnM93TS4,15702
87
+ pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
88
+ pulse/routing.py,sha256=XZdq4gjfYeuz1wKtjPza6YA8ya76_cQ58b2l4dBDbr4,13243
89
+ pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
90
+ pulse/state.py,sha256=ikQbK4R8PieV96qd4uWREUvs0jXo9sCapawY7i6oCYo,10776
91
+ pulse/transpiler/__init__.py,sha256=sgfHxLwZEPj3rBBMtzD4997qx7GTr5Wt22-296e-uC8,6492
92
+ pulse/transpiler/builtins.py,sha256=V_H3bpgU22Yb_GzM6YvOutZ65O36xHLzRANl7uHRqUI,22401
93
+ pulse/transpiler/constants.py,sha256=GBYfTGgzDCuy-U5wC6iRYezSPK5UpZuyXrEp_yO6yTM,3188
94
+ pulse/transpiler/context.py,sha256=e-Nh0AKsq9_wVOI8gL_gn-UAP6HzcYN14zWLfNNzjWw,714
95
+ pulse/transpiler/errors.py,sha256=JC6tTEmnHf6JdyW4GIvfXB0IBLe7p3FvCLh14PocH28,43
96
+ pulse/transpiler/function.py,sha256=rP-MZl15_mwaGwPToPcFVYHXqyVdMn91cxtF7MKFPHA,7526
97
+ pulse/transpiler/ids.py,sha256=d91B_LFaAALKXHjGPmL8tJmDqGDFz7-GquYmnV9IZ0o,327
98
+ pulse/transpiler/imports.py,sha256=C4lSi5cRQcoo559_urDqbHAdeTiSeU3eEh89o2YDWhI,10848
99
+ pulse/transpiler/js_module.py,sha256=AjguCbKV_FnoanzojrW5-vlW-DQCTtyd-4FfBYQLxxQ,9603
100
+ pulse/transpiler/modules/__init__.py,sha256=YPM2WhILHXQFDSxDcbs-hHdhFh6i3N5ZJLEVGDZeR3Y,1065
101
+ pulse/transpiler/modules/asyncio.py,sha256=FetybIKGJhVJ3uEQJBw6Z2fsh6g7vA-9tqec4nn5FtI,1409
102
+ pulse/transpiler/modules/json.py,sha256=rlOPiJTpUiB2BGEpLqlP9gqO6jegpBd6vigE_h0iIfw,554
103
+ pulse/transpiler/modules/math.py,sha256=9_h6GGJA1SMIBKIB1AwoHErT-CqgSpyEWWneIcxRofE,9415
104
+ pulse/transpiler/modules/re.py,sha256=lAVy-0ej6PEbxNJ4Z0f9N6f9Fol3IsxPKoqBxh4Il-A,13115
105
+ pulse/transpiler/modules/tags.py,sha256=O3K9bLppCzYNlkXjeTvoQN7OI_HfW3xGy55CBT2Qn84,9079
106
+ pulse/transpiler/modules/typing.py,sha256=Vtiv33JzCe7_2K6IBpGdugI4f0sHJUq5I9qI_-sW80M,1639
107
+ pulse/transpiler/nodes.py,sha256=QAzIiu6R9nmBj-sYhiZf4-ffzdX8n_8Z7GswB7qXe5Y,33079
108
+ pulse/transpiler/py_module.py,sha256=bdSL_tUjOeYorWkOVPW4bx5tuFxYbWcGvuYQ5r8WnWk,3859
109
+ pulse/transpiler/transpiler.py,sha256=xwcJc5px1EShgXpCsqJyIynez2GdKynWy6zn2WaRZJA,29292
110
+ pulse/transpiler/utils.py,sha256=W5PAOWvmYdJCEw1eY7QEJRQFmNLVjFTdlCyWzmnTrCc,94
111
+ pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
+ pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
113
+ pulse/user_session.py,sha256=FITxLSEl3JU-jod6UWuUYC6EpnPG2rbaLCnIOdkQPtg,7803
114
+ pulse/vdom.py,sha256=yHMOYiXcaP3IVnYOAZrd_7cXWULKQ2fRQAiPDPSsOyU,18494
115
+ pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
116
+ pulse_framework-0.1.47.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
117
+ pulse_framework-0.1.47.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
118
+ pulse_framework-0.1.47.dist-info/METADATA,sha256=AMFl4QOZ07Oxunrd6GrbkKyUiFW6C04HOQuixmt_hcE,580
119
+ pulse_framework-0.1.47.dist-info/RECORD,,
pulse/codegen/imports.py DELETED
@@ -1,204 +0,0 @@
1
- from collections.abc import Iterable
2
- from dataclasses import dataclass, field
3
- from operator import concat
4
-
5
- from pulse.codegen.utils import NameRegistry
6
-
7
-
8
- class Imported:
9
- name: str
10
- src: str
11
- is_default: bool
12
- prop: str | None
13
- alias: str | None
14
-
15
- def __init__(
16
- self,
17
- name: str,
18
- src: str,
19
- is_default: bool = False,
20
- prop: str | None = None,
21
- alias: str | None = None,
22
- ) -> None:
23
- self.name = name
24
- self.src = src
25
- self.is_default = is_default
26
- self.prop = prop
27
- self.alias = alias
28
-
29
- @property
30
- def expr(self):
31
- if self.prop:
32
- return f"{self.alias or self.name}.{self.prop}"
33
- return self.alias or self.name
34
-
35
-
36
- @dataclass
37
- class ImportMember:
38
- name: str
39
- alias: str | None = None
40
-
41
- @property
42
- def identifier(self):
43
- return self.alias or self.name
44
-
45
-
46
- @dataclass
47
- class ImportStatement:
48
- src: str
49
- values: list[ImportMember] = field(default_factory=list)
50
- types: list[ImportMember] = field(default_factory=list)
51
- default_import: str | None = None
52
- # When True, emit a side-effect import: `import "<src>";`
53
- # Can be combined with named/default imports; side-effect line is emitted
54
- # only when there are no named/default/type imports for the source.
55
- side_effect: bool = False
56
- # Optional ordering constraint: ensure this statement is emitted before
57
- # any import statements whose `src` matches one of these values.
58
- # Example: ImportStatement(src="@mantine/core/styles.css", side_effect=True,
59
- # before=["@mantine/dates/styles.css"]) ensures
60
- # core styles are imported before dates styles.
61
- before: list[str] = field(default_factory=list)
62
-
63
-
64
- class Imports:
65
- names: NameRegistry
66
-
67
- def __init__(
68
- self,
69
- imports: Iterable[ImportStatement | Imported],
70
- names: NameRegistry | None = None,
71
- ) -> None:
72
- self.names = names or NameRegistry()
73
- # Map (src, name) -> identifier (either name or alias)
74
- self._import_map: dict[tuple[str, str], str] = {}
75
- self.sources: dict[str, ImportStatement] = {}
76
- for stmt in imports:
77
- if not isinstance(stmt, ImportStatement):
78
- continue
79
-
80
- if stmt.default_import:
81
- stmt.default_import = self.names.register(stmt.default_import)
82
-
83
- for imp in concat(stmt.values, stmt.types):
84
- name = self.names.register(imp.name)
85
- if name != imp.name:
86
- imp.alias = name
87
- self._import_map[(stmt.src, imp.name)] = name
88
-
89
- self.sources[stmt.src] = stmt
90
-
91
- def import_(
92
- self, src: str, name: str, is_type: bool = False, is_default: bool = False
93
- ) -> str:
94
- stmt = self.sources.get(src)
95
- if not stmt:
96
- stmt = ImportStatement(src)
97
- self.sources[src] = stmt
98
-
99
- if is_default:
100
- if stmt.default_import:
101
- return stmt.default_import
102
- stmt.default_import = self.names.register(name)
103
- return stmt.default_import
104
-
105
- else:
106
- if (src, name) in self._import_map:
107
- return self._import_map[(src, name)]
108
-
109
- unique_name = self.names.register(name)
110
- alias = unique_name if unique_name != name else None
111
- imp = ImportMember(name, alias)
112
- if is_type:
113
- stmt.types.append(imp)
114
- else:
115
- stmt.values.append(imp)
116
- # Remember mapping so future imports of the same (src, name) reuse identifier
117
- self._import_map[(src, name)] = imp.identifier
118
- return imp.identifier
119
-
120
- def add_statement(self, stmt: ImportStatement) -> None:
121
- """Merge an ImportStatement into the current Imports registry.
122
-
123
- Ensures consistent aliasing via NameRegistry and de-duplicates
124
- previously imported names from the same source.
125
- """
126
- existing = self.sources.get(stmt.src)
127
- if not existing:
128
- # Normalize names through registry to avoid later conflicts
129
- if stmt.default_import:
130
- stmt.default_import = self.names.register(stmt.default_import)
131
- for imp in concat(stmt.values, stmt.types):
132
- name = self.names.register(imp.name)
133
- if name != imp.name:
134
- imp.alias = name
135
- self._import_map[(stmt.src, imp.name)] = name
136
- self.sources[stmt.src] = stmt
137
- return
138
-
139
- # Merge into existing statement for the same src
140
- if stmt.default_import and not existing.default_import:
141
- existing.default_import = self.names.register(stmt.default_import)
142
-
143
- # Merge named imports
144
- def _merge_list(
145
- dst: list[ImportMember], src_list: list[ImportMember], is_type: bool = False
146
- ):
147
- for imp in src_list:
148
- key = (stmt.src, imp.name)
149
- if key in self._import_map:
150
- continue
151
- unique = self.names.register(imp.name)
152
- if unique != imp.name:
153
- imp.alias = unique
154
- self._import_map[key] = imp.alias or imp.name
155
- dst.append(imp)
156
-
157
- _merge_list(existing.values, stmt.values, is_type=False)
158
- _merge_list(existing.types, stmt.types, is_type=True)
159
- existing.side_effect = existing.side_effect or stmt.side_effect
160
- # Merge ordering constraints
161
- if stmt.before:
162
- # Preserve order, avoid duplicates
163
- seen = set(existing.before)
164
- for s in stmt.before:
165
- if s not in seen:
166
- existing.before.append(s)
167
- seen.add(s)
168
-
169
- def ordered_sources(self) -> list[ImportStatement]:
170
- """Return sources ordered to satisfy `before` constraints.
171
-
172
- Uses a stable topological sort (Kahn's algorithm) where insertion order
173
- is preserved among nodes with equal dependency rank. Falls back to
174
- insertion order if cycles are detected.
175
- """
176
- # Build graph: edge u->v means u must come before v
177
- keys = list(self.sources.keys())
178
- index = {k: i for i, k in enumerate(keys)} # for stability
179
- indegree: dict[str, int] = {k: 0 for k in keys}
180
- adj: dict[str, list[str]] = {k: [] for k in keys}
181
- for u, stmt in self.sources.items():
182
- for v in stmt.before:
183
- if v in adj: # only consider edges to imports present
184
- adj[u].append(v)
185
- indegree[v] += 1
186
-
187
- # Kahn's algorithm
188
- queue = [k for k, d in indegree.items() if d == 0]
189
- # Stable ordering of initial nodes
190
- queue.sort(key=lambda k: index[k])
191
- ordered: list[str] = []
192
- while queue:
193
- u = queue.pop(0)
194
- ordered.append(u)
195
- for v in adj[u]:
196
- indegree[v] -= 1
197
- if indegree[v] == 0:
198
- queue.append(v)
199
- queue.sort(key=lambda k: index[k])
200
-
201
- # If not all nodes processed, cycle detected; fall back to insertion order
202
- if len(ordered) != len(keys):
203
- ordered = keys
204
- return [self.sources[k] for k in ordered]