pulse-framework 0.1.51__py3-none-any.whl → 0.1.52__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 +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -1001
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
- pulse_framework-0.1.52.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -599
- pulse_framework-0.1.51.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/vdom.py
DELETED
|
@@ -1,599 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HTML library that generates UI tree nodes directly.
|
|
3
|
-
|
|
4
|
-
This library provides a Python API for building UI trees that match
|
|
5
|
-
the TypeScript UINode format exactly, eliminating the need for translation.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import functools
|
|
9
|
-
import re
|
|
10
|
-
import warnings
|
|
11
|
-
from collections.abc import Callable, Iterable, Sequence
|
|
12
|
-
from inspect import Parameter, signature
|
|
13
|
-
from types import NoneType
|
|
14
|
-
from typing import (
|
|
15
|
-
Any,
|
|
16
|
-
Generic,
|
|
17
|
-
Literal,
|
|
18
|
-
NamedTuple,
|
|
19
|
-
NotRequired,
|
|
20
|
-
ParamSpec,
|
|
21
|
-
TypeAlias,
|
|
22
|
-
TypedDict,
|
|
23
|
-
final,
|
|
24
|
-
overload,
|
|
25
|
-
override,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
from pulse.env import env
|
|
29
|
-
from pulse.hooks.core import HookContext
|
|
30
|
-
from pulse.hooks.init import rewrite_init_blocks
|
|
31
|
-
|
|
32
|
-
# ============================================================================
|
|
33
|
-
# Core VDOM
|
|
34
|
-
# ============================================================================
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class VDOMNode(TypedDict):
|
|
38
|
-
tag: str
|
|
39
|
-
key: NotRequired[str]
|
|
40
|
-
props: NotRequired[dict[str, Any]] # does not include callbacks
|
|
41
|
-
children: "NotRequired[Sequence[VDOMNode | Primitive] | None]"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class Callback(NamedTuple):
|
|
45
|
-
fn: Callable[..., Any]
|
|
46
|
-
n_args: int
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@final
|
|
50
|
-
class Node:
|
|
51
|
-
__slots__ = (
|
|
52
|
-
"tag",
|
|
53
|
-
"props",
|
|
54
|
-
"children",
|
|
55
|
-
"allow_children",
|
|
56
|
-
"key",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
tag: str
|
|
60
|
-
props: dict[str, Any] | None
|
|
61
|
-
children: "list[Element] | None"
|
|
62
|
-
allow_children: bool
|
|
63
|
-
key: str | None
|
|
64
|
-
|
|
65
|
-
def __init__(
|
|
66
|
-
self,
|
|
67
|
-
tag: str,
|
|
68
|
-
props: dict[str, Any] | None | None = None,
|
|
69
|
-
children: "Children | None" = None,
|
|
70
|
-
key: str | None = None,
|
|
71
|
-
allow_children: bool = True,
|
|
72
|
-
):
|
|
73
|
-
self.tag = tag
|
|
74
|
-
self.props = props or None
|
|
75
|
-
self.children = (
|
|
76
|
-
_flatten_children(children, parent_name=f"<{self.tag}>")
|
|
77
|
-
if children
|
|
78
|
-
else None
|
|
79
|
-
)
|
|
80
|
-
self.allow_children = allow_children
|
|
81
|
-
self.key = key or None
|
|
82
|
-
if key is not None and not isinstance(key, str):
|
|
83
|
-
raise ValueError("key must be a string or None")
|
|
84
|
-
if not self.allow_children and children:
|
|
85
|
-
clean_tag = clean_element_name(self.tag)
|
|
86
|
-
raise ValueError(f"{clean_tag} cannot have children")
|
|
87
|
-
|
|
88
|
-
# --- Pretty printing helpers -------------------------------------------------
|
|
89
|
-
@override
|
|
90
|
-
def __repr__(self) -> str:
|
|
91
|
-
return (
|
|
92
|
-
f"Node(tag={self.tag!r}, key={self.key!r}, props={_short_props(self.props)}, "
|
|
93
|
-
f"children={_short_children(self.children)})"
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
def __getitem__(
|
|
97
|
-
self,
|
|
98
|
-
children_arg: "Child | tuple[Child, ...]",
|
|
99
|
-
):
|
|
100
|
-
"""Support indexing syntax: div()[children] or div()["text"]
|
|
101
|
-
|
|
102
|
-
Children may include iterables (lists, generators) of nodes, which will
|
|
103
|
-
be flattened during render.
|
|
104
|
-
"""
|
|
105
|
-
if self.children:
|
|
106
|
-
raise ValueError(f"Node already has children: {self.children}")
|
|
107
|
-
|
|
108
|
-
if isinstance(children_arg, tuple):
|
|
109
|
-
new_children = list(children_arg)
|
|
110
|
-
else:
|
|
111
|
-
new_children = [children_arg]
|
|
112
|
-
|
|
113
|
-
return Node(
|
|
114
|
-
tag=self.tag,
|
|
115
|
-
props=self.props,
|
|
116
|
-
children=new_children,
|
|
117
|
-
key=self.key,
|
|
118
|
-
allow_children=self.allow_children,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
@staticmethod
|
|
122
|
-
def from_vdom(
|
|
123
|
-
vdom: "VDOM",
|
|
124
|
-
callbacks: "Callbacks | None" = None,
|
|
125
|
-
*,
|
|
126
|
-
path: str = "",
|
|
127
|
-
) -> "Node | Primitive":
|
|
128
|
-
"""Create a Node tree from a VDOM structure.
|
|
129
|
-
|
|
130
|
-
- Primitive values are returned as-is
|
|
131
|
-
- Callbacks can be reattached by providing both `callbacks` (the
|
|
132
|
-
callable registry) and `callback_props` (props per VDOM path)
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
if isinstance(vdom, (str, int, float, bool, NoneType)):
|
|
136
|
-
return vdom
|
|
137
|
-
|
|
138
|
-
tag = vdom.get("tag")
|
|
139
|
-
props = vdom.get("props") or {}
|
|
140
|
-
key_value = vdom.get("key")
|
|
141
|
-
|
|
142
|
-
callbacks = callbacks or {}
|
|
143
|
-
prefix = f"{path}." if path else ""
|
|
144
|
-
prop_names: list[str] = []
|
|
145
|
-
for key in callbacks.keys():
|
|
146
|
-
if path:
|
|
147
|
-
if not key.startswith(prefix):
|
|
148
|
-
continue
|
|
149
|
-
remainder = key[len(prefix) :]
|
|
150
|
-
else:
|
|
151
|
-
remainder = key
|
|
152
|
-
if "." in remainder:
|
|
153
|
-
continue
|
|
154
|
-
prop_names.append(remainder)
|
|
155
|
-
if prop_names:
|
|
156
|
-
props = props.copy()
|
|
157
|
-
for name in prop_names:
|
|
158
|
-
callback_key = f"{path}.{name}" if path else name
|
|
159
|
-
callback = callbacks.get(callback_key)
|
|
160
|
-
if not callback:
|
|
161
|
-
raise ValueError(f"Missing callback '{callback_key}'")
|
|
162
|
-
props[name] = callback.fn
|
|
163
|
-
|
|
164
|
-
children_value: list[Element] | None = None
|
|
165
|
-
raw_children = vdom.get("children")
|
|
166
|
-
if raw_children is not None:
|
|
167
|
-
children_value = []
|
|
168
|
-
for idx, raw_child in enumerate(raw_children):
|
|
169
|
-
child_path = f"{path}.{idx}" if path else str(idx)
|
|
170
|
-
children_value.append(
|
|
171
|
-
Node.from_vdom(
|
|
172
|
-
raw_child,
|
|
173
|
-
callbacks=callbacks,
|
|
174
|
-
path=child_path,
|
|
175
|
-
)
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
return Node(
|
|
179
|
-
tag=tag,
|
|
180
|
-
props=props or None,
|
|
181
|
-
children=children_value,
|
|
182
|
-
key=key_value,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# ============================================================================
|
|
187
|
-
# Tag Definition Functions
|
|
188
|
-
# ============================================================================
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# --- Components ---
|
|
192
|
-
|
|
193
|
-
P = ParamSpec("P")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class Component(Generic[P]):
|
|
197
|
-
fn: "Callable[P, Element]"
|
|
198
|
-
name: str
|
|
199
|
-
_takes_children: bool
|
|
200
|
-
|
|
201
|
-
def __init__(self, fn: "Callable[P, Element]", name: str | None = None) -> None:
|
|
202
|
-
self.fn = fn
|
|
203
|
-
self.name = name or _infer_component_name(fn)
|
|
204
|
-
self._takes_children = _takes_children(fn)
|
|
205
|
-
|
|
206
|
-
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "ComponentNode":
|
|
207
|
-
key = kwargs.get("key")
|
|
208
|
-
if key is not None and not isinstance(key, str):
|
|
209
|
-
raise ValueError("key must be a string or None")
|
|
210
|
-
|
|
211
|
-
# Flatten children if component accepts them via `*children` parameter
|
|
212
|
-
if self._takes_children and args:
|
|
213
|
-
flattened = _flatten_children(
|
|
214
|
-
args, # pyright: ignore[reportArgumentType]
|
|
215
|
-
parent_name=f"<{self.name}>",
|
|
216
|
-
warn_stacklevel=4,
|
|
217
|
-
)
|
|
218
|
-
args = tuple(flattened) # pyright: ignore[reportAssignmentType]
|
|
219
|
-
|
|
220
|
-
return ComponentNode(
|
|
221
|
-
fn=self.fn,
|
|
222
|
-
key=key,
|
|
223
|
-
args=args,
|
|
224
|
-
kwargs=kwargs,
|
|
225
|
-
name=self.name,
|
|
226
|
-
takes_children=self._takes_children,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
@override
|
|
230
|
-
def __repr__(self) -> str:
|
|
231
|
-
return f"Component(name={self.name!r}, fn={_callable_qualname(self.fn)!r})"
|
|
232
|
-
|
|
233
|
-
@override
|
|
234
|
-
def __str__(self) -> str:
|
|
235
|
-
return self.name
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@final
|
|
239
|
-
class ComponentNode:
|
|
240
|
-
__slots__ = (
|
|
241
|
-
"fn",
|
|
242
|
-
"args",
|
|
243
|
-
"kwargs",
|
|
244
|
-
"key",
|
|
245
|
-
"name",
|
|
246
|
-
"takes_children",
|
|
247
|
-
"hooks",
|
|
248
|
-
"contents",
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
fn: Callable[..., Any]
|
|
252
|
-
args: tuple[Any, ...]
|
|
253
|
-
kwargs: dict[str, Any]
|
|
254
|
-
key: str | None
|
|
255
|
-
name: str
|
|
256
|
-
takes_children: bool
|
|
257
|
-
hooks: HookContext
|
|
258
|
-
contents: "Element | None"
|
|
259
|
-
|
|
260
|
-
def __init__(
|
|
261
|
-
self,
|
|
262
|
-
fn: Callable[..., Any],
|
|
263
|
-
args: tuple[Any, ...],
|
|
264
|
-
kwargs: dict[str, Any],
|
|
265
|
-
name: str | None = None,
|
|
266
|
-
key: str | None = None,
|
|
267
|
-
takes_children: bool = True,
|
|
268
|
-
) -> None:
|
|
269
|
-
self.fn = fn
|
|
270
|
-
self.args = args
|
|
271
|
-
self.kwargs = kwargs
|
|
272
|
-
self.key = key
|
|
273
|
-
self.name = name or _infer_component_name(fn)
|
|
274
|
-
self.takes_children = takes_children
|
|
275
|
-
# Used for rendering
|
|
276
|
-
self.contents = None
|
|
277
|
-
self.hooks = HookContext()
|
|
278
|
-
|
|
279
|
-
def __getitem__(self, children_arg: "Child | tuple[Child, ...]"):
|
|
280
|
-
if not self.takes_children:
|
|
281
|
-
raise TypeError(
|
|
282
|
-
f"Component {self.name} does not accept children. "
|
|
283
|
-
+ "Update the component signature to include '*children' to allow children."
|
|
284
|
-
)
|
|
285
|
-
if self.args:
|
|
286
|
-
raise ValueError(
|
|
287
|
-
f"Component {self.name} already received positional arguments. Pass all arguments as keyword arguments in order to pass children using brackets."
|
|
288
|
-
)
|
|
289
|
-
if not isinstance(children_arg, tuple):
|
|
290
|
-
children_arg = (children_arg,)
|
|
291
|
-
# Flatten children when component accepts them via `*children` parameter
|
|
292
|
-
flattened_children = _flatten_children(
|
|
293
|
-
children_arg, parent_name=f"<{self.name}>", warn_stacklevel=4
|
|
294
|
-
)
|
|
295
|
-
return ComponentNode(
|
|
296
|
-
fn=self.fn,
|
|
297
|
-
args=tuple(flattened_children),
|
|
298
|
-
kwargs=self.kwargs,
|
|
299
|
-
name=self.name,
|
|
300
|
-
key=self.key,
|
|
301
|
-
takes_children=self.takes_children,
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
@override
|
|
305
|
-
def __repr__(self) -> str:
|
|
306
|
-
return (
|
|
307
|
-
f"ComponentNode(name={self.name!r}, key={self.key!r}, "
|
|
308
|
-
f"args={_short_args(self.args)}, kwargs={_short_props(self.kwargs)})"
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@overload
|
|
313
|
-
def component(fn: "Callable[P, Element]") -> Component[P]: ...
|
|
314
|
-
@overload
|
|
315
|
-
def component(
|
|
316
|
-
fn: None = None, *, name: str | None = None
|
|
317
|
-
) -> "Callable[[Callable[P, Element]], Component[P]]": ...
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
# The explicit return type is necessary for the type checker to be happy
|
|
321
|
-
def component(
|
|
322
|
-
fn: "Callable[P, Element] | None" = None, *, name: str | None = None
|
|
323
|
-
) -> "Component[P] | Callable[[Callable[P, Element]], Component[P]]":
|
|
324
|
-
def decorator(fn: Callable[P, Element]):
|
|
325
|
-
rewritten = rewrite_init_blocks(fn)
|
|
326
|
-
return Component(rewritten, name)
|
|
327
|
-
|
|
328
|
-
if fn is not None:
|
|
329
|
-
return decorator(fn)
|
|
330
|
-
return decorator
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
Primitive = str | int | float | None
|
|
334
|
-
Element = Node | ComponentNode | Primitive
|
|
335
|
-
# A child can be an Element or any iterable yielding children (e.g., generators)
|
|
336
|
-
Child: TypeAlias = Element | Iterable[Element]
|
|
337
|
-
Children: TypeAlias = Sequence[Child]
|
|
338
|
-
|
|
339
|
-
Callbacks = dict[str, Callback]
|
|
340
|
-
VDOM: TypeAlias = VDOMNode | Primitive
|
|
341
|
-
Props = dict[str, Any]
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
# ============================================================================
|
|
345
|
-
# VDOM Operations (updates sent from server to client)
|
|
346
|
-
# ============================================================================
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
class ReplaceOperation(TypedDict):
|
|
350
|
-
type: Literal["replace"]
|
|
351
|
-
path: str
|
|
352
|
-
data: VDOM
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
# This payload makes it easy for the client to rebuild an array of React nodes
|
|
356
|
-
# from the previous children array:
|
|
357
|
-
# - Allocate array of size N
|
|
358
|
-
# - For i in 0..N-1, check the following scenarios
|
|
359
|
-
# - i matches the next index in `new` -> use provided tree
|
|
360
|
-
# - i matches the next index in `reuse` -> reuse previous child
|
|
361
|
-
# - otherwise, reuse the element at the same index
|
|
362
|
-
class ReconciliationOperation(TypedDict):
|
|
363
|
-
type: Literal["reconciliation"]
|
|
364
|
-
path: str
|
|
365
|
-
N: int
|
|
366
|
-
new: tuple[list[int], list[VDOM]]
|
|
367
|
-
reuse: tuple[list[int], list[int]]
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
class UpdatePropsDelta(TypedDict, total=False):
|
|
371
|
-
# Only send changed/new keys under `set` and removed keys under `remove`
|
|
372
|
-
set: Props
|
|
373
|
-
remove: list[str]
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
class UpdatePropsOperation(TypedDict):
|
|
377
|
-
type: Literal["update_props"]
|
|
378
|
-
path: str
|
|
379
|
-
data: UpdatePropsDelta
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
class PathDelta(TypedDict, total=False):
|
|
383
|
-
add: list[str]
|
|
384
|
-
remove: list[str]
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
class UpdateCallbacksOperation(TypedDict):
|
|
388
|
-
type: Literal["update_callbacks"]
|
|
389
|
-
path: str
|
|
390
|
-
data: PathDelta
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
class UpdateRenderPropsOperation(TypedDict):
|
|
394
|
-
type: Literal["update_render_props"]
|
|
395
|
-
path: str
|
|
396
|
-
data: PathDelta
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
class UpdateJsExprPathsOperation(TypedDict):
|
|
400
|
-
type: Literal["update_jsexpr_paths"]
|
|
401
|
-
path: str
|
|
402
|
-
data: PathDelta
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
VDOMOperation: TypeAlias = (
|
|
406
|
-
ReplaceOperation
|
|
407
|
-
| UpdatePropsOperation
|
|
408
|
-
| ReconciliationOperation
|
|
409
|
-
| UpdateCallbacksOperation
|
|
410
|
-
| UpdateRenderPropsOperation
|
|
411
|
-
| UpdateJsExprPathsOperation
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
# ----------------------------------------------------------------------------
|
|
416
|
-
# Component naming heuristics
|
|
417
|
-
# ----------------------------------------------------------------------------
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def clean_element_name(parent_name: str) -> str:
|
|
421
|
-
"""Strip $$ prefix and hexadecimal suffix from ReactComponent tags in warning messages.
|
|
422
|
-
|
|
423
|
-
ReactComponent tags are in the format <$$ComponentName_1a2b> or <$$ComponentName_1a2b.prop>.
|
|
424
|
-
This function strips the $$ prefix and _1a2b suffix to show just the component name.
|
|
425
|
-
"""
|
|
426
|
-
|
|
427
|
-
# Match ReactComponent tags: <$$ComponentName_hex> or <$$ComponentName_hex.prop>
|
|
428
|
-
# Strip the $$ prefix and _hex suffix but keep the rest (hex digits are 0-9, a-f)
|
|
429
|
-
return re.sub(r"\$\$([^_]+)_[0-9a-f]+", r"\1", parent_name)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def _flatten_children(
|
|
433
|
-
children: Children, *, parent_name: str, warn_stacklevel: int = 5
|
|
434
|
-
) -> list[Element]:
|
|
435
|
-
"""Flatten children and emit warnings for unkeyed iterables (dev mode only).
|
|
436
|
-
|
|
437
|
-
Args:
|
|
438
|
-
children: The children sequence to flatten.
|
|
439
|
-
parent_name: Name of the parent element for error messages.
|
|
440
|
-
warn_stacklevel: Stack level for warnings. Adjust based on call site:
|
|
441
|
-
- 5 for Node.__init__ via tag factory (user -> tag factory -> Node.__init__ -> _flatten_children -> visit -> warn)
|
|
442
|
-
- 4 for ComponentNode.__getitem__ or Component.__call__ (user -> method -> _flatten_children -> visit -> warn)
|
|
443
|
-
"""
|
|
444
|
-
flat: list[Element] = []
|
|
445
|
-
is_dev = env.pulse_env == "dev"
|
|
446
|
-
|
|
447
|
-
def visit(item: Child) -> None:
|
|
448
|
-
if isinstance(item, Iterable) and not isinstance(item, str):
|
|
449
|
-
# If any Node/ComponentNode yielded by this iterable lacks a key,
|
|
450
|
-
# emit a single warning for this iterable (dev mode only).
|
|
451
|
-
missing_key = False
|
|
452
|
-
for sub in item:
|
|
453
|
-
if (
|
|
454
|
-
is_dev
|
|
455
|
-
and isinstance(sub, (Node, ComponentNode))
|
|
456
|
-
and sub.key is None
|
|
457
|
-
):
|
|
458
|
-
missing_key = True
|
|
459
|
-
visit(sub)
|
|
460
|
-
if missing_key:
|
|
461
|
-
# Warn once per iterable without keys on its elements.
|
|
462
|
-
clean_name = clean_element_name(parent_name)
|
|
463
|
-
warnings.warn(
|
|
464
|
-
(
|
|
465
|
-
f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
|
|
466
|
-
"Add a stable 'key' to each element inside iterables to improve reconciliation."
|
|
467
|
-
),
|
|
468
|
-
stacklevel=warn_stacklevel,
|
|
469
|
-
)
|
|
470
|
-
else:
|
|
471
|
-
# Not an iterable child: must be a Element or primitive
|
|
472
|
-
flat.append(item)
|
|
473
|
-
|
|
474
|
-
for child in children:
|
|
475
|
-
visit(child)
|
|
476
|
-
|
|
477
|
-
seen_keys: set[str] = set()
|
|
478
|
-
for child in flat:
|
|
479
|
-
if isinstance(child, (Node, ComponentNode)) and child.key is not None:
|
|
480
|
-
if child.key in seen_keys:
|
|
481
|
-
clean_name = clean_element_name(parent_name)
|
|
482
|
-
raise ValueError(
|
|
483
|
-
f"[Pulse] Duplicate key '{child.key}' found among children of {clean_name}. "
|
|
484
|
-
+ "Keys must be unique per sibling set."
|
|
485
|
-
)
|
|
486
|
-
seen_keys.add(child.key)
|
|
487
|
-
|
|
488
|
-
return flat
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def _short_args(args: tuple[Any, ...], max_items: int = 4) -> list[str] | str:
|
|
492
|
-
if not args:
|
|
493
|
-
return []
|
|
494
|
-
out: list[str] = []
|
|
495
|
-
for a in args[: max_items - 1]:
|
|
496
|
-
s = repr(a)
|
|
497
|
-
if len(s) > 32:
|
|
498
|
-
s = s[:29] + "…" + s[-1]
|
|
499
|
-
out.append(s)
|
|
500
|
-
if len(args) > (max_items - 1):
|
|
501
|
-
out.append(f"…(+{len(args) - (max_items - 1)})")
|
|
502
|
-
return out
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def _infer_component_name(fn: Callable[..., Any]) -> str:
|
|
506
|
-
# Unwrap partials and single-level wrappers
|
|
507
|
-
original = fn
|
|
508
|
-
if isinstance(original, functools.partial):
|
|
509
|
-
original = original.func # type: ignore[attr-defined]
|
|
510
|
-
|
|
511
|
-
name: str | None = getattr(original, "__name__", None)
|
|
512
|
-
if name and name != "<lambda>":
|
|
513
|
-
return name
|
|
514
|
-
|
|
515
|
-
qualname: str | None = getattr(original, "__qualname__", None)
|
|
516
|
-
if qualname and "<locals>" not in qualname:
|
|
517
|
-
# Best-effort: take the last path component
|
|
518
|
-
return qualname.split(".")[-1]
|
|
519
|
-
|
|
520
|
-
# Callable instances (classes defining __call__)
|
|
521
|
-
cls = getattr(original, "__class__", None)
|
|
522
|
-
if cls and getattr(cls, "__name__", None):
|
|
523
|
-
return cls.__name__
|
|
524
|
-
|
|
525
|
-
# Fallback
|
|
526
|
-
return "Component"
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
def _callable_qualname(fn: Callable[..., Any]) -> str:
|
|
530
|
-
mod = getattr(fn, "__module__", None) or "__main__"
|
|
531
|
-
qual = (
|
|
532
|
-
getattr(fn, "__qualname__", None)
|
|
533
|
-
or getattr(fn, "__name__", None)
|
|
534
|
-
or "<callable>"
|
|
535
|
-
)
|
|
536
|
-
return f"{mod}.{qual}"
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _takes_children(fn: Callable[..., Any]) -> bool:
|
|
540
|
-
"""Return True if function accepts children via `*children` parameter.
|
|
541
|
-
|
|
542
|
-
Convention: A component accepts children if and only if it has a VAR_POSITIONAL
|
|
543
|
-
parameter named "children". This convention should be documented in user-facing docs.
|
|
544
|
-
"""
|
|
545
|
-
try:
|
|
546
|
-
sig = signature(fn)
|
|
547
|
-
except (ValueError, TypeError):
|
|
548
|
-
# Builtins or callables without inspectable signature: assume no children
|
|
549
|
-
return False
|
|
550
|
-
for p in sig.parameters.values():
|
|
551
|
-
if p.kind is Parameter.VAR_POSITIONAL and p.name == "children":
|
|
552
|
-
return True
|
|
553
|
-
return False
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
# ----------------------------------------------------------------------------
|
|
557
|
-
# Formatting helpers (internal)
|
|
558
|
-
# ----------------------------------------------------------------------------
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
def _pretty_repr(node: Element):
|
|
562
|
-
if isinstance(node, Node):
|
|
563
|
-
return f"<{node.tag}>"
|
|
564
|
-
if isinstance(node, ComponentNode):
|
|
565
|
-
return f"<{node.name}"
|
|
566
|
-
return repr(node)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
def _short_props(
|
|
570
|
-
props: dict[str, Any] | None, max_items: int = 6
|
|
571
|
-
) -> dict[str, Any] | str:
|
|
572
|
-
if not props:
|
|
573
|
-
return {}
|
|
574
|
-
items = list(props.items())
|
|
575
|
-
if len(items) <= max_items:
|
|
576
|
-
return props
|
|
577
|
-
head = dict(items[: max_items - 1])
|
|
578
|
-
return {**head, "…": f"+{len(items) - (max_items - 1)} more"}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
def _short_children(
|
|
582
|
-
children: Sequence[Child] | None, max_items: int = 4
|
|
583
|
-
) -> list[str] | str:
|
|
584
|
-
if not children:
|
|
585
|
-
return []
|
|
586
|
-
out: list[str] = []
|
|
587
|
-
i = 0
|
|
588
|
-
while i < len(children) and len(out) < max_items:
|
|
589
|
-
child = children[i]
|
|
590
|
-
i += 1
|
|
591
|
-
if isinstance(child, Iterable) and not isinstance(child, str):
|
|
592
|
-
child = list(child)
|
|
593
|
-
n_items = min(len(child), max_items - len(out))
|
|
594
|
-
out.extend(_pretty_repr(c) for c in child[:n_items])
|
|
595
|
-
else:
|
|
596
|
-
out.append(_pretty_repr(child))
|
|
597
|
-
if len(children) > (max_items - 1):
|
|
598
|
-
out.append(f"…(+{len(children) - (max_items - 1)})")
|
|
599
|
-
return out
|