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.
- 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 -999
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/serializer.py +11 -1
- 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.50.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 -667
- pulse_framework-0.1.50.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.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/react_component.py
CHANGED
|
@@ -1,1001 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
# React Component Integration
|
|
3
|
-
# ============================================================================
|
|
1
|
+
"""Thin wrappers for transpiler react_component integration."""
|
|
4
2
|
|
|
5
|
-
import
|
|
6
|
-
import typing
|
|
7
|
-
from collections import defaultdict
|
|
8
|
-
from collections.abc import Callable
|
|
9
|
-
from contextvars import ContextVar
|
|
10
|
-
from functools import cache
|
|
11
|
-
from inspect import Parameter
|
|
12
|
-
from types import UnionType
|
|
13
|
-
from typing import (
|
|
14
|
-
Annotated,
|
|
15
|
-
Any,
|
|
16
|
-
ClassVar,
|
|
17
|
-
Generic,
|
|
18
|
-
Literal,
|
|
19
|
-
ParamSpec,
|
|
20
|
-
TypeVar,
|
|
21
|
-
Unpack,
|
|
22
|
-
cast,
|
|
23
|
-
get_args,
|
|
24
|
-
get_origin,
|
|
25
|
-
override,
|
|
26
|
-
)
|
|
3
|
+
from pulse.transpiler.react_component import default_signature, react_component
|
|
27
4
|
|
|
28
|
-
|
|
29
|
-
from pulse.reactive_extensions import unwrap
|
|
30
|
-
from pulse.transpiler.errors import JSCompilationError
|
|
31
|
-
from pulse.transpiler.imports import Import
|
|
32
|
-
from pulse.transpiler.nodes import (
|
|
33
|
-
JSExpr,
|
|
34
|
-
JSMember,
|
|
35
|
-
JSSpread,
|
|
36
|
-
JSXElement,
|
|
37
|
-
JSXProp,
|
|
38
|
-
JSXSpreadProp,
|
|
39
|
-
)
|
|
40
|
-
from pulse.vdom import Child, Element, Node
|
|
41
|
-
|
|
42
|
-
T = TypeVar("T")
|
|
43
|
-
P = ParamSpec("P")
|
|
44
|
-
DEFAULT: Any = Sentinel("DEFAULT")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# ----------------------------------------------------------------------------
|
|
48
|
-
# Detection for stringified annotations (from __future__ import annotations)
|
|
49
|
-
# ----------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _function_has_string_annotations(fn: Callable[..., Any]) -> bool:
|
|
53
|
-
"""Return True if any function annotations are strings.
|
|
54
|
-
|
|
55
|
-
This happens when the defining module uses `from __future__ import annotations`.
|
|
56
|
-
In that case, resolving types at runtime is fragile; we skip PropSpec building.
|
|
57
|
-
"""
|
|
58
|
-
try:
|
|
59
|
-
anns = getattr(fn, "__annotations__", {}) or {}
|
|
60
|
-
return any(isinstance(v, str) for v in anns.values())
|
|
61
|
-
except Exception:
|
|
62
|
-
return False
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _is_any_annotation(annotation: object) -> bool:
|
|
66
|
-
"""Return True when an annotation is effectively `typing.Any`."""
|
|
67
|
-
try:
|
|
68
|
-
return annotation is Any or annotation == Any
|
|
69
|
-
except TypeError:
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class Prop(Generic[T]):
|
|
74
|
-
default: T | None
|
|
75
|
-
required: bool
|
|
76
|
-
default_factory: Callable[[], T] | None
|
|
77
|
-
serialize: Callable[[T], Any] | None
|
|
78
|
-
map_to: str | None
|
|
79
|
-
typ_: type | tuple[type, ...] | None
|
|
80
|
-
|
|
81
|
-
def __init__(
|
|
82
|
-
self,
|
|
83
|
-
default: T | None = DEFAULT,
|
|
84
|
-
# Will be set by all the conventional ways of defining PropSpec
|
|
85
|
-
required: bool = DEFAULT, # type: ignore
|
|
86
|
-
default_factory: Callable[[], T] | None = None,
|
|
87
|
-
serialize: Callable[[T], Any] | None = None,
|
|
88
|
-
map_to: str | None = None,
|
|
89
|
-
typ_: type | tuple[type, ...] | None = None,
|
|
90
|
-
) -> None:
|
|
91
|
-
self.default = default
|
|
92
|
-
self.required = required
|
|
93
|
-
self.default_factory = default_factory
|
|
94
|
-
self.serialize = serialize
|
|
95
|
-
self.map_to = map_to
|
|
96
|
-
self.typ_ = typ_
|
|
97
|
-
|
|
98
|
-
@override
|
|
99
|
-
def __repr__(self) -> str:
|
|
100
|
-
def _callable_name(fn: Callable[..., Any] | None) -> str:
|
|
101
|
-
if fn is None:
|
|
102
|
-
return "None"
|
|
103
|
-
return getattr(fn, "__name__", fn.__class__.__name__)
|
|
104
|
-
|
|
105
|
-
parts: list[str] = []
|
|
106
|
-
if self.typ_:
|
|
107
|
-
parts.append(f"type={_format_runtime_type(self.typ_)}")
|
|
108
|
-
if self.required is not DEFAULT:
|
|
109
|
-
parts.append(f"required={self.required}")
|
|
110
|
-
if self.default is not None:
|
|
111
|
-
parts.append(f"default={self.default!r}")
|
|
112
|
-
if self.default_factory is not None:
|
|
113
|
-
parts.append(f"default_factory={_callable_name(self.default_factory)}")
|
|
114
|
-
if self.serialize is not None:
|
|
115
|
-
parts.append(f"serialize={_callable_name(self.serialize)}")
|
|
116
|
-
if self.map_to is not None:
|
|
117
|
-
parts.append(f"map_to={self.map_to!r}")
|
|
118
|
-
return f"Prop({', '.join(parts)})"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def prop(
|
|
122
|
-
default: T = DEFAULT,
|
|
123
|
-
*,
|
|
124
|
-
default_factory: Callable[[], T] | None = None,
|
|
125
|
-
serialize: Callable[[T], Any] | None = None,
|
|
126
|
-
map_to: str | None = None,
|
|
127
|
-
) -> T:
|
|
128
|
-
"""
|
|
129
|
-
Convenience constructor for Prop to be used inside TypedDict defaults.
|
|
130
|
-
"""
|
|
131
|
-
return Prop( # pyright: ignore[reportReturnType]
|
|
132
|
-
default=default,
|
|
133
|
-
default_factory=default_factory,
|
|
134
|
-
serialize=serialize,
|
|
135
|
-
map_to=map_to,
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class PropSpec:
|
|
140
|
-
allow_unspecified: bool
|
|
141
|
-
|
|
142
|
-
def __init__(
|
|
143
|
-
self,
|
|
144
|
-
required: dict[str, Prop[Any]],
|
|
145
|
-
optional: dict[str, Prop[Any]],
|
|
146
|
-
allow_unspecified: bool = False,
|
|
147
|
-
) -> None:
|
|
148
|
-
if "key" in required or "key" in optional:
|
|
149
|
-
raise ValueError(
|
|
150
|
-
"'key' is a reserved prop, please use another name (like 'id', 'label', or even 'key_')"
|
|
151
|
-
)
|
|
152
|
-
self.required: dict[str, Prop[Any]] = required
|
|
153
|
-
self.optional: dict[str, Prop[Any]] = optional
|
|
154
|
-
self.allow_unspecified = allow_unspecified
|
|
155
|
-
# Precompute optional keys that provide defaults so we can apply them
|
|
156
|
-
# without scanning all optional props on every apply call.
|
|
157
|
-
self._optional_with_defaults: tuple[str, ...] = tuple(
|
|
158
|
-
k
|
|
159
|
-
for k, p in optional.items()
|
|
160
|
-
if (p.default is not DEFAULT) or (p.default_factory is not None)
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
@override
|
|
164
|
-
def __repr__(self) -> str:
|
|
165
|
-
keys_list = list(self.keys())
|
|
166
|
-
keys_preview = ", ".join(keys_list[:5])
|
|
167
|
-
if len(keys_list) > 5:
|
|
168
|
-
keys_preview += ", ..."
|
|
169
|
-
return f"Props(keys=[{keys_preview}])"
|
|
170
|
-
|
|
171
|
-
def as_dict(self) -> dict[str, Prop[Any]]:
|
|
172
|
-
"""Return a merged dict of required and optional props."""
|
|
173
|
-
return self.required | self.optional
|
|
174
|
-
|
|
175
|
-
def __getitem__(self, key: str) -> Prop[Any]:
|
|
176
|
-
if key in self.required:
|
|
177
|
-
return self.required[key]
|
|
178
|
-
if key in self.optional:
|
|
179
|
-
return self.optional[key]
|
|
180
|
-
raise KeyError(key)
|
|
181
|
-
|
|
182
|
-
def keys(self):
|
|
183
|
-
return self.required.keys() | self.optional.keys()
|
|
184
|
-
|
|
185
|
-
def merge(self, other: "PropSpec"):
|
|
186
|
-
conflicts = self.keys() & other.keys()
|
|
187
|
-
if conflicts:
|
|
188
|
-
conflict_list = ", ".join(sorted(conflicts))
|
|
189
|
-
raise ValueError(
|
|
190
|
-
f"Conflicting prop definitions for: {conflict_list}. Define each prop only once across explicit params and Unpack[TypedDict]",
|
|
191
|
-
)
|
|
192
|
-
merged_required = self.required | other.required
|
|
193
|
-
merged_optional = self.optional | other.optional
|
|
194
|
-
return PropSpec(
|
|
195
|
-
required=merged_required,
|
|
196
|
-
optional=merged_optional,
|
|
197
|
-
allow_unspecified=self.allow_unspecified or other.allow_unspecified,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
def apply(self, comp_tag: str, props: dict[str, Any]):
|
|
201
|
-
result: dict[str, Any] = {}
|
|
202
|
-
known_keys = self.keys()
|
|
203
|
-
|
|
204
|
-
# Unknown keys handling (exclude 'key' as it's special)
|
|
205
|
-
unknown_keys = props.keys() - known_keys - {"key"}
|
|
206
|
-
if not self.allow_unspecified and unknown_keys:
|
|
207
|
-
bad = ", ".join(repr(k) for k in sorted(unknown_keys))
|
|
208
|
-
raise ValueError(f"Unexpected prop(s) for component '{comp_tag}': {bad}")
|
|
209
|
-
if self.allow_unspecified:
|
|
210
|
-
for k in unknown_keys:
|
|
211
|
-
v = props[k]
|
|
212
|
-
if v is not DEFAULT:
|
|
213
|
-
result[k] = v
|
|
214
|
-
|
|
215
|
-
missing_props: list[str] = []
|
|
216
|
-
overlaps: dict[str, list[str]] = defaultdict(list)
|
|
217
|
-
|
|
218
|
-
# 1) Apply required props (including their defaults if provided)
|
|
219
|
-
for py_key, prop in self.required.items():
|
|
220
|
-
p = prop if isinstance(prop, Prop) else Prop(typ_=prop)
|
|
221
|
-
if py_key in props:
|
|
222
|
-
value = props[py_key]
|
|
223
|
-
elif p.default_factory is not None:
|
|
224
|
-
value = p.default_factory()
|
|
225
|
-
else:
|
|
226
|
-
value = p.default
|
|
227
|
-
|
|
228
|
-
if value is DEFAULT:
|
|
229
|
-
missing_props.append(py_key)
|
|
230
|
-
continue
|
|
231
|
-
|
|
232
|
-
if p.serialize is not None:
|
|
233
|
-
value = p.serialize(value)
|
|
234
|
-
|
|
235
|
-
js_key = p.map_to or py_key
|
|
236
|
-
if js_key in result:
|
|
237
|
-
overlaps[js_key].append(py_key)
|
|
238
|
-
continue
|
|
239
|
-
result[js_key] = value
|
|
240
|
-
|
|
241
|
-
# 2) Apply provided optional props (only those present)
|
|
242
|
-
provided_known_optional = props.keys() & self.optional.keys()
|
|
243
|
-
for py_key in provided_known_optional:
|
|
244
|
-
prop = self.optional[py_key]
|
|
245
|
-
p = prop if isinstance(prop, Prop) else Prop(typ_=prop)
|
|
246
|
-
value = props[py_key]
|
|
247
|
-
|
|
248
|
-
if value is DEFAULT:
|
|
249
|
-
# Omit optional prop when DEFAULT sentinel is used
|
|
250
|
-
continue
|
|
251
|
-
|
|
252
|
-
if p.serialize is not None:
|
|
253
|
-
value = p.serialize(value)
|
|
254
|
-
|
|
255
|
-
js_key = p.map_to or py_key
|
|
256
|
-
if js_key in result:
|
|
257
|
-
overlaps[js_key].append(py_key)
|
|
258
|
-
continue
|
|
259
|
-
result[js_key] = value
|
|
260
|
-
|
|
261
|
-
# 3) Apply optional props that have defaults and were not provided
|
|
262
|
-
for py_key in self._optional_with_defaults:
|
|
263
|
-
if py_key in props:
|
|
264
|
-
continue
|
|
265
|
-
prop = self.optional[py_key]
|
|
266
|
-
p = prop if isinstance(prop, Prop) else Prop(typ_=prop)
|
|
267
|
-
if p.default_factory is not None:
|
|
268
|
-
value = p.default_factory()
|
|
269
|
-
else:
|
|
270
|
-
value = p.default
|
|
271
|
-
|
|
272
|
-
if value is DEFAULT:
|
|
273
|
-
continue
|
|
274
|
-
|
|
275
|
-
if p.serialize is not None:
|
|
276
|
-
value = p.serialize(value)
|
|
277
|
-
|
|
278
|
-
js_key = p.map_to or py_key
|
|
279
|
-
if js_key in result:
|
|
280
|
-
overlaps[js_key].append(py_key)
|
|
281
|
-
continue
|
|
282
|
-
result[js_key] = value
|
|
283
|
-
|
|
284
|
-
if missing_props or overlaps:
|
|
285
|
-
errors: list[str] = []
|
|
286
|
-
if missing_props:
|
|
287
|
-
errors.append(f"Missing required props: {', '.join(missing_props)}")
|
|
288
|
-
if overlaps:
|
|
289
|
-
for js_key, py_keys in overlaps.items():
|
|
290
|
-
errors.append(
|
|
291
|
-
f"Multiple props map to '{js_key}': {', '.join(py_keys)}"
|
|
292
|
-
)
|
|
293
|
-
raise ValueError(
|
|
294
|
-
f"Invalid props for component '{comp_tag}': {'; '.join(errors)}"
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
return result or None
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def default_signature(
|
|
301
|
-
*children: Child, key: str | None = None, **props: Any
|
|
302
|
-
) -> Element: ...
|
|
303
|
-
def default_fn_signature_without_children(
|
|
304
|
-
key: str | None = None, **props: Any
|
|
305
|
-
) -> Element: ...
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
# ----------------------------------------------------------------------------
|
|
309
|
-
# JSX transpilation helpers
|
|
310
|
-
# ----------------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def _build_jsx_props(kwargs: dict[str, Any]) -> list[JSXProp | JSXSpreadProp]:
|
|
314
|
-
"""Build JSX props list from kwargs dict.
|
|
315
|
-
|
|
316
|
-
Kwargs maps:
|
|
317
|
-
- "propName" -> value for named props
|
|
318
|
-
- "__spread_N" -> JSSpread(expr) for spread props
|
|
319
|
-
"""
|
|
320
|
-
props: list[JSXProp | JSXSpreadProp] = []
|
|
321
|
-
for key, value in kwargs.items():
|
|
322
|
-
if isinstance(value, JSSpread):
|
|
323
|
-
props.append(JSXSpreadProp(value.expr))
|
|
324
|
-
else:
|
|
325
|
-
props.append(JSXProp(key, JSExpr.of(value)))
|
|
326
|
-
return props
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def _flatten_children(items: list[Any], out: list[JSExpr | JSXElement | str]) -> None:
|
|
330
|
-
"""Flatten arrays and handle spreads in children list."""
|
|
331
|
-
from pulse.transpiler.nodes import JSArray, JSString
|
|
332
|
-
|
|
333
|
-
for it in items:
|
|
334
|
-
# Convert raw values first
|
|
335
|
-
it = JSExpr.of(it) if not isinstance(it, JSExpr) else it
|
|
336
|
-
if isinstance(it, JSArray):
|
|
337
|
-
_flatten_children(list(it.elements), out)
|
|
338
|
-
elif isinstance(it, JSSpread):
|
|
339
|
-
out.append(it.expr)
|
|
340
|
-
elif isinstance(it, JSString):
|
|
341
|
-
out.append(it.value)
|
|
342
|
-
else:
|
|
343
|
-
out.append(it)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
class ReactComponentCallExpr(JSExpr):
|
|
347
|
-
"""JSX call expression for a ReactComponent.
|
|
348
|
-
|
|
349
|
-
Created when a ReactComponent is called with props. Supports subscripting
|
|
350
|
-
to add children, producing the final JSXElement.
|
|
351
|
-
"""
|
|
352
|
-
|
|
353
|
-
is_jsx: ClassVar[bool] = True
|
|
354
|
-
component: "ReactComponent[...]"
|
|
355
|
-
props: tuple[JSXProp | JSXSpreadProp, ...]
|
|
356
|
-
children: tuple[str | JSExpr | JSXElement, ...]
|
|
357
|
-
|
|
358
|
-
def __init__(
|
|
359
|
-
self,
|
|
360
|
-
component: "ReactComponent[...]",
|
|
361
|
-
props: tuple[JSXProp | JSXSpreadProp, ...],
|
|
362
|
-
children: tuple[str | JSExpr | JSXElement, ...],
|
|
363
|
-
) -> None:
|
|
364
|
-
self.component = component
|
|
365
|
-
self.props = props
|
|
366
|
-
self.children = children
|
|
367
|
-
|
|
368
|
-
@override
|
|
369
|
-
def emit(self) -> str:
|
|
370
|
-
return JSXElement(self.component, self.props, self.children).emit()
|
|
371
|
-
|
|
372
|
-
@override
|
|
373
|
-
def emit_subscript(self, indices: list[Any]) -> JSExpr:
|
|
374
|
-
"""Handle Component(props...)[children] -> JSXElement."""
|
|
375
|
-
extra_children: list[JSExpr | JSXElement | str] = []
|
|
376
|
-
_flatten_children(indices, extra_children)
|
|
377
|
-
all_children = list(self.children) + extra_children
|
|
378
|
-
return JSXElement(self.component, self.props, all_children)
|
|
379
|
-
|
|
380
|
-
@override
|
|
381
|
-
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
382
|
-
"""Calling an already-called component is an error."""
|
|
383
|
-
raise JSCompilationError(
|
|
384
|
-
f"Cannot call <{self.component.name}> - already called. "
|
|
385
|
-
+ "Use subscript for children: Component(props...)[children]"
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
class ReactComponent(JSExpr, Generic[P]):
|
|
390
|
-
"""
|
|
391
|
-
A React component that can be used within the UI tree.
|
|
392
|
-
Returns a function that creates mount point UITreeNode instances.
|
|
393
|
-
|
|
394
|
-
Args:
|
|
395
|
-
name: Name of the component (or "default" for default export)
|
|
396
|
-
src: Module path to import the component from
|
|
397
|
-
is_default: True if this is a default export, else named export
|
|
398
|
-
prop: Optional property name to access the component from the imported object
|
|
399
|
-
lazy: Whether to lazy load the component
|
|
400
|
-
version: Optional npm semver constraint for this component's package
|
|
401
|
-
prop_spec: Optional PropSpec for the component
|
|
402
|
-
fn_signature: Function signature to parse for props
|
|
403
|
-
extra_imports: Additional imports to include (CSS files, etc.)
|
|
404
|
-
|
|
405
|
-
Returns:
|
|
406
|
-
A function that creates Node instances with mount point tags
|
|
407
|
-
"""
|
|
408
|
-
|
|
409
|
-
import_: Import
|
|
410
|
-
props_spec: PropSpec
|
|
411
|
-
fn_signature: Callable[P, Element]
|
|
412
|
-
lazy: bool
|
|
413
|
-
_prop: str | None # Property access like AppShell.Header
|
|
414
|
-
|
|
415
|
-
def __init__(
|
|
416
|
-
self,
|
|
417
|
-
name: str,
|
|
418
|
-
src: str,
|
|
419
|
-
*,
|
|
420
|
-
is_default: bool = False,
|
|
421
|
-
prop: str | None = None,
|
|
422
|
-
lazy: bool = False,
|
|
423
|
-
version: str | None = None,
|
|
424
|
-
prop_spec: PropSpec | None = None,
|
|
425
|
-
fn_signature: Callable[P, Element] = default_signature,
|
|
426
|
-
extra_imports: tuple[Import, ...] | list[Import] | None = None,
|
|
427
|
-
):
|
|
428
|
-
# Create the Import directly (prop is stored separately on ReactComponent)
|
|
429
|
-
if is_default:
|
|
430
|
-
self.import_ = Import.default(name, src)
|
|
431
|
-
else:
|
|
432
|
-
self.import_ = Import.named(name, src)
|
|
433
|
-
self._prop = prop
|
|
434
|
-
|
|
435
|
-
# Build props_spec from fn_signature if provided and props not provided
|
|
436
|
-
if prop_spec:
|
|
437
|
-
self.props_spec = prop_spec
|
|
438
|
-
elif fn_signature not in (
|
|
439
|
-
default_signature,
|
|
440
|
-
default_fn_signature_without_children,
|
|
441
|
-
):
|
|
442
|
-
self.props_spec = parse_fn_signature(fn_signature)
|
|
443
|
-
else:
|
|
444
|
-
self.props_spec = PropSpec({}, {}, allow_unspecified=True)
|
|
445
|
-
|
|
446
|
-
self.fn_signature = fn_signature
|
|
447
|
-
self.lazy = lazy
|
|
448
|
-
# Optional npm semver constraint for this component's package
|
|
449
|
-
self.version: str | None = version
|
|
450
|
-
# Additional imports to include in route where this component is used
|
|
451
|
-
self.extra_imports: list[Import] = list(extra_imports or [])
|
|
452
|
-
COMPONENT_REGISTRY.get().add(self)
|
|
453
|
-
|
|
454
|
-
@override
|
|
455
|
-
def emit(self) -> str:
|
|
456
|
-
if self.prop:
|
|
457
|
-
return JSMember(self.import_, self.prop).emit()
|
|
458
|
-
return self.import_.emit()
|
|
459
|
-
|
|
460
|
-
@override
|
|
461
|
-
def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
|
|
462
|
-
"""Handle Component(props...) -> ReactComponentCallExpr."""
|
|
463
|
-
props_list = _build_jsx_props(kwargs)
|
|
464
|
-
children_list: list[JSExpr | JSXElement | str] = []
|
|
465
|
-
_flatten_children(args, children_list)
|
|
466
|
-
return ReactComponentCallExpr(self, tuple(props_list), tuple(children_list))
|
|
467
|
-
|
|
468
|
-
@override
|
|
469
|
-
def emit_subscript(self, indices: list[Any]) -> JSExpr:
|
|
470
|
-
"""Direct subscript on ReactComponent is not allowed.
|
|
471
|
-
|
|
472
|
-
Use Component(props...)[children] instead of Component[children].
|
|
473
|
-
"""
|
|
474
|
-
raise JSCompilationError(
|
|
475
|
-
f"Cannot subscript ReactComponent '{self.name}' directly. "
|
|
476
|
-
+ "Use Component(props...)[children] or Component()[children] instead."
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
@property
|
|
480
|
-
def name(self) -> str:
|
|
481
|
-
return self.import_.name
|
|
482
|
-
|
|
483
|
-
@property
|
|
484
|
-
def src(self) -> str:
|
|
485
|
-
return self.import_.src
|
|
486
|
-
|
|
487
|
-
@property
|
|
488
|
-
def is_default(self) -> bool:
|
|
489
|
-
return self.import_.is_default
|
|
490
|
-
|
|
491
|
-
@property
|
|
492
|
-
def prop(self) -> str | None:
|
|
493
|
-
return self._prop
|
|
494
|
-
|
|
495
|
-
@property
|
|
496
|
-
def expr(self) -> str:
|
|
497
|
-
"""Expression for the component in the registry and VDOM tags.
|
|
498
|
-
|
|
499
|
-
Uses the import's js_name (with unique ID suffix) to match the
|
|
500
|
-
unified registry on the client side.
|
|
501
|
-
"""
|
|
502
|
-
if self.prop:
|
|
503
|
-
return f"{self.import_.js_name}.{self.prop}"
|
|
504
|
-
return self.import_.js_name
|
|
505
|
-
|
|
506
|
-
@override
|
|
507
|
-
def __repr__(self) -> str:
|
|
508
|
-
default_part = ", default=True" if self.is_default else ""
|
|
509
|
-
prop_part = f", prop='{self.prop}'" if self.prop else ""
|
|
510
|
-
lazy_part = ", lazy=True" if self.lazy else ""
|
|
511
|
-
props_part = f", props_spec={self.props_spec!r}"
|
|
512
|
-
return f"ReactComponent(name='{self.name}', src='{self.src}'{prop_part}{default_part}{lazy_part}{props_part})"
|
|
513
|
-
|
|
514
|
-
@override
|
|
515
|
-
def __call__(self, *children: P.args, **props: P.kwargs) -> Node: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
516
|
-
key = props.get("key")
|
|
517
|
-
if key is not None and not isinstance(key, str):
|
|
518
|
-
raise ValueError("key must be a string or None")
|
|
519
|
-
# Apply optional props specification: fill defaults, enforce required,
|
|
520
|
-
# run serializers, and remap keys.
|
|
521
|
-
real_props = self.props_spec.apply(self.name, props)
|
|
522
|
-
if real_props:
|
|
523
|
-
real_props = {key: unwrap(value) for key, value in real_props.items()}
|
|
524
|
-
|
|
525
|
-
return Node(
|
|
526
|
-
tag=f"$${self.expr}",
|
|
527
|
-
key=key,
|
|
528
|
-
props=real_props,
|
|
529
|
-
children=cast(tuple[Child], children),
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
def parse_fn_signature(fn: Callable[..., Any]) -> PropSpec:
|
|
534
|
-
"""Parse a function signature into a Props spec using a single pass.
|
|
535
|
-
|
|
536
|
-
Rules:
|
|
537
|
-
- May accept var-positional children `*children` (if annotated, must be Child)
|
|
538
|
-
- Must define `key: Optional[str] = None` (keyword-accepting)
|
|
539
|
-
- Other props may be explicit keyword params and/or via **props: Unpack[TypedDict]
|
|
540
|
-
- A prop may not be specified both explicitly and in the Unpack
|
|
541
|
-
- Annotated[..., Prop(...)] on parameters is disallowed (use default Prop instead)
|
|
542
|
-
"""
|
|
543
|
-
|
|
544
|
-
# If annotations are stringified, skip building and allow unspecified props.
|
|
545
|
-
if _function_has_string_annotations(fn):
|
|
546
|
-
return PropSpec({}, {}, allow_unspecified=True)
|
|
547
|
-
|
|
548
|
-
sig = inspect.signature(fn)
|
|
549
|
-
params = list(sig.parameters.values())
|
|
550
|
-
|
|
551
|
-
explicit_required: dict[str, Prop[Any]] = {}
|
|
552
|
-
explicit_optional: dict[str, Prop[Any]] = {}
|
|
553
|
-
explicit_order: list[str] = []
|
|
554
|
-
explicit_spec: PropSpec
|
|
555
|
-
unpack_spec: PropSpec
|
|
556
|
-
|
|
557
|
-
var_positional: Parameter | None = None
|
|
558
|
-
var_kw: Parameter | None = None
|
|
559
|
-
key: Parameter | None = None
|
|
560
|
-
|
|
561
|
-
# One pass: collect structure and build explicit spec as we go
|
|
562
|
-
for p in params:
|
|
563
|
-
# Disallow positional-only parameters
|
|
564
|
-
if p.kind is Parameter.POSITIONAL_ONLY:
|
|
565
|
-
raise ValueError(
|
|
566
|
-
"Function must not declare positional-only parameters besides *children",
|
|
567
|
-
)
|
|
568
|
-
|
|
569
|
-
if p.kind is Parameter.VAR_POSITIONAL:
|
|
570
|
-
var_positional = p
|
|
571
|
-
continue
|
|
572
|
-
|
|
573
|
-
if p.kind is Parameter.VAR_KEYWORD:
|
|
574
|
-
var_kw = p
|
|
575
|
-
continue
|
|
576
|
-
|
|
577
|
-
if p.name == "key":
|
|
578
|
-
key = p
|
|
579
|
-
continue
|
|
580
|
-
|
|
581
|
-
# For regular params, forbid additional required positionals
|
|
582
|
-
if p.kind is Parameter.POSITIONAL_OR_KEYWORD and p.default is Parameter.empty:
|
|
583
|
-
raise ValueError(
|
|
584
|
-
"Function signature must not declare additional required positional parameters; only *children is allowed for positionals",
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
if p.kind not in (
|
|
588
|
-
Parameter.POSITIONAL_OR_KEYWORD,
|
|
589
|
-
Parameter.KEYWORD_ONLY,
|
|
590
|
-
):
|
|
591
|
-
continue
|
|
592
|
-
|
|
593
|
-
# Build explicit spec (skip 'key' handled above)
|
|
594
|
-
annotation = p.annotation if p.annotation is not Parameter.empty else Any
|
|
595
|
-
origin = get_origin(annotation)
|
|
596
|
-
annotation_args = get_args(annotation)
|
|
597
|
-
|
|
598
|
-
# Disallow Annotated[..., Prop(...)] on parameters
|
|
599
|
-
if (
|
|
600
|
-
origin is Annotated
|
|
601
|
-
and annotation_args
|
|
602
|
-
and any(isinstance(m, Prop) for m in annotation_args[1:])
|
|
603
|
-
):
|
|
604
|
-
raise TypeError(
|
|
605
|
-
"Annotated[..., ps.prop(...)] is not allowed on function parameters; use a default `= ps.prop(...)` or a TypedDict",
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
runtime_type = _annotation_to_runtime_type(
|
|
609
|
-
annotation_args[0]
|
|
610
|
-
if origin is Annotated and annotation_args
|
|
611
|
-
else annotation
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
if isinstance(p.default, Prop):
|
|
615
|
-
prop = p.default
|
|
616
|
-
if prop.typ_ is None:
|
|
617
|
-
prop.typ_ = runtime_type
|
|
618
|
-
# Has default via Prop -> optional
|
|
619
|
-
explicit_optional[p.name] = prop
|
|
620
|
-
explicit_order.append(p.name)
|
|
621
|
-
elif p.default is not Parameter.empty:
|
|
622
|
-
prop = Prop(default=p.default, required=False, typ_=runtime_type)
|
|
623
|
-
explicit_optional[p.name] = prop
|
|
624
|
-
explicit_order.append(p.name)
|
|
625
|
-
else:
|
|
626
|
-
prop = Prop(typ_=runtime_type)
|
|
627
|
-
# No default -> required
|
|
628
|
-
prop.required = True
|
|
629
|
-
explicit_required[p.name] = prop
|
|
630
|
-
explicit_order.append(p.name)
|
|
631
|
-
|
|
632
|
-
explicit_spec = PropSpec(
|
|
633
|
-
explicit_required,
|
|
634
|
-
explicit_optional,
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
# Validate *children annotation if present
|
|
638
|
-
if var_positional is not None:
|
|
639
|
-
annotation = var_positional.annotation
|
|
640
|
-
if (
|
|
641
|
-
annotation is not Parameter.empty
|
|
642
|
-
and annotation is not Child
|
|
643
|
-
and not _is_any_annotation(annotation)
|
|
644
|
-
):
|
|
645
|
-
raise TypeError(
|
|
646
|
-
f"*{var_positional.name} must be annotated as `*{var_positional.name}: ps.Child`"
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
# Validate `key`` argument
|
|
650
|
-
if key is None:
|
|
651
|
-
raise ValueError("Function must define a `key: str | None = None` parameter")
|
|
652
|
-
if key.default is not None:
|
|
653
|
-
raise ValueError("'key' parameter must default to None")
|
|
654
|
-
if key.kind not in (
|
|
655
|
-
Parameter.KEYWORD_ONLY,
|
|
656
|
-
Parameter.POSITIONAL_OR_KEYWORD,
|
|
657
|
-
):
|
|
658
|
-
raise ValueError("'key' parameter must be a keyword argument")
|
|
659
|
-
|
|
660
|
-
# Parse **props as Unpack[TypedDict]
|
|
661
|
-
unpack_spec = parse_typed_dict_props(var_kw)
|
|
662
|
-
|
|
663
|
-
return unpack_spec.merge(explicit_spec)
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
class ComponentRegistry:
|
|
667
|
-
"""A registry for React components that can be used as a context manager."""
|
|
668
|
-
|
|
669
|
-
_token: Any
|
|
670
|
-
|
|
671
|
-
def __init__(self):
|
|
672
|
-
self.components: list[ReactComponent[...]] = []
|
|
673
|
-
self._token = None
|
|
674
|
-
|
|
675
|
-
def add(self, component: ReactComponent[...]):
|
|
676
|
-
"""Adds a component to the registry."""
|
|
677
|
-
self.components.append(component)
|
|
678
|
-
|
|
679
|
-
def clear(self):
|
|
680
|
-
self.components.clear()
|
|
681
|
-
|
|
682
|
-
def __enter__(self) -> "ComponentRegistry":
|
|
683
|
-
self._token = COMPONENT_REGISTRY.set(self)
|
|
684
|
-
return self
|
|
685
|
-
|
|
686
|
-
def __exit__(
|
|
687
|
-
self,
|
|
688
|
-
exc_type: type[BaseException] | None,
|
|
689
|
-
exc_val: BaseException | None,
|
|
690
|
-
exc_tb: Any,
|
|
691
|
-
) -> Literal[False]:
|
|
692
|
-
if self._token:
|
|
693
|
-
COMPONENT_REGISTRY.reset(self._token)
|
|
694
|
-
self._token = None
|
|
695
|
-
return False
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
COMPONENT_REGISTRY: ContextVar[ComponentRegistry] = ContextVar(
|
|
699
|
-
"component_registry",
|
|
700
|
-
default=ComponentRegistry(), # noqa: B039
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
def registered_react_components():
|
|
705
|
-
"""Get all registered React components."""
|
|
706
|
-
return COMPONENT_REGISTRY.get().components
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
# ----------------------------------------------------------------------------
|
|
710
|
-
# Utilities: Build Props specs from TypedDict definitions
|
|
711
|
-
# ----------------------------------------------------------------------------
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
def _is_typeddict_type(cls: type) -> bool:
|
|
715
|
-
"""Best-effort detection for TypedDict types across Python versions."""
|
|
716
|
-
return isinstance(getattr(cls, "__annotations__", None), dict) and getattr(
|
|
717
|
-
cls, "__total__", None
|
|
718
|
-
) in (True, False)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
def _unwrap_required_notrequired(annotation: Any) -> tuple[Any, bool | None]:
|
|
722
|
-
"""
|
|
723
|
-
If annotation is typing.Required[T] or typing.NotRequired[T], return (T, required?).
|
|
724
|
-
Otherwise return (annotation, None).
|
|
725
|
-
"""
|
|
726
|
-
|
|
727
|
-
origin = get_origin(annotation)
|
|
728
|
-
if origin is typing.Required:
|
|
729
|
-
args = get_args(annotation)
|
|
730
|
-
inner = args[0] if args else Any
|
|
731
|
-
return inner, True
|
|
732
|
-
if origin is typing.NotRequired:
|
|
733
|
-
args = get_args(annotation)
|
|
734
|
-
inner = args[0] if args else Any
|
|
735
|
-
return inner, False
|
|
736
|
-
return annotation, None
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
@cache
|
|
740
|
-
def _annotation_to_runtime_type(annotation: Any) -> type | tuple[type, ...]:
|
|
741
|
-
"""
|
|
742
|
-
Convert a typing annotation into a runtime-checkable class or tuple of classes
|
|
743
|
-
suitable for isinstance(). This is intentionally lossy but practical.
|
|
744
|
-
"""
|
|
745
|
-
# Unwrap Required/NotRequired
|
|
746
|
-
annotation, _ = _unwrap_required_notrequired(annotation)
|
|
747
|
-
|
|
748
|
-
origin = get_origin(annotation)
|
|
749
|
-
args = get_args(annotation)
|
|
750
|
-
|
|
751
|
-
# Any -> accept anything
|
|
752
|
-
if annotation is Any:
|
|
753
|
-
return object
|
|
754
|
-
|
|
755
|
-
# Annotated[T, ...] -> T
|
|
756
|
-
if origin is Annotated and args:
|
|
757
|
-
return _annotation_to_runtime_type(args[0])
|
|
758
|
-
|
|
759
|
-
# Optional[T] / Union[...] / X | Y
|
|
760
|
-
if origin is UnionType:
|
|
761
|
-
# Fallback for some Python versions where get_origin may be odd
|
|
762
|
-
union_args = args or getattr(annotation, "__args__", ())
|
|
763
|
-
runtime_types: list[type] = []
|
|
764
|
-
for a in union_args:
|
|
765
|
-
rt = _annotation_to_runtime_type(a)
|
|
766
|
-
if isinstance(rt, tuple):
|
|
767
|
-
runtime_types.extend(rt)
|
|
768
|
-
elif isinstance(rt, type):
|
|
769
|
-
runtime_types.append(rt)
|
|
770
|
-
# Deduplicate while preserving order
|
|
771
|
-
out: list[type] = []
|
|
772
|
-
for t in runtime_types:
|
|
773
|
-
if t not in out:
|
|
774
|
-
out.append(t)
|
|
775
|
-
return tuple(out) if len(out) > 1 else (out[0] if out else object)
|
|
776
|
-
|
|
777
|
-
# Literal[...] -> base types of provided literals
|
|
778
|
-
if origin is Literal:
|
|
779
|
-
literal_types: set[type] = {type(v) for v in args}
|
|
780
|
-
# None appears as NoneType
|
|
781
|
-
if len(literal_types) == 0:
|
|
782
|
-
return object
|
|
783
|
-
if len(literal_types) == 1:
|
|
784
|
-
return next(iter(literal_types))
|
|
785
|
-
return tuple(literal_types)
|
|
786
|
-
|
|
787
|
-
# Parametrized containers -> use their builtin origins for isinstance
|
|
788
|
-
if origin in (list, dict, set, tuple):
|
|
789
|
-
return cast(type | tuple[type, ...], origin)
|
|
790
|
-
|
|
791
|
-
# TypedDict nested -> treat as dict
|
|
792
|
-
if isinstance(annotation, type) and _is_typeddict_type(annotation):
|
|
793
|
-
return dict
|
|
794
|
-
|
|
795
|
-
# Direct classes
|
|
796
|
-
if isinstance(annotation, type):
|
|
797
|
-
return annotation
|
|
798
|
-
|
|
799
|
-
# Fallback: accept anything
|
|
800
|
-
return object
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
def _extract_prop_from_annotated(annotation: Any) -> tuple[Any, Prop[Any] | None]:
|
|
804
|
-
"""
|
|
805
|
-
If annotation is Annotated[T, ...] and any metadata item is a Prop, return (T, Prop).
|
|
806
|
-
Otherwise return (annotation, None).
|
|
807
|
-
"""
|
|
808
|
-
origin = get_origin(annotation)
|
|
809
|
-
args = get_args(annotation)
|
|
810
|
-
if origin is Annotated and args:
|
|
811
|
-
base = args[0]
|
|
812
|
-
meta = args[1:]
|
|
813
|
-
for m in meta:
|
|
814
|
-
if isinstance(m, Prop):
|
|
815
|
-
return base, m
|
|
816
|
-
return annotation, None
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
def _clone_prop(p: Prop[Any]) -> Prop[Any]:
|
|
820
|
-
"""Shallow clone a Prop to avoid sharing instances across cached specs."""
|
|
821
|
-
return Prop(
|
|
822
|
-
default=p.default,
|
|
823
|
-
required=p.required,
|
|
824
|
-
default_factory=p.default_factory,
|
|
825
|
-
serialize=p.serialize,
|
|
826
|
-
map_to=p.map_to,
|
|
827
|
-
typ_=p.typ_,
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
def _typed_dict_bases(typed_dict_cls: type) -> list[type]:
|
|
832
|
-
return [
|
|
833
|
-
b for b in getattr(typed_dict_cls, "__bases__", ()) if _is_typeddict_type(b)
|
|
834
|
-
]
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
@cache
|
|
838
|
-
def prop_spec_from_typeddict(typed_dict_cls: type) -> PropSpec:
|
|
839
|
-
"""Build and cache a Props spec from a TypedDict class tree.
|
|
840
|
-
|
|
841
|
-
Caches by the TypedDict class object (stable and hashable). This speeds up
|
|
842
|
-
repeated reuse of common props like HTMLProps and HTMLAnchorProps across many
|
|
843
|
-
component definitions.
|
|
844
|
-
"""
|
|
845
|
-
annotations: dict[str, Any] = getattr(typed_dict_cls, "__annotations__", {})
|
|
846
|
-
# If TypedDict annotations are stringified, skip building and allow unspecified.
|
|
847
|
-
if annotations and any(isinstance(v, str) for v in annotations.values()):
|
|
848
|
-
return PropSpec({}, {}, allow_unspecified=True)
|
|
849
|
-
required_keys: set[str] | None = getattr(typed_dict_cls, "__required_keys__", None)
|
|
850
|
-
is_total: bool = bool(getattr(typed_dict_cls, "__total__", True))
|
|
851
|
-
|
|
852
|
-
# 1) Merge cached specs from TypedDict base classes (preserve insertion order)
|
|
853
|
-
merged: dict[str, Prop[Any]] = {}
|
|
854
|
-
for base in _typed_dict_bases(typed_dict_cls):
|
|
855
|
-
base_spec = prop_spec_from_typeddict(base)
|
|
856
|
-
for k, p in base_spec.as_dict().items():
|
|
857
|
-
merged[k] = _clone_prop(p)
|
|
858
|
-
|
|
859
|
-
# 2) Add/override keys declared locally on this class in definition order.
|
|
860
|
-
# If a key exists in a base, this intentionally shadows the base entry.
|
|
861
|
-
for key in annotations.keys():
|
|
862
|
-
annotation = annotations[key]
|
|
863
|
-
# First see if runtime provides explicit required/optional wrappers
|
|
864
|
-
annotation, _annotation_required = _unwrap_required_notrequired(annotation)
|
|
865
|
-
# Extract Prop metadata from Annotated if present
|
|
866
|
-
annotation, annotation_prop = _extract_prop_from_annotated(annotation)
|
|
867
|
-
|
|
868
|
-
runtime_type = _annotation_to_runtime_type(annotation)
|
|
869
|
-
prop = annotation_prop or Prop()
|
|
870
|
-
if prop.required is not DEFAULT:
|
|
871
|
-
raise TypeError(
|
|
872
|
-
"Use total=True + NotRequired[T] or total=False + Required[T] to define required and optional props within a TypedDict"
|
|
873
|
-
)
|
|
874
|
-
prop.typ_ = runtime_type
|
|
875
|
-
merged[key] = prop
|
|
876
|
-
|
|
877
|
-
# 3) Finalize required flags per this class's semantics
|
|
878
|
-
if required_keys is not None:
|
|
879
|
-
for k, p in merged.items():
|
|
880
|
-
p.required = k in required_keys
|
|
881
|
-
else:
|
|
882
|
-
# Fallback: infer via wrappers if available; otherwise default to class total
|
|
883
|
-
for k, p in merged.items():
|
|
884
|
-
ann = annotations.get(k, None)
|
|
885
|
-
if ann is not None:
|
|
886
|
-
_, req = _unwrap_required_notrequired(ann)
|
|
887
|
-
if req is not None:
|
|
888
|
-
p.required = req
|
|
889
|
-
continue
|
|
890
|
-
p.required = is_total
|
|
891
|
-
|
|
892
|
-
# Split into required/optional
|
|
893
|
-
required: dict[str, Prop[Any]] = {}
|
|
894
|
-
optional: dict[str, Prop[Any]] = {}
|
|
895
|
-
for k, p in merged.items():
|
|
896
|
-
if p.required is True:
|
|
897
|
-
required[k] = p
|
|
898
|
-
else:
|
|
899
|
-
optional[k] = p
|
|
900
|
-
|
|
901
|
-
return PropSpec(required, optional)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
def parse_typed_dict_props(var_kw: Parameter | None) -> PropSpec:
|
|
905
|
-
"""
|
|
906
|
-
Build a Props spec from a TypedDict class.
|
|
907
|
-
|
|
908
|
-
- Required vs optional is inferred from __required_keys__/__optional_keys__ when
|
|
909
|
-
available, otherwise from Required/NotRequired wrappers or the class __total__.
|
|
910
|
-
- Types are converted to runtime-checkable types for isinstance checks.
|
|
911
|
-
"""
|
|
912
|
-
# No **props -> no keyword arguments defined here
|
|
913
|
-
if not var_kw:
|
|
914
|
-
return PropSpec({}, {})
|
|
915
|
-
|
|
916
|
-
# Untyped **props -> allow all
|
|
917
|
-
annot = var_kw.annotation
|
|
918
|
-
if annot in (None, Parameter.empty):
|
|
919
|
-
return PropSpec({}, {}, allow_unspecified=True)
|
|
920
|
-
if _is_any_annotation(annot):
|
|
921
|
-
return PropSpec({}, {}, allow_unspecified=True)
|
|
922
|
-
# Stringified annotations like "Unpack[Props]" are too fragile to parse.
|
|
923
|
-
if isinstance(annot, str):
|
|
924
|
-
return PropSpec({}, {}, allow_unspecified=True)
|
|
925
|
-
|
|
926
|
-
# From here, we should have **props: Unpack[MyProps] where MyProps is a TypedDict
|
|
927
|
-
origin = get_origin(annot)
|
|
928
|
-
if origin is not Unpack:
|
|
929
|
-
raise TypeError(
|
|
930
|
-
"**props must be annotated as typing.Unpack[Props] where Props is a TypedDict"
|
|
931
|
-
)
|
|
932
|
-
unpack_args = get_args(annot)
|
|
933
|
-
if not unpack_args:
|
|
934
|
-
raise TypeError("Unpack must wrap a TypedDict class, e.g., Unpack[MyProps]")
|
|
935
|
-
typed_arg = unpack_args[0]
|
|
936
|
-
|
|
937
|
-
# Handle parameterized TypedDicts like MyProps[T] or MyProps[int]
|
|
938
|
-
# typing.get_origin returns the underlying class for parameterized generics
|
|
939
|
-
origin_td = get_origin(typed_arg) or typed_arg
|
|
940
|
-
|
|
941
|
-
if not isinstance(origin_td, type) or not _is_typeddict_type(origin_td):
|
|
942
|
-
raise TypeError("Unpack must wrap a TypedDict class, e.g., Unpack[MyProps]")
|
|
943
|
-
|
|
944
|
-
# NOTE: For TypedDicts, the annotations contain the fields of all classes in
|
|
945
|
-
# the hierarchy, we don't need to walk the MRO. Use cached builder.
|
|
946
|
-
return prop_spec_from_typeddict(origin_td)
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
# ----------------------------------------------------------------------------
|
|
950
|
-
# Public decorator: define a wrapped React component from a Python function
|
|
951
|
-
# ----------------------------------------------------------------------------
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
def react_component(
|
|
955
|
-
name: str | Literal["default"],
|
|
956
|
-
src: str,
|
|
957
|
-
*,
|
|
958
|
-
prop: str | None = None,
|
|
959
|
-
is_default: bool = False,
|
|
960
|
-
lazy: bool = False,
|
|
961
|
-
version: str | None = None,
|
|
962
|
-
extra_imports: list[Import] | None = None,
|
|
963
|
-
) -> Callable[[Callable[P, None] | Callable[P, Element]], ReactComponent[P]]:
|
|
964
|
-
"""
|
|
965
|
-
Decorator to define a React component wrapper. The decorated function is
|
|
966
|
-
passed to `ReactComponent`, which parses and validates its signature.
|
|
967
|
-
|
|
968
|
-
Args:
|
|
969
|
-
tag: Name of the component (or "default" for default export)
|
|
970
|
-
import_: Module path to import the component from
|
|
971
|
-
property: Optional property name to access the component from the imported object
|
|
972
|
-
is_default: True if this is a default export, else named export
|
|
973
|
-
lazy: Whether to lazy load the component
|
|
974
|
-
"""
|
|
975
|
-
|
|
976
|
-
def decorator(fn: Callable[P, None] | Callable[P, Element]) -> ReactComponent[P]:
|
|
977
|
-
return ReactComponent(
|
|
978
|
-
name=name,
|
|
979
|
-
src=src,
|
|
980
|
-
prop=prop,
|
|
981
|
-
is_default=is_default,
|
|
982
|
-
lazy=lazy,
|
|
983
|
-
version=version,
|
|
984
|
-
fn_signature=fn,
|
|
985
|
-
extra_imports=extra_imports,
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
return decorator
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
# ----------------------------------------------------------------------------
|
|
992
|
-
# Helpers for display of runtime types
|
|
993
|
-
# ----------------------------------------------------------------------------
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
def _format_runtime_type(t: type | tuple[type, ...]) -> str:
|
|
997
|
-
if isinstance(t, tuple):
|
|
998
|
-
return "(" + ", ".join(_format_runtime_type(x) for x in t) + ")"
|
|
999
|
-
if isinstance(t, type):
|
|
1000
|
-
return getattr(t, "__name__", repr(t))
|
|
1001
|
-
return repr(t)
|
|
5
|
+
__all__ = ["react_component", "default_signature"]
|