pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. pulse/__init__.py +9 -23
  2. pulse/app.py +6 -25
  3. pulse/cli/processes.py +1 -0
  4. pulse/codegen/codegen.py +43 -88
  5. pulse/codegen/js.py +35 -5
  6. pulse/codegen/templates/route.py +341 -254
  7. pulse/form.py +1 -1
  8. pulse/helpers.py +51 -27
  9. pulse/hooks/core.py +2 -2
  10. pulse/hooks/effects.py +1 -1
  11. pulse/hooks/init.py +2 -1
  12. pulse/hooks/setup.py +1 -1
  13. pulse/hooks/stable.py +2 -2
  14. pulse/hooks/states.py +2 -2
  15. pulse/html/props.py +3 -2
  16. pulse/html/tags.py +135 -0
  17. pulse/html/tags.pyi +4 -0
  18. pulse/js/__init__.py +110 -0
  19. pulse/js/__init__.pyi +95 -0
  20. pulse/js/_types.py +297 -0
  21. pulse/js/array.py +253 -0
  22. pulse/js/console.py +47 -0
  23. pulse/js/date.py +113 -0
  24. pulse/js/document.py +138 -0
  25. pulse/js/error.py +139 -0
  26. pulse/js/json.py +62 -0
  27. pulse/js/map.py +84 -0
  28. pulse/js/math.py +66 -0
  29. pulse/js/navigator.py +76 -0
  30. pulse/js/number.py +54 -0
  31. pulse/js/object.py +173 -0
  32. pulse/js/promise.py +150 -0
  33. pulse/js/regexp.py +54 -0
  34. pulse/js/set.py +109 -0
  35. pulse/js/string.py +35 -0
  36. pulse/js/weakmap.py +50 -0
  37. pulse/js/weakset.py +45 -0
  38. pulse/js/window.py +199 -0
  39. pulse/messages.py +22 -3
  40. pulse/proxy.py +21 -8
  41. pulse/react_component.py +167 -14
  42. pulse/reactive_extensions.py +5 -5
  43. pulse/render_session.py +144 -34
  44. pulse/renderer.py +80 -115
  45. pulse/routing.py +1 -18
  46. pulse/transpiler/__init__.py +131 -0
  47. pulse/transpiler/builtins.py +731 -0
  48. pulse/transpiler/constants.py +110 -0
  49. pulse/transpiler/context.py +26 -0
  50. pulse/transpiler/errors.py +2 -0
  51. pulse/transpiler/function.py +250 -0
  52. pulse/transpiler/ids.py +16 -0
  53. pulse/transpiler/imports.py +409 -0
  54. pulse/transpiler/js_module.py +274 -0
  55. pulse/transpiler/modules/__init__.py +30 -0
  56. pulse/transpiler/modules/asyncio.py +38 -0
  57. pulse/transpiler/modules/json.py +20 -0
  58. pulse/transpiler/modules/math.py +320 -0
  59. pulse/transpiler/modules/re.py +466 -0
  60. pulse/transpiler/modules/tags.py +268 -0
  61. pulse/transpiler/modules/typing.py +59 -0
  62. pulse/transpiler/nodes.py +1216 -0
  63. pulse/transpiler/py_module.py +119 -0
  64. pulse/transpiler/transpiler.py +938 -0
  65. pulse/transpiler/utils.py +4 -0
  66. pulse/vdom.py +112 -6
  67. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
  68. pulse_framework-0.1.48.dist-info/RECORD +119 -0
  69. pulse/codegen/imports.py +0 -204
  70. pulse/css.py +0 -155
  71. pulse_framework-0.1.46.dist-info/RECORD +0 -80
  72. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
  73. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/entry_points.txt +0 -0
pulse/js/window.py ADDED
@@ -0,0 +1,199 @@
1
+ """Browser window global Any.
2
+
3
+ Usage:
4
+ from pulse.js import window
5
+ window.alert("Hello!") # -> window.alert("Hello!")
6
+ window.innerWidth # -> window.innerWidth
7
+ """
8
+
9
+ from collections.abc import Callable as _Callable
10
+ from typing import Any as _Any
11
+
12
+ from pulse.js._types import Element as _Element
13
+ from pulse.js._types import Selection as _Selection
14
+ from pulse.transpiler.js_module import register_js_module as _register_js_module
15
+
16
+ # Dimensions
17
+ innerWidth: int
18
+ innerHeight: int
19
+ outerWidth: int
20
+ outerHeight: int
21
+
22
+ # Scroll position
23
+ scrollX: float
24
+ scrollY: float
25
+ pageXOffset: float # Alias for scrollX
26
+ pageYOffset: float # Alias for scrollY
27
+
28
+ # Screen information
29
+ devicePixelRatio: float
30
+
31
+ # Location and history (typed as Any since they're complex interfaces)
32
+ location: _Any
33
+ history: _Any
34
+ navigator: _Any
35
+ document: _Any
36
+
37
+ # Storage
38
+ localStorage: _Any
39
+ sessionStorage: _Any
40
+
41
+ # Performance
42
+ performance: _Any
43
+
44
+
45
+ # Dialog methods
46
+ def alert(message: str = "") -> None:
47
+ """Display an alert dialog with the given message."""
48
+ ...
49
+
50
+
51
+ def confirm(message: str = "") -> bool:
52
+ """Display a confirmation dialog. Returns True if user clicks OK."""
53
+ ...
54
+
55
+
56
+ def prompt(message: str = "", default: str = "") -> str | None:
57
+ """Display a prompt dialog. Returns input or None if cancelled."""
58
+ ...
59
+
60
+
61
+ # Scroll methods
62
+ def scrollTo(x: float | dict[str, float], y: float | None = None) -> None:
63
+ """Scroll to the given position."""
64
+ ...
65
+
66
+
67
+ def scrollBy(x: float | dict[str, float], y: float | None = None) -> None:
68
+ """Scroll by the given amount."""
69
+ ...
70
+
71
+
72
+ def scroll(x: float | dict[str, float], y: float | None = None) -> None:
73
+ """Alias for scrollTo."""
74
+ ...
75
+
76
+
77
+ # Selection
78
+ def getSelection() -> _Selection | None:
79
+ """Return the current text selection."""
80
+ ...
81
+
82
+
83
+ def getComputedStyle(element: _Element, pseudoElt: str | None = None) -> _Any:
84
+ """Return the computed style of an element."""
85
+ ...
86
+
87
+
88
+ # Focus
89
+ def focus() -> None:
90
+ """Give focus to the window."""
91
+ ...
92
+
93
+
94
+ def blur() -> None:
95
+ """Remove focus from the window."""
96
+ ...
97
+
98
+
99
+ # Open/close
100
+ def open(
101
+ url: str = "",
102
+ target: str = "_blank",
103
+ features: str = "",
104
+ ) -> _Any | None:
105
+ """Open a new window. Returns the new window Any or None."""
106
+ ...
107
+
108
+
109
+ def close() -> None:
110
+ """Close the window (only works for windows opened by script)."""
111
+ ...
112
+
113
+
114
+ # Timers (these return timer IDs)
115
+ def setTimeout(handler: _Callable[..., None], timeout: int = 0, *args: _Any) -> int:
116
+ """Schedule a function to run after a delay. Returns timer ID."""
117
+ ...
118
+
119
+
120
+ def clearTimeout(timeoutId: int) -> None:
121
+ """Cancel a timeout scheduled with setTimeout."""
122
+ ...
123
+
124
+
125
+ def setInterval(handler: _Callable[..., None], timeout: int = 0, *args: _Any) -> int:
126
+ """Schedule a function to run repeatedly. Returns timer ID."""
127
+ ...
128
+
129
+
130
+ def clearInterval(intervalId: int) -> None:
131
+ """Cancel an interval scheduled with setInterval."""
132
+ ...
133
+
134
+
135
+ # Animation
136
+ def requestAnimationFrame(callback: _Callable[[float], None]) -> int:
137
+ """Request a callback before the next repaint. Returns request ID."""
138
+ ...
139
+
140
+
141
+ def cancelAnimationFrame(requestId: int) -> None:
142
+ """Cancel an animation frame request."""
143
+ ...
144
+
145
+
146
+ # Event listeners
147
+ def addEventListener(
148
+ type: str,
149
+ listener: _Callable[..., None],
150
+ options: bool | dict[str, bool] | None = None,
151
+ ) -> None:
152
+ """Add an event listener to the window."""
153
+ ...
154
+
155
+
156
+ def removeEventListener(
157
+ type: str,
158
+ listener: _Callable[..., None],
159
+ options: bool | dict[str, bool] | None = None,
160
+ ) -> None:
161
+ """Remove an event listener from the window."""
162
+ ...
163
+
164
+
165
+ def dispatchEvent(event: _Any) -> bool:
166
+ """Dispatch an event to the window."""
167
+ ...
168
+
169
+
170
+ # Encoding
171
+ def atob(encoded: str) -> str:
172
+ """Decode a base64 encoded string."""
173
+ ...
174
+
175
+
176
+ def btoa(data: str) -> str:
177
+ """Encode a string as base64."""
178
+ ...
179
+
180
+
181
+ # Misc
182
+ def matchMedia(query: str) -> _Any:
183
+ """Return a MediaQueryList for the given media query."""
184
+ ...
185
+
186
+
187
+ def print_() -> None:
188
+ """Open the print dialog."""
189
+ ...
190
+
191
+
192
+ def postMessage(
193
+ message: _Any, targetOrigin: str, transfer: list[_Any] | None = None
194
+ ) -> None:
195
+ """Post a message to another window."""
196
+ ...
197
+
198
+
199
+ _register_js_module(name="window")
pulse/messages.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from typing import Any, Literal, NotRequired, TypedDict
2
2
 
3
- from pulse.renderer import VDOMOperation
4
3
  from pulse.routing import RouteInfo
5
- from pulse.vdom import VDOM
4
+ from pulse.vdom import VDOM, VDOMOperation
6
5
 
7
6
 
8
7
  # ====================
@@ -14,7 +13,7 @@ class ServerInitMessage(TypedDict):
14
13
  vdom: VDOM
15
14
  callbacks: list[str]
16
15
  render_props: list[str]
17
- css_refs: list[str]
16
+ jsexpr_paths: list[str] # paths containing JS expressions
18
17
 
19
18
 
20
19
  class ServerUpdateMessage(TypedDict):
@@ -82,6 +81,15 @@ class ServerChannelResponseMessage(TypedDict):
82
81
  error: NotRequired[Any]
83
82
 
84
83
 
84
+ class ServerJsExecMessage(TypedDict):
85
+ """Execute JavaScript code on the client."""
86
+
87
+ type: Literal["js_exec"]
88
+ path: str
89
+ id: str
90
+ code: str
91
+
92
+
85
93
  # ====================
86
94
  # Client messages
87
95
  # ====================
@@ -136,6 +144,15 @@ class ClientChannelResponseMessage(TypedDict):
136
144
  error: NotRequired[Any]
137
145
 
138
146
 
147
+ class ClientJsResultMessage(TypedDict):
148
+ """Result of client-side JS execution."""
149
+
150
+ type: Literal["js_result"]
151
+ id: str
152
+ result: Any
153
+ error: str | None
154
+
155
+
139
156
  ServerChannelMessage = ServerChannelRequestMessage | ServerChannelResponseMessage
140
157
  ServerMessage = (
141
158
  ServerInitMessage
@@ -144,6 +161,7 @@ ServerMessage = (
144
161
  | ServerApiCallMessage
145
162
  | ServerNavigateToMessage
146
163
  | ServerChannelMessage
164
+ | ServerJsExecMessage
147
165
  )
148
166
 
149
167
 
@@ -153,6 +171,7 @@ ClientPulseMessage = (
153
171
  | ClientNavigateMessage
154
172
  | ClientUnmountMessage
155
173
  | ClientApiResultMessage
174
+ | ClientJsResultMessage
156
175
  )
157
176
  ClientChannelMessage = ClientChannelRequestMessage | ClientChannelResponseMessage
158
177
  ClientMessage = ClientPulseMessage | ClientChannelMessage
pulse/proxy.py CHANGED
@@ -21,19 +21,32 @@ logger = logging.getLogger(__name__)
21
21
  class ReactProxy:
22
22
  """
23
23
  Handles proxying HTTP requests and WebSocket connections to React Router server.
24
+
25
+ In single-server mode, the Python server proxies unmatched routes to the React
26
+ dev server. This proxy rewrites URLs in responses to use the external server
27
+ address instead of the internal React server address.
24
28
  """
25
29
 
26
30
  react_server_address: str
31
+ server_address: str
27
32
  _client: httpx.AsyncClient | None
28
33
 
29
- def __init__(self, react_server_address: str):
34
+ def __init__(self, react_server_address: str, server_address: str):
30
35
  """
31
36
  Args:
32
- react_server_address: React Router server full URL (required in single-server mode)
37
+ react_server_address: Internal React Router server URL (e.g., http://localhost:5173)
38
+ server_address: External server URL exposed to clients (e.g., http://localhost:8000)
33
39
  """
34
40
  self.react_server_address = react_server_address
41
+ self.server_address = server_address
35
42
  self._client = None
36
43
 
44
+ def _rewrite_url(self, url: str) -> str:
45
+ """Rewrite internal React server URLs to external server address."""
46
+ if self.react_server_address in url:
47
+ return url.replace(self.react_server_address, self.server_address)
48
+ return url
49
+
37
50
  @property
38
51
  def client(self) -> httpx.AsyncClient:
39
52
  """Lazy initialization of HTTP client."""
@@ -190,12 +203,12 @@ class ReactProxy:
190
203
  # Send request with streaming
191
204
  r = await self.client.send(req, stream=True)
192
205
 
193
- # Filter out headers that shouldn't be present in streaming responses
194
- response_headers = {
195
- k: v
196
- for k, v in r.headers.items()
197
- # if k.lower() not in ("content-length", "transfer-encoding")
198
- }
206
+ # Rewrite headers that may contain internal React server URLs
207
+ response_headers: dict[str, str] = {}
208
+ for k, v in r.headers.items():
209
+ if k.lower() in ("location", "content-location"):
210
+ v = self._rewrite_url(v)
211
+ response_headers[k] = v
199
212
 
200
213
  return StreamingResponse(
201
214
  r.aiter_raw(),
pulse/react_component.py CHANGED
@@ -13,6 +13,7 @@ from types import UnionType
13
13
  from typing import (
14
14
  Annotated,
15
15
  Any,
16
+ ClassVar,
16
17
  Generic,
17
18
  Literal,
18
19
  ParamSpec,
@@ -24,9 +25,18 @@ from typing import (
24
25
  override,
25
26
  )
26
27
 
27
- from pulse.codegen.imports import Imported, ImportStatement
28
28
  from pulse.helpers import Sentinel
29
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
+ )
30
40
  from pulse.vdom import Child, Element, Node
31
41
 
32
42
  T = TypeVar("T")
@@ -295,25 +305,112 @@ def default_fn_signature_without_children(
295
305
  ) -> Element: ...
296
306
 
297
307
 
298
- class ReactComponent(Generic[P], Imported):
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]):
299
390
  """
300
391
  A React component that can be used within the UI tree.
301
392
  Returns a function that creates mount point UITreeNode instances.
302
393
 
303
394
  Args:
304
- tag: Name of the component (or "default" for default export)
305
- import_path: Module path to import the component from
306
- alias: Optional alias for the component in the registry
395
+ name: Name of the component (or "default" for default export)
396
+ src: Module path to import the component from
307
397
  is_default: True if this is a default export, else named export
308
- import_name: If specified, import this name from import_path and access tag as a property of it
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.)
309
404
 
310
405
  Returns:
311
406
  A function that creates Node instances with mount point tags
312
407
  """
313
408
 
409
+ import_: Import
314
410
  props_spec: PropSpec
315
411
  fn_signature: Callable[P, Element]
316
412
  lazy: bool
413
+ _prop: str | None # Property access like AppShell.Header
317
414
 
318
415
  def __init__(
319
416
  self,
@@ -326,11 +423,14 @@ class ReactComponent(Generic[P], Imported):
326
423
  version: str | None = None,
327
424
  prop_spec: PropSpec | None = None,
328
425
  fn_signature: Callable[P, Element] = default_signature,
329
- extra_imports: tuple[ImportStatement, ...]
330
- | list[ImportStatement]
331
- | None = None,
426
+ extra_imports: tuple[Import, ...] | list[Import] | None = None,
332
427
  ):
333
- super().__init__(name, src, is_default=is_default, prop=prop)
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
334
434
 
335
435
  # Build props_spec from fn_signature if provided and props not provided
336
436
  if prop_spec:
@@ -347,10 +447,62 @@ class ReactComponent(Generic[P], Imported):
347
447
  self.lazy = lazy
348
448
  # Optional npm semver constraint for this component's package
349
449
  self.version: str | None = version
350
- # Additional import statements to include in route where this component is used
351
- self.extra_imports: list[ImportStatement] = list(extra_imports or [])
450
+ # Additional imports to include in route where this component is used
451
+ self.extra_imports: list[Import] = list(extra_imports or [])
352
452
  COMPONENT_REGISTRY.get().add(self)
353
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
+
354
506
  @override
355
507
  def __repr__(self) -> str:
356
508
  default_part = ", default=True" if self.is_default else ""
@@ -359,7 +511,8 @@ class ReactComponent(Generic[P], Imported):
359
511
  props_part = f", props_spec={self.props_spec!r}"
360
512
  return f"ReactComponent(name='{self.name}', src='{self.src}'{prop_part}{default_part}{lazy_part}{props_part})"
361
513
 
362
- def __call__(self, *children: P.args, **props: P.kwargs) -> Node:
514
+ @override
515
+ def __call__(self, *children: P.args, **props: P.kwargs) -> Node: # pyright: ignore[reportIncompatibleMethodOverride]
363
516
  key = props.get("key")
364
517
  if key is not None and not isinstance(key, str):
365
518
  raise ValueError("key must be a string or None")
@@ -806,7 +959,7 @@ def react_component(
806
959
  is_default: bool = False,
807
960
  lazy: bool = False,
808
961
  version: str | None = None,
809
- extra_imports: list[ImportStatement] | None = None,
962
+ extra_imports: list[Import] | None = None,
810
963
  ) -> Callable[[Callable[P, None] | Callable[P, Element]], ReactComponent[P]]:
811
964
  """
812
965
  Decorator to define a React component wrapper. The decorated function is
@@ -45,7 +45,7 @@ class SupportsKeysAndGetItem(Protocol[T1, T2_co]):
45
45
 
46
46
  # Return an iterable view that subscribes to per-key signals during iteration
47
47
  class ReactiveDictItems(Generic[T1, T2]):
48
- __slots__: tuple[str, ...] = ("_host",)
48
+ __slots__ = ("_host",) # pyright: ignore[reportUnannotatedClassAttribute]
49
49
  _host: ReactiveDict[T1, T2]
50
50
 
51
51
  def __init__(self, host: ReactiveDict[T1, T2]) -> None:
@@ -60,7 +60,7 @@ class ReactiveDictItems(Generic[T1, T2]):
60
60
 
61
61
 
62
62
  class ReactiveDictValues(Generic[T1, T2]):
63
- __slots__: tuple[str, ...] = ("_host",)
63
+ __slots__ = ("_host",) # pyright: ignore[reportUnannotatedClassAttribute]
64
64
  _host: ReactiveDict[T1, T2]
65
65
 
66
66
  def __init__(self, host: ReactiveDict[T1, T2]) -> None:
@@ -84,7 +84,7 @@ class ReactiveDict(dict[T1, T2]):
84
84
  - Iteration, membership checks, and len are reactive to structural changes
85
85
  """
86
86
 
87
- __slots__: tuple[str, ...] = ("_signals", "_structure")
87
+ __slots__ = ("_signals", "_structure") # pyright: ignore[reportUnannotatedClassAttribute]
88
88
 
89
89
  def __init__(self, initial: Mapping[T1, T2] | None = None) -> None:
90
90
  super().__init__()
@@ -409,7 +409,7 @@ class ReactiveList(list[T1]):
409
409
  - len() subscribes to structural changes
410
410
  """
411
411
 
412
- __slots__: tuple[str, ...] = ("_signals", "_structure")
412
+ __slots__ = ("_signals", "_structure") # pyright: ignore[reportUnannotatedClassAttribute]
413
413
 
414
414
  def __init__(self, initial: Iterable[T1] | None = None) -> None:
415
415
  super().__init__()
@@ -645,7 +645,7 @@ class ReactiveSet(set[T1]):
645
645
  - Iteration subscribes to membership signals for all elements
646
646
  """
647
647
 
648
- __slots__: tuple[str, ...] = ("_signals",)
648
+ __slots__ = ("_signals",) # pyright: ignore[reportUnannotatedClassAttribute]
649
649
 
650
650
  def __init__(self, initial: Iterable[T1] | None = None) -> None:
651
651
  super().__init__()