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