pulse-framework 0.1.62__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 (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,345 @@
1
+ """Route code generation using transpiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from pulse.transpiler import (
8
+ Constant,
9
+ Import,
10
+ collect_function_graph,
11
+ emit,
12
+ get_registered_imports,
13
+ registered_constants,
14
+ registered_functions,
15
+ )
16
+ from pulse.transpiler.emit_context import EmitContext
17
+ from pulse.transpiler.function import AnyJsFunction
18
+
19
+
20
+ def _get_import_src(imp: Import) -> str:
21
+ """Get the import source path, remapping to asset path for local imports."""
22
+ if imp.asset:
23
+ return imp.asset.import_path()
24
+ return imp.src
25
+
26
+
27
+ def _generate_import_statement(
28
+ src: str,
29
+ imports: list[Import],
30
+ ) -> str:
31
+ """Generate import statement(s) for a source module.
32
+
33
+ Args:
34
+ src: The original source path (may be remapped for local imports)
35
+ imports: List of Import objects for this source
36
+ """
37
+ default_imports: list[Import] = []
38
+ namespace_imports: list[Import] = []
39
+ named_imports: list[Import] = []
40
+ type_imports: list[Import] = []
41
+ has_side_effect = False
42
+
43
+ for imp in imports:
44
+ if imp.is_side_effect:
45
+ has_side_effect = True
46
+ elif imp.is_namespace:
47
+ namespace_imports.append(imp)
48
+ elif imp.is_default:
49
+ if imp.is_type:
50
+ type_imports.append(imp)
51
+ else:
52
+ default_imports.append(imp)
53
+ else:
54
+ if imp.is_type:
55
+ type_imports.append(imp)
56
+ else:
57
+ named_imports.append(imp)
58
+
59
+ # Remap source path if this is a local import
60
+ import_src = src
61
+ for imp in imports:
62
+ if imp.asset:
63
+ import_src = imp.asset.import_path()
64
+ break
65
+
66
+ lines: list[str] = []
67
+
68
+ # Namespace import (only one allowed per source)
69
+ if namespace_imports:
70
+ imp = namespace_imports[0]
71
+ lines.append(f'import * as {imp.js_name} from "{import_src}";')
72
+
73
+ # Default import (only one allowed per source)
74
+ if default_imports:
75
+ imp = default_imports[0]
76
+ lines.append(f'import {imp.js_name} from "{import_src}";')
77
+
78
+ # Named imports
79
+ if named_imports:
80
+ members = [f"{imp.name} as {imp.js_name}" for imp in named_imports]
81
+ lines.append(f'import {{ {", ".join(members)} }} from "{import_src}";')
82
+
83
+ # Type imports
84
+ if type_imports:
85
+ type_members: list[str] = []
86
+ for imp in type_imports:
87
+ if imp.is_default:
88
+ type_members.append(f"default as {imp.js_name}")
89
+ else:
90
+ type_members.append(f"{imp.name} as {imp.js_name}")
91
+ lines.append(
92
+ f'import type {{ {", ".join(type_members)} }} from "{import_src}";'
93
+ )
94
+
95
+ # Side-effect only import (only if no other imports)
96
+ if (
97
+ has_side_effect
98
+ and not default_imports
99
+ and not namespace_imports
100
+ and not named_imports
101
+ and not type_imports
102
+ ):
103
+ lines.append(f'import "{import_src}";')
104
+
105
+ return "\n".join(lines)
106
+
107
+
108
+ def _generate_imports_section(imports: Sequence[Import]) -> str:
109
+ """Generate the full imports section with deduplication and topological ordering.
110
+
111
+ Args:
112
+ imports: List of Import objects to generate (should be eager imports only)
113
+ """
114
+ if not imports:
115
+ return ""
116
+
117
+ # Deduplicate imports by ID
118
+ seen_ids: set[str] = set()
119
+ unique_imports: list[Import] = []
120
+ for imp in imports:
121
+ if imp.id not in seen_ids:
122
+ seen_ids.add(imp.id)
123
+ unique_imports.append(imp)
124
+
125
+ # Group by source
126
+ grouped: dict[str, list[Import]] = {}
127
+ for imp in unique_imports:
128
+ if imp.src not in grouped:
129
+ grouped[imp.src] = []
130
+ grouped[imp.src].append(imp)
131
+
132
+ # Topological sort using Import.before constraints (Kahn's algorithm)
133
+ keys = list(grouped.keys())
134
+ if not keys:
135
+ return ""
136
+
137
+ index = {k: i for i, k in enumerate(keys)} # for stability
138
+ indegree: dict[str, int] = {k: 0 for k in keys}
139
+ adj: dict[str, list[str]] = {k: [] for k in keys}
140
+
141
+ for src, src_imports in grouped.items():
142
+ for imp in src_imports:
143
+ for before_src in imp.before:
144
+ if before_src in adj:
145
+ adj[src].append(before_src)
146
+ indegree[before_src] += 1
147
+
148
+ queue = [k for k, d in indegree.items() if d == 0]
149
+ queue.sort(key=lambda k: index[k])
150
+ ordered: list[str] = []
151
+
152
+ while queue:
153
+ u = queue.pop(0)
154
+ ordered.append(u)
155
+ for v in adj[u]:
156
+ indegree[v] -= 1
157
+ if indegree[v] == 0:
158
+ queue.append(v)
159
+ queue.sort(key=lambda k: index[k])
160
+
161
+ # Fall back to insertion order if cycle detected
162
+ if len(ordered) != len(keys):
163
+ ordered = keys
164
+
165
+ lines: list[str] = []
166
+ for src in ordered:
167
+ stmt = _generate_import_statement(src, grouped[src])
168
+ if stmt:
169
+ lines.append(stmt)
170
+
171
+ return "\n".join(lines)
172
+
173
+
174
+ def _generate_lazy_imports_section(imports: Sequence[Import]) -> str:
175
+ """Generate lazy import factories for code-splitting.
176
+
177
+ Lazy imports are emitted as factory functions compatible with React.lazy.
178
+ React.lazy requires factories that return { default: Component }.
179
+
180
+ For default imports: () => import("./Chart")
181
+ For named imports: () => import("./Chart").then(m => ({ default: m.LineChart }))
182
+
183
+ Args:
184
+ imports: List of lazy Import objects
185
+ """
186
+ if not imports:
187
+ return ""
188
+
189
+ lines: list[str] = ["// Lazy imports"]
190
+ for imp in imports:
191
+ import_src = _get_import_src(imp)
192
+
193
+ if imp.is_default or imp.is_namespace:
194
+ # Default/namespace: () => import("module") - already has { default }
195
+ factory = f'() => import("{import_src}")'
196
+ else:
197
+ # Named: wrap in { default } for React.lazy compatibility
198
+ factory = (
199
+ f'() => import("{import_src}").then(m => ({{ default: m.{imp.name} }}))'
200
+ )
201
+
202
+ lines.append(f"const {imp.js_name} = {factory};")
203
+
204
+ return "\n".join(lines)
205
+
206
+
207
+ def _generate_constants_section(constants: Sequence[Constant]) -> str:
208
+ """Generate the constants section."""
209
+ if not constants:
210
+ return ""
211
+
212
+ lines: list[str] = ["// Constants"]
213
+ for const in constants:
214
+ js_value = emit(const.expr)
215
+ lines.append(f"const {const.js_name} = {js_value};")
216
+
217
+ return "\n".join(lines)
218
+
219
+
220
+ def _generate_functions_section(functions: Sequence[AnyJsFunction]) -> str:
221
+ """Generate the functions section with actual transpiled code."""
222
+ if not functions:
223
+ return ""
224
+
225
+ lines: list[str] = ["// Functions"]
226
+ for fn in functions:
227
+ js_code = emit(fn.transpile())
228
+ lines.append(js_code)
229
+
230
+ return "\n".join(lines)
231
+
232
+
233
+ def _generate_registry_section(
234
+ imports: Sequence[Import],
235
+ constants: Sequence[Constant],
236
+ functions: Sequence[AnyJsFunction],
237
+ ) -> str:
238
+ """Generate the unified registry from all registered entities.
239
+
240
+ The registry contains all values that need to be looked up at runtime,
241
+ keyed by their unique ID.
242
+ """
243
+ lines: list[str] = []
244
+ lines.append("// Unified Registry")
245
+ lines.append("const __registry = {")
246
+
247
+ # Add imports
248
+ for imp in imports:
249
+ if not imp.is_side_effect:
250
+ lines.append(f' "{imp.id}": {imp.js_name},')
251
+
252
+ # Add constants
253
+ for const in constants:
254
+ lines.append(f' "{const.id}": {const.js_name},')
255
+
256
+ # Add functions
257
+ for fn in functions:
258
+ lines.append(f' "{fn.id}": {fn.js_name},')
259
+
260
+ lines.append("};")
261
+
262
+ return "\n".join(lines)
263
+
264
+
265
+ def generate_route(
266
+ path: str,
267
+ route_file_path: str,
268
+ ) -> str:
269
+ """Generate a route file with all registered imports, functions, and components.
270
+
271
+ Args:
272
+ path: The route path (e.g., "/users/:id")
273
+ route_file_path: Path from pulse root (e.g., "routes/users/index.tsx")
274
+ """
275
+ with EmitContext(route_file_path=route_file_path):
276
+ # Add core Pulse imports
277
+ pulse_view_import = Import("PulseView", "pulse-ui-client")
278
+
279
+ # Collect function graph (constants + functions in dependency order)
280
+ fn_constants, funcs = collect_function_graph(registered_functions())
281
+
282
+ # Include all registered constants (not just function dependencies)
283
+ # This ensures constants used as component tags are included
284
+ fn_const_ids = {c.id for c in fn_constants}
285
+ all_constants = list(fn_constants)
286
+ for const in registered_constants():
287
+ if const.id not in fn_const_ids:
288
+ all_constants.append(const)
289
+ constants = all_constants
290
+
291
+ # Get all registered imports and split by lazy flag
292
+ all_imports = list(get_registered_imports())
293
+ eager_imports = [imp for imp in all_imports if not imp.lazy]
294
+ lazy_imports = [imp for imp in all_imports if imp.lazy]
295
+
296
+ # Generate output sections
297
+ output_parts: list[str] = []
298
+
299
+ # Eager imports (ES6 import statements)
300
+ imports_section = _generate_imports_section(eager_imports)
301
+ if imports_section:
302
+ output_parts.append(imports_section)
303
+
304
+ output_parts.append("")
305
+
306
+ # Lazy imports (factory functions)
307
+ lazy_section = _generate_lazy_imports_section(lazy_imports)
308
+ if lazy_section:
309
+ output_parts.append(lazy_section)
310
+ output_parts.append("")
311
+
312
+ if constants:
313
+ output_parts.append(_generate_constants_section(constants))
314
+ output_parts.append("")
315
+
316
+ if funcs:
317
+ output_parts.append(_generate_functions_section(funcs))
318
+ output_parts.append("")
319
+
320
+ # Generate the unified registry including all imports, constants and functions
321
+ output_parts.append(_generate_registry_section(all_imports, constants, funcs))
322
+ output_parts.append("")
323
+
324
+ # Route component
325
+ pulse_view_js = pulse_view_import.js_name
326
+ output_parts.append(f'''const path = "{path}";
327
+
328
+ export default function RouteComponent() {{
329
+ return (
330
+ <{pulse_view_js} key={{path}} registry={{__registry}} path={{path}} />
331
+ );
332
+ }}''')
333
+ output_parts.append("")
334
+
335
+ # Headers function
336
+ output_parts.append("""// Action and loader headers are not returned automatically
337
+ function hasAnyHeaders(headers) {
338
+ return [...headers].length > 0;
339
+ }
340
+
341
+ export function headers({ actionHeaders, loaderHeaders }) {
342
+ return hasAnyHeaders(actionHeaders) ? actionHeaders : loaderHeaders;
343
+ }""")
344
+
345
+ return "\n".join(output_parts)
@@ -0,0 +1,42 @@
1
+ from mako.template import Template
2
+
3
+ # Mako template for routes configuration
4
+ ROUTES_CONFIG_TEMPLATE = Template(
5
+ """import {
6
+ type RouteConfig,
7
+ route,
8
+ layout,
9
+ index,
10
+ } from "@react-router/dev/routes";
11
+ import { rrPulseRouteTree, type RRRouteObject } from "./routes.runtime";
12
+
13
+ function toDevRoute(node: RRRouteObject): any {
14
+ const children = (node.children ?? []).map(toDevRoute);
15
+ if (node.index) return index(node.file!);
16
+ if (node.path !== undefined) {
17
+ return children.length ? route(node.path, node.file!, children) : route(node.path, node.file!);
18
+ }
19
+ // Layout node (pathless)
20
+ return layout(node.file!, children);
21
+ }
22
+
23
+ export const routes = [
24
+ layout("${pulse_dir}/_layout.tsx", rrPulseRouteTree.map(toDevRoute)),
25
+ ] satisfies RouteConfig;
26
+ """
27
+ )
28
+
29
+ # Runtime route tree for matching (used by main layout loader)
30
+ ROUTES_RUNTIME_TEMPLATE = Template(
31
+ """import type { RouteObject } from "react-router";
32
+
33
+ export type RRRouteObject = RouteObject & {
34
+ id: string;
35
+ uniquePath?: string;
36
+ children?: RRRouteObject[];
37
+ file: string;
38
+ }
39
+
40
+ export const rrPulseRouteTree = ${routes_str} satisfies RRRouteObject[];
41
+ """
42
+ )
pulse/codegen/utils.py ADDED
@@ -0,0 +1,20 @@
1
+ from collections.abc import Iterable
2
+
3
+
4
+ class NameRegistry:
5
+ def __init__(self, names: Iterable[str] | None = None) -> None:
6
+ self.names: set[str] = set(names or [])
7
+
8
+ def register(self, name: str, suffix: str | None = None, allow_rename: bool = True):
9
+ if name not in self.names:
10
+ self.names.add(name)
11
+ return name
12
+ if not allow_rename:
13
+ raise ValueError(f"Duplicate identifier {name}")
14
+ i = 2
15
+ aliased = f"{name}{i}"
16
+ while aliased in self.names:
17
+ i += 1
18
+ aliased = f"{name}{i}"
19
+ self.names.add(aliased)
20
+ return aliased
pulse/component.py ADDED
@@ -0,0 +1,237 @@
1
+ """Component definition and VDOM node types for Pulse.
2
+
3
+ This module provides the core component abstraction for building Pulse UIs,
4
+ including the `@component` decorator and the `Component` class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from inspect import Parameter, signature
11
+ from types import CodeType
12
+ from typing import Any, Generic, ParamSpec, TypeVar, overload, override
13
+
14
+ from pulse.code_analysis import is_stub_function
15
+ from pulse.hooks.init import rewrite_init_blocks
16
+ from pulse.transpiler.nodes import (
17
+ Children,
18
+ Node,
19
+ Primitive,
20
+ PulseNode,
21
+ flatten_children,
22
+ )
23
+ from pulse.transpiler.nodes import Element as Element
24
+ from pulse.transpiler.vdom import VDOMNode
25
+
26
+ P = ParamSpec("P")
27
+ _T = TypeVar("_T")
28
+
29
+ _COMPONENT_CODES: set[CodeType] = set()
30
+
31
+
32
+ def is_component_code(code: CodeType) -> bool:
33
+ return code in _COMPONENT_CODES
34
+
35
+
36
+ class Component(Generic[P]):
37
+ """A callable wrapper that turns a function into a Pulse component.
38
+
39
+ Component instances are created by the `@component` decorator. When called,
40
+ they return a `PulseNode` that represents the component in the virtual DOM.
41
+
42
+ Attributes:
43
+ name: Display name of the component (defaults to function name).
44
+ fn: The underlying render function (lazily initialized for stubs).
45
+
46
+ Example:
47
+
48
+ ```python
49
+ @ps.component
50
+ def Card(title: str):
51
+ return ps.div(ps.h3(title))
52
+
53
+ Card(title="Hello") # Returns a PulseNode
54
+ Card(title="Hello", key="card-1") # With reconciliation key
55
+ ```
56
+ """
57
+
58
+ _raw_fn: Callable[P, Any]
59
+ _fn: Callable[P, Any] | None
60
+ name: str
61
+ _takes_children: bool | None
62
+
63
+ def __init__(self, fn: Callable[P, Any], name: str | None = None) -> None:
64
+ """Initialize a Component.
65
+
66
+ Args:
67
+ fn: The function to wrap as a component.
68
+ name: Custom display name. Defaults to the function's `__name__`.
69
+ """
70
+ self._raw_fn = fn
71
+ self.name = name or _infer_component_name(fn)
72
+ # Only lazy-init for stubs (avoid heavy work for JS module bindings)
73
+ # Real components need immediate rewrite for early error detection
74
+ if is_stub_function(fn):
75
+ self._fn = None
76
+ self._takes_children = None
77
+ else:
78
+ self._fn = rewrite_init_blocks(fn)
79
+ self._takes_children = _takes_children(fn)
80
+ _COMPONENT_CODES.add(self._fn.__code__)
81
+
82
+ @property
83
+ def fn(self) -> Callable[P, Any]:
84
+ """The render function (lazily initialized for stub functions)."""
85
+ if self._fn is None:
86
+ self._fn = rewrite_init_blocks(self._raw_fn)
87
+ self._takes_children = _takes_children(self._raw_fn)
88
+ _COMPONENT_CODES.add(self._fn.__code__)
89
+ return self._fn
90
+
91
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> PulseNode:
92
+ """Invoke the component to create a PulseNode.
93
+
94
+ Args:
95
+ *args: Positional arguments passed to the component function.
96
+ **kwargs: Keyword arguments passed to the component function.
97
+ The special `key` kwarg is used for reconciliation.
98
+
99
+ Returns:
100
+ A PulseNode representing this component invocation in the VDOM.
101
+
102
+ Raises:
103
+ ValueError: If `key` is provided but is not a string.
104
+ """
105
+ key = kwargs.get("key")
106
+ if key is not None and not isinstance(key, str):
107
+ raise ValueError("key must be a string or None")
108
+
109
+ # Access self.fn to trigger lazy init (sets _takes_children)
110
+ _ = self.fn
111
+ if self._takes_children is True and args:
112
+ flattened = flatten_children(
113
+ args, # pyright: ignore[reportArgumentType]
114
+ parent_name=f"<{self.name}>",
115
+ warn_stacklevel=4,
116
+ )
117
+ args = tuple(flattened) # pyright: ignore[reportAssignmentType]
118
+
119
+ return PulseNode(fn=self.fn, args=args, kwargs=kwargs, key=key, name=self.name)
120
+
121
+ @override
122
+ def __repr__(self) -> str:
123
+ return f"Component(name={self.name!r}, fn={_callable_qualname(self._raw_fn)!r})"
124
+
125
+ @override
126
+ def __str__(self) -> str:
127
+ return self.name
128
+
129
+
130
+ @overload
131
+ def component(fn: Callable[P, Any]) -> Component[P]: ...
132
+
133
+
134
+ @overload
135
+ def component(
136
+ fn: None = None, *, name: str | None = None
137
+ ) -> Callable[[Callable[P, Any]], Component[P]]: ...
138
+
139
+
140
+ # The explicit return type is necessary for the type checker to be happy
141
+ def component(
142
+ fn: Callable[P, Any] | None = None, *, name: str | None = None
143
+ ) -> Component[P] | Callable[[Callable[P, Any]], Component[P]]:
144
+ """Decorator that creates a Pulse component from a function.
145
+
146
+ Can be used with or without parentheses. The decorated function becomes
147
+ callable and returns a `PulseNode` when invoked.
148
+
149
+ Args:
150
+ fn: Function to wrap as a component. When used as `@component` without
151
+ parentheses, this is the decorated function.
152
+ name: Custom component name for debugging/dev tools. Defaults to the
153
+ function's `__name__`.
154
+
155
+ Returns:
156
+ A `Component` instance if `fn` is provided, otherwise a decorator.
157
+
158
+ Example:
159
+
160
+ Basic usage:
161
+
162
+ ```python
163
+ @ps.component
164
+ def Card(title: str):
165
+ return ps.div(ps.h3(title))
166
+ ```
167
+
168
+ With custom name:
169
+
170
+ ```python
171
+ @ps.component(name="MyCard")
172
+ def card_impl(title: str):
173
+ return ps.div(ps.h3(title))
174
+ ```
175
+
176
+ With children (use `*children` parameter):
177
+
178
+ ```python
179
+ @ps.component
180
+ def Container(*children):
181
+ return ps.div(*children, className="container")
182
+
183
+ # Children can be passed via subscript syntax:
184
+ Container()[
185
+ Card(title="First"),
186
+ Card(title="Second"),
187
+ ]
188
+ ```
189
+ """
190
+
191
+ def decorator(fn: Callable[P, Any]) -> Component[P]:
192
+ return Component(fn, name)
193
+
194
+ if fn is not None:
195
+ return decorator(fn)
196
+ return decorator
197
+
198
+
199
+ def _takes_children(fn: Callable[..., Any]) -> bool:
200
+ try:
201
+ sig = signature(fn)
202
+ except (ValueError, TypeError):
203
+ return False
204
+ for p in sig.parameters.values():
205
+ if p.kind == Parameter.VAR_POSITIONAL and p.name == "children":
206
+ return True
207
+ return False
208
+
209
+
210
+ # ----------------------------------------------------------------------------
211
+ # Formatting helpers
212
+ # ----------------------------------------------------------------------------
213
+
214
+
215
+ def _infer_component_name(fn: Callable[..., Any]) -> str:
216
+ name = getattr(fn, "__name__", None)
217
+ if name:
218
+ return name
219
+ return "Component"
220
+
221
+
222
+ def _callable_qualname(fn: Callable[..., Any]) -> str:
223
+ mod = getattr(fn, "__module__", "<unknown>")
224
+ qname = getattr(fn, "__qualname__", getattr(fn, "__name__", "<callable>"))
225
+ return f"{mod}.{qname}"
226
+
227
+
228
+ __all__ = [
229
+ "Node",
230
+ "Children",
231
+ "Component",
232
+ "Element",
233
+ "Primitive",
234
+ "VDOMNode",
235
+ "component",
236
+ "is_component_code",
237
+ ]
File without changes