pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__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 (73) hide show
  1. pulse/__init__.py +9 -23
  2. pulse/app.py +6 -25
  3. pulse/cli/processes.py +1 -0
  4. pulse/codegen/codegen.py +43 -88
  5. pulse/codegen/js.py +35 -5
  6. pulse/codegen/templates/route.py +341 -254
  7. pulse/form.py +1 -1
  8. pulse/helpers.py +51 -27
  9. pulse/hooks/core.py +2 -2
  10. pulse/hooks/effects.py +1 -1
  11. pulse/hooks/init.py +2 -1
  12. pulse/hooks/setup.py +1 -1
  13. pulse/hooks/stable.py +2 -2
  14. pulse/hooks/states.py +2 -2
  15. pulse/html/props.py +3 -2
  16. pulse/html/tags.py +135 -0
  17. pulse/html/tags.pyi +4 -0
  18. pulse/js/__init__.py +110 -0
  19. pulse/js/__init__.pyi +95 -0
  20. pulse/js/_types.py +297 -0
  21. pulse/js/array.py +253 -0
  22. pulse/js/console.py +47 -0
  23. pulse/js/date.py +113 -0
  24. pulse/js/document.py +138 -0
  25. pulse/js/error.py +139 -0
  26. pulse/js/json.py +62 -0
  27. pulse/js/map.py +84 -0
  28. pulse/js/math.py +66 -0
  29. pulse/js/navigator.py +76 -0
  30. pulse/js/number.py +54 -0
  31. pulse/js/object.py +173 -0
  32. pulse/js/promise.py +150 -0
  33. pulse/js/regexp.py +54 -0
  34. pulse/js/set.py +109 -0
  35. pulse/js/string.py +35 -0
  36. pulse/js/weakmap.py +50 -0
  37. pulse/js/weakset.py +45 -0
  38. pulse/js/window.py +199 -0
  39. pulse/messages.py +22 -3
  40. pulse/proxy.py +21 -8
  41. pulse/react_component.py +167 -14
  42. pulse/reactive_extensions.py +5 -5
  43. pulse/render_session.py +144 -34
  44. pulse/renderer.py +80 -115
  45. pulse/routing.py +1 -18
  46. pulse/transpiler/__init__.py +131 -0
  47. pulse/transpiler/builtins.py +731 -0
  48. pulse/transpiler/constants.py +110 -0
  49. pulse/transpiler/context.py +26 -0
  50. pulse/transpiler/errors.py +2 -0
  51. pulse/transpiler/function.py +250 -0
  52. pulse/transpiler/ids.py +16 -0
  53. pulse/transpiler/imports.py +409 -0
  54. pulse/transpiler/js_module.py +274 -0
  55. pulse/transpiler/modules/__init__.py +30 -0
  56. pulse/transpiler/modules/asyncio.py +38 -0
  57. pulse/transpiler/modules/json.py +20 -0
  58. pulse/transpiler/modules/math.py +320 -0
  59. pulse/transpiler/modules/re.py +466 -0
  60. pulse/transpiler/modules/tags.py +268 -0
  61. pulse/transpiler/modules/typing.py +59 -0
  62. pulse/transpiler/nodes.py +1216 -0
  63. pulse/transpiler/py_module.py +119 -0
  64. pulse/transpiler/transpiler.py +938 -0
  65. pulse/transpiler/utils.py +4 -0
  66. pulse/vdom.py +112 -6
  67. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
  68. pulse_framework-0.1.48.dist-info/RECORD +119 -0
  69. pulse/codegen/imports.py +0 -204
  70. pulse/css.py +0 -155
  71. pulse_framework-0.1.46.dist-info/RECORD +0 -80
  72. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
  73. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.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]
pulse/vdom.py CHANGED
@@ -7,6 +7,7 @@ the TypeScript UINode format exactly, eliminating the need for translation.
7
7
 
8
8
  import functools
9
9
  import math
10
+ import re
10
11
  import warnings
11
12
  from collections.abc import Callable, Iterable, Sequence
12
13
  from inspect import Parameter, signature
@@ -14,11 +15,13 @@ from types import NoneType
14
15
  from typing import (
15
16
  Any,
16
17
  Generic,
18
+ Literal,
17
19
  NamedTuple,
18
20
  NotRequired,
19
21
  ParamSpec,
20
22
  TypeAlias,
21
23
  TypedDict,
24
+ final,
22
25
  overload,
23
26
  override,
24
27
  )
@@ -94,11 +97,16 @@ class Callback(NamedTuple):
94
97
  n_args: int
95
98
 
96
99
 
97
- def NOOP(*_args: Any):
98
- return None
99
-
100
-
100
+ @final
101
101
  class Node:
102
+ __slots__ = (
103
+ "tag",
104
+ "props",
105
+ "children",
106
+ "allow_children",
107
+ "key",
108
+ )
109
+
102
110
  tag: str
103
111
  props: dict[str, Any] | None
104
112
  children: "Sequence[Element] | None"
@@ -284,7 +292,19 @@ class Component(Generic[P]):
284
292
  return self.name
285
293
 
286
294
 
295
+ @final
287
296
  class ComponentNode:
297
+ __slots__ = (
298
+ "fn",
299
+ "args",
300
+ "kwargs",
301
+ "key",
302
+ "name",
303
+ "takes_children",
304
+ "hooks",
305
+ "contents",
306
+ )
307
+
288
308
  fn: Callable[..., Any]
289
309
  args: tuple[Any, ...]
290
310
  kwargs: dict[str, Any]
@@ -292,6 +312,7 @@ class ComponentNode:
292
312
  name: str
293
313
  takes_children: bool
294
314
  hooks: HookContext
315
+ contents: "Element | None"
295
316
 
296
317
  def __init__(
297
318
  self,
@@ -309,7 +330,7 @@ class ComponentNode:
309
330
  self.name = name or _infer_component_name(fn)
310
331
  self.takes_children = takes_children
311
332
  # Used for rendering
312
- self.contents: Element | None = None
333
+ self.contents = None
313
334
  self.hooks = HookContext()
314
335
  # Dev-only validation for JSON-unsafe values
315
336
  if env.pulse_env == "dev":
@@ -388,11 +409,95 @@ Callbacks = dict[str, Callback]
388
409
  VDOM: TypeAlias = VDOMNode | Primitive
389
410
  Props = dict[str, Any]
390
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
+
391
484
  # ----------------------------------------------------------------------------
392
485
  # Component naming heuristics
393
486
  # ----------------------------------------------------------------------------
394
487
 
395
488
 
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
+
396
501
  def _flatten_children(
397
502
  children: Children, *, parent_name: str, warn_stacklevel: int = 5
398
503
  ) -> Sequence[Element]:
@@ -424,9 +529,10 @@ def _flatten_children(
424
529
  visit(sub)
425
530
  if missing_key:
426
531
  # Warn once per iterable without keys on its elements.
532
+ clean_name = _clean_parent_name_for_warning(parent_name)
427
533
  warnings.warn(
428
534
  (
429
- f"[Pulse] Iterable children of {parent_name} contain elements without 'key'. "
535
+ f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
430
536
  "Add a stable 'key' to each element inside iterables to improve reconciliation."
431
537
  ),
432
538
  stacklevel=warn_stacklevel,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.46
3
+ Version: 0.1.48
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=lHlGKQ8kTRTx30cvL3EaBjBZ7rNYVjeItsganPsLqoQ,31540
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=5Z8UXzw5rHco7_W67NVhp0fEOKmW8Ewb-_inDz32zMI,7052
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=v054teQPOFNJZMgs_G7-BIGsvTLvolTEABgtoSUR3_c,14890
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=wDKz8zJvLTpcowEoHclOppindK7Xp0Qquh58dVWKgdc,7255
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.48.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
117
+ pulse_framework-0.1.48.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
118
+ pulse_framework-0.1.48.dist-info/METADATA,sha256=NYRK6Ja0Dxl3wB3n24TdGbBO8FWGp1oOnGfeSKb6Hqo,580
119
+ pulse_framework-0.1.48.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]
pulse/css.py DELETED
@@ -1,155 +0,0 @@
1
- import hashlib
2
- import inspect
3
- from collections.abc import Iterable, Iterator, MutableMapping
4
- from dataclasses import dataclass
5
- from pathlib import Path
6
- from typing import override
7
-
8
- _CSS_MODULES: MutableMapping[Path, "CssModule"] = {}
9
- _CSS_IMPORTS: dict[str, "CssImport"] = {}
10
-
11
-
12
- def _caller_file() -> Path:
13
- frame = inspect.currentframe()
14
- try:
15
- if frame is None or frame.f_back is None:
16
- raise RuntimeError("Cannot determine caller frame for ps.css()")
17
- caller = frame.f_back
18
- # Walk past helper wrappers (ps.css may be imported under different name)
19
- while caller and caller.f_code.co_filename == __file__:
20
- caller = caller.f_back
21
- if caller is None:
22
- raise RuntimeError("Cannot determine caller for ps.css()")
23
- return Path(caller.f_code.co_filename).resolve()
24
- finally:
25
- del frame
26
-
27
-
28
- def css_module(path: str | Path, *, relative: bool = False) -> "CssModule":
29
- source = Path(path)
30
- caller = _caller_file()
31
- if relative:
32
- source = caller.parent / source
33
- source = source.resolve()
34
- if not source.exists():
35
- raise FileNotFoundError(f"CSS module '{source}' not found")
36
- module = _CSS_MODULES.get(source)
37
- if not module:
38
- module = CssModule.create(source)
39
- _CSS_MODULES[source] = module
40
- return module
41
-
42
-
43
- def css(path: str | Path, *, relative: bool = False) -> "CssImport":
44
- caller = _caller_file()
45
- if relative:
46
- source_path = (caller.parent / Path(path)).resolve()
47
- if not source_path.exists():
48
- raise FileNotFoundError(
49
- f"CSS import '{path}' not found relative to {caller.parent}"
50
- )
51
- key = f"file://{source_path}"
52
- existing = _CSS_IMPORTS.get(key)
53
- if existing:
54
- return existing
55
- imp = CssImport(
56
- _import_id(str(source_path)), specifier=None, source_path=source_path
57
- )
58
- _CSS_IMPORTS[key] = imp
59
- return imp
60
-
61
- spec = str(path)
62
- existing = _CSS_IMPORTS.get(spec)
63
- if existing:
64
- return existing
65
- imp = CssImport(_import_id(spec), specifier=spec, source_path=None)
66
- _CSS_IMPORTS[spec] = imp
67
- return imp
68
-
69
-
70
- def registered_css_modules() -> list["CssModule"]:
71
- return list(_CSS_MODULES.values())
72
-
73
-
74
- def registered_css_imports() -> list["CssImport"]:
75
- return list(_CSS_IMPORTS.values())
76
-
77
-
78
- @dataclass(frozen=True)
79
- class CssModule:
80
- id: str
81
- source_path: Path
82
-
83
- @staticmethod
84
- def create(path: Path) -> "CssModule":
85
- module_id = _module_id(path)
86
- return CssModule(module_id, path)
87
-
88
- def __getattr__(self, key: str) -> "CssReference":
89
- if key.startswith("__") and key.endswith("__"):
90
- raise AttributeError(key)
91
- return CssReference(self, key)
92
-
93
- def __getitem__(self, key: str) -> "CssReference":
94
- return self.__getattr__(key)
95
-
96
- def iter(self, names: Iterable[str]) -> Iterator["CssReference"]:
97
- for name in names:
98
- yield CssReference(self, name)
99
-
100
-
101
- @dataclass(frozen=True)
102
- class CssReference:
103
- module: CssModule
104
- name: str
105
-
106
- def __post_init__(self) -> None:
107
- if not self.name:
108
- raise ValueError("CSS class name cannot be empty")
109
-
110
- def __bool__(self) -> bool:
111
- raise TypeError("CssReference objects cannot be coerced to bool")
112
-
113
- def __int__(self) -> int:
114
- raise TypeError("CssReference objects cannot be converted to int")
115
-
116
- def __float__(self) -> float:
117
- raise TypeError("CssReference objects cannot be converted to float")
118
-
119
- @override
120
- def __str__(self) -> str:
121
- raise TypeError("CssReference objects cannot be converted to str")
122
-
123
- @override
124
- def __repr__(self) -> str:
125
- return f"CssReference(module={self.module.id!r}, name={self.name!r})"
126
-
127
-
128
- def _module_id(path: Path) -> str:
129
- data = str(path).encode("utf-8")
130
- digest = hashlib.sha1(data).hexdigest()
131
- return f"css_{digest[:12]}"
132
-
133
-
134
- @dataclass(frozen=True)
135
- class CssImport:
136
- id: str
137
- specifier: str | None
138
- source_path: Path | None
139
-
140
-
141
- def _import_id(value: str) -> str:
142
- data = value.encode("utf-8")
143
- digest = hashlib.sha1(data).hexdigest()
144
- return f"css_import_{digest[:12]}"
145
-
146
-
147
- __all__ = [
148
- "CssModule",
149
- "CssReference",
150
- "CssImport",
151
- "css",
152
- "css_module",
153
- "registered_css_modules",
154
- "registered_css_imports",
155
- ]