reactpy 2.0.0b4__py3-none-any.whl → 2.0.0b6__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 (55) hide show
  1. reactpy/__init__.py +3 -2
  2. reactpy/_console/rewrite_props.py +2 -2
  3. reactpy/_html.py +11 -9
  4. reactpy/_option.py +2 -1
  5. reactpy/config.py +2 -2
  6. reactpy/core/_life_cycle_hook.py +12 -10
  7. reactpy/core/_thread_local.py +2 -1
  8. reactpy/core/component.py +4 -38
  9. reactpy/core/events.py +61 -36
  10. reactpy/core/hooks.py +25 -35
  11. reactpy/core/layout.py +193 -201
  12. reactpy/core/serve.py +17 -22
  13. reactpy/core/vdom.py +9 -12
  14. reactpy/executors/asgi/__init__.py +9 -4
  15. reactpy/executors/asgi/middleware.py +1 -2
  16. reactpy/executors/asgi/pyscript.py +3 -7
  17. reactpy/executors/asgi/standalone.py +4 -6
  18. reactpy/executors/asgi/types.py +2 -2
  19. reactpy/pyscript/components.py +3 -3
  20. reactpy/pyscript/utils.py +49 -46
  21. reactpy/reactjs/__init__.py +353 -0
  22. reactpy/reactjs/module.py +203 -0
  23. reactpy/reactjs/types.py +7 -0
  24. reactpy/reactjs/utils.py +183 -0
  25. reactpy/static/index-h31022cd.js +5 -0
  26. reactpy/static/index-h31022cd.js.map +11 -0
  27. reactpy/static/index-sbddj6ms.js +5 -0
  28. reactpy/static/index-sbddj6ms.js.map +10 -0
  29. reactpy/static/index-y71bxs88.js +5 -0
  30. reactpy/static/index-y71bxs88.js.map +10 -0
  31. reactpy/static/index.js +2 -2
  32. reactpy/static/index.js.map +6 -10
  33. reactpy/static/react-dom.js +4 -0
  34. reactpy/static/react-dom.js.map +11 -0
  35. reactpy/static/react-jsx-runtime.js +4 -0
  36. reactpy/static/react-jsx-runtime.js.map +9 -0
  37. reactpy/static/react.js +4 -0
  38. reactpy/static/react.js.map +10 -0
  39. reactpy/testing/backend.py +6 -5
  40. reactpy/testing/common.py +3 -5
  41. reactpy/testing/display.py +2 -1
  42. reactpy/testing/logs.py +1 -1
  43. reactpy/transforms.py +2 -2
  44. reactpy/types.py +117 -58
  45. reactpy/utils.py +8 -8
  46. reactpy/web/__init__.py +0 -6
  47. reactpy/web/module.py +37 -470
  48. reactpy/web/utils.py +2 -158
  49. reactpy/widgets.py +2 -2
  50. {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/METADATA +4 -7
  51. {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/RECORD +54 -39
  52. reactpy/web/templates/react.js +0 -61
  53. {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/WHEEL +0 -0
  54. {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/entry_points.txt +0 -0
  55. {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/licenses/LICENSE +0 -0
reactpy/core/layout.py CHANGED
@@ -1,26 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
- import abc
4
3
  from asyncio import (
5
4
  FIRST_COMPLETED,
6
5
  CancelledError,
7
6
  Queue,
8
7
  Task,
9
8
  create_task,
9
+ current_task,
10
10
  get_running_loop,
11
11
  wait,
12
12
  )
13
13
  from collections import Counter
14
- from collections.abc import Sequence
15
- from contextlib import AsyncExitStack
14
+ from collections.abc import Callable
15
+ from contextlib import AsyncExitStack, suppress
16
16
  from logging import getLogger
17
17
  from types import TracebackType
18
18
  from typing import (
19
19
  Any,
20
- Callable,
21
20
  Generic,
22
21
  NamedTuple,
23
22
  NewType,
23
+ TypeAlias,
24
24
  TypeVar,
25
25
  cast,
26
26
  )
@@ -28,25 +28,25 @@ from uuid import uuid4
28
28
  from weakref import ref as weakref
29
29
 
30
30
  from anyio import Semaphore
31
- from typing_extensions import TypeAlias
32
31
 
33
32
  from reactpy.config import (
34
33
  REACTPY_ASYNC_RENDERING,
35
34
  REACTPY_CHECK_VDOM_SPEC,
36
35
  REACTPY_DEBUG,
37
36
  )
38
- from reactpy.core._life_cycle_hook import LifeCycleHook
37
+ from reactpy.core._life_cycle_hook import HOOK_STACK, LifeCycleHook
39
38
  from reactpy.core.vdom import validate_vdom_json
40
39
  from reactpy.types import (
41
- ComponentType,
40
+ BaseLayout,
41
+ Component,
42
42
  Context,
43
+ ContextProvider,
43
44
  Event,
44
45
  EventHandlerDict,
45
46
  Key,
46
47
  LayoutEventMessage,
47
48
  LayoutUpdateMessage,
48
49
  VdomChild,
49
- VdomDict,
50
50
  VdomJson,
51
51
  )
52
52
  from reactpy.utils import Ref
@@ -54,26 +54,11 @@ from reactpy.utils import Ref
54
54
  logger = getLogger(__name__)
55
55
 
56
56
 
57
- class Layout:
58
- """Responsible for "rendering" components. That is, turning them into VDOM."""
59
-
60
- __slots__: tuple[str, ...] = (
61
- "_event_handlers",
62
- "_model_states_by_life_cycle_state_id",
63
- "_render_tasks",
64
- "_render_tasks_ready",
65
- "_rendering_queue",
66
- "_root_life_cycle_state_id",
67
- "root",
68
- )
69
-
70
- if not hasattr(abc.ABC, "__weakref__"): # nocov
71
- __slots__ += ("__weakref__",)
72
-
73
- def __init__(self, root: ComponentType | Context[Any]) -> None:
57
+ class Layout(BaseLayout):
58
+ def __init__(self, root: Component | Context[Any] | ContextProvider[Any]) -> None:
74
59
  super().__init__()
75
- if not isinstance(root, ComponentType):
76
- msg = f"Expected a ComponentType, not {type(root)!r}."
60
+ if not isinstance(root, Component):
61
+ msg = f"Expected a ReactPy component, not {type(root)!r}."
77
62
  raise TypeError(msg)
78
63
  self.root = root
79
64
 
@@ -81,6 +66,9 @@ class Layout:
81
66
  # create attributes here to avoid access before entering context manager
82
67
  self._event_handlers: EventHandlerDict = {}
83
68
  self._render_tasks: set[Task[LayoutUpdateMessage]] = set()
69
+ self._render_tasks_by_id: dict[
70
+ _LifeCycleStateId, Task[LayoutUpdateMessage]
71
+ ] = {}
84
72
  self._render_tasks_ready: Semaphore = Semaphore(0)
85
73
  self._rendering_queue: _ThreadSafeQueue[_LifeCycleStateId] = _ThreadSafeQueue()
86
74
  root_model_state = _new_root_model_state(self.root, self._schedule_render_task)
@@ -98,16 +86,14 @@ class Layout:
98
86
 
99
87
  for t in self._render_tasks:
100
88
  t.cancel()
101
- try:
89
+ with suppress(CancelledError):
102
90
  await t
103
- except CancelledError:
104
- pass
105
-
106
91
  await self._unmount_model_states([root_model_state])
107
92
 
108
93
  # delete attributes here to avoid access after exiting context manager
109
94
  del self._event_handlers
110
95
  del self._rendering_queue
96
+ del self._render_tasks_by_id
111
97
  del self._root_life_cycle_state_id
112
98
  del self._model_states_by_life_cycle_state_id
113
99
 
@@ -155,42 +141,121 @@ class Layout:
155
141
  """Await to fetch the first completed render within our asyncio task group.
156
142
  We use the `asyncio.tasks.wait` API in order to return the first completed task.
157
143
  """
158
- await self._render_tasks_ready.acquire()
159
- done, _ = await wait(self._render_tasks, return_when=FIRST_COMPLETED)
160
- update_task: Task[LayoutUpdateMessage] = done.pop()
161
- self._render_tasks.remove(update_task)
162
- return update_task.result()
144
+ while True:
145
+ await self._render_tasks_ready.acquire()
146
+ if not self._render_tasks: # nocov
147
+ continue
148
+ done, _ = await wait(self._render_tasks, return_when=FIRST_COMPLETED)
149
+ update_task: Task[LayoutUpdateMessage] = done.pop()
150
+ self._render_tasks.discard(update_task)
151
+
152
+ for lcs_id, task in list(self._render_tasks_by_id.items()):
153
+ if task is update_task:
154
+ del self._render_tasks_by_id[lcs_id]
155
+ break
156
+
157
+ try:
158
+ return update_task.result()
159
+ except CancelledError: # nocov
160
+ continue
163
161
 
164
162
  async def _create_layout_update(
165
163
  self, old_state: _ModelState
166
164
  ) -> LayoutUpdateMessage:
167
- new_state = _copy_component_model_state(old_state)
168
- component = new_state.life_cycle_state.component
169
-
170
- async with AsyncExitStack() as exit_stack:
171
- await self._render_component(exit_stack, old_state, new_state, component)
172
-
173
- if REACTPY_CHECK_VDOM_SPEC.current:
174
- validate_vdom_json(new_state.model.current)
165
+ token = HOOK_STACK.initialize()
166
+ try:
167
+ component = old_state.life_cycle_state.component
168
+ try:
169
+ parent: _ModelState | None = old_state.parent
170
+ except AttributeError:
171
+ parent = None
172
+
173
+ async with AsyncExitStack() as exit_stack:
174
+ new_state = await self._render_component(
175
+ exit_stack,
176
+ old_state,
177
+ parent,
178
+ old_state.index,
179
+ old_state.key,
180
+ component,
181
+ )
175
182
 
176
- return {
177
- "type": "layout-update",
178
- "path": new_state.patch_path,
179
- "model": new_state.model.current,
180
- }
183
+ if parent is not None:
184
+ parent.children_by_key[new_state.key] = new_state
185
+ old_parent_model = parent.model.current
186
+ old_parent_children = old_parent_model.setdefault("children", [])
187
+ parent.model.current = {
188
+ **old_parent_model,
189
+ "children": [
190
+ *old_parent_children[: new_state.index],
191
+ new_state.model.current,
192
+ *old_parent_children[new_state.index + 1 :],
193
+ ],
194
+ }
195
+
196
+ if REACTPY_CHECK_VDOM_SPEC.current:
197
+ validate_vdom_json(new_state.model.current)
198
+
199
+ return {
200
+ "type": "layout-update",
201
+ "path": new_state.patch_path,
202
+ "model": new_state.model.current,
203
+ }
204
+ finally:
205
+ HOOK_STACK.reset(token)
181
206
 
182
207
  async def _render_component(
183
208
  self,
184
209
  exit_stack: AsyncExitStack,
185
210
  old_state: _ModelState | None,
186
- new_state: _ModelState,
187
- component: ComponentType,
188
- ) -> None:
211
+ parent: _ModelState | None,
212
+ index: int,
213
+ key: Any,
214
+ component: Component,
215
+ ) -> _ModelState:
216
+ if old_state is None:
217
+ new_state = _make_component_model_state(
218
+ parent, index, key, component, self._schedule_render_task
219
+ )
220
+ elif (
221
+ old_state.is_component_state
222
+ and old_state.life_cycle_state.component.type != component.type
223
+ ):
224
+ await self._unmount_model_states([old_state])
225
+ new_state = _make_component_model_state(
226
+ parent, index, key, component, self._schedule_render_task
227
+ )
228
+ old_state = None
229
+ elif not old_state.is_component_state:
230
+ await self._unmount_model_states([old_state])
231
+ new_state = _make_component_model_state(
232
+ parent, index, key, component, self._schedule_render_task
233
+ )
234
+ old_state = None
235
+ elif parent is None:
236
+ new_state = _copy_component_model_state(old_state)
237
+ new_state.life_cycle_state = _update_life_cycle_state(
238
+ old_state.life_cycle_state, component
239
+ )
240
+ else:
241
+ new_state = _update_component_model_state(
242
+ old_state, parent, index, component, self._schedule_render_task
243
+ )
244
+
189
245
  life_cycle_state = new_state.life_cycle_state
190
246
  life_cycle_hook = life_cycle_state.hook
191
247
 
192
248
  self._model_states_by_life_cycle_state_id[life_cycle_state.id] = new_state
193
249
 
250
+ # If this component is scheduled to render, we can cancel that task since we are
251
+ # rendering it now.
252
+ if life_cycle_state.id in self._render_tasks_by_id:
253
+ task = self._render_tasks_by_id[life_cycle_state.id]
254
+ if task is not current_task():
255
+ del self._render_tasks_by_id[life_cycle_state.id]
256
+ task.cancel()
257
+ self._render_tasks.discard(task)
258
+
194
259
  await life_cycle_hook.affect_component_will_render(component)
195
260
  exit_stack.push_async_callback(life_cycle_hook.affect_layout_did_render)
196
261
  try:
@@ -198,8 +263,10 @@ class Layout:
198
263
  # wrap the model in a fragment (i.e. tagName="") to ensure components have
199
264
  # a separate node in the model state tree. This could be removed if this
200
265
  # components are given a node in the tree some other way
201
- wrapper_model = VdomDict(tagName="", children=[raw_model])
202
- await self._render_model(exit_stack, old_state, new_state, wrapper_model)
266
+ new_state.model.current = {"tagName": ""}
267
+ await self._render_model_children(
268
+ exit_stack, old_state, new_state, [raw_model]
269
+ )
203
270
  except Exception as error:
204
271
  logger.exception(f"Failed to render {component}")
205
272
  new_state.model.current = {
@@ -211,45 +278,41 @@ class Layout:
211
278
  finally:
212
279
  await life_cycle_hook.affect_component_did_render()
213
280
 
214
- try:
215
- parent = new_state.parent
216
- except AttributeError:
217
- pass # only happens for root component
218
- else:
219
- key, index = new_state.key, new_state.index
220
- parent.children_by_key[key] = new_state
221
- # need to add this model to parent's children without mutating parent model
222
- old_parent_model = parent.model.current
223
- old_parent_children = old_parent_model.setdefault("children", [])
224
- parent.model.current = {
225
- **old_parent_model,
226
- "children": [
227
- *old_parent_children[:index],
228
- new_state.model.current,
229
- *old_parent_children[index + 1 :],
230
- ],
231
- }
281
+ return new_state
232
282
 
233
283
  async def _render_model(
234
284
  self,
235
285
  exit_stack: AsyncExitStack,
236
286
  old_state: _ModelState | None,
237
- new_state: _ModelState,
287
+ parent: _ModelState,
288
+ index: int,
289
+ key: Any,
238
290
  raw_model: Any,
239
- ) -> None:
291
+ ) -> _ModelState:
292
+ if old_state is None:
293
+ new_state = _make_element_model_state(parent, index, key)
294
+ elif old_state.is_component_state:
295
+ await self._unmount_model_states([old_state])
296
+ new_state = _make_element_model_state(parent, index, key)
297
+ old_state = None
298
+ else:
299
+ new_state = _update_element_model_state(old_state, parent, index)
300
+
240
301
  try:
241
302
  new_state.model.current = {"tagName": raw_model["tagName"]}
242
303
  except Exception as e: # nocov
243
304
  msg = f"Expected a VDOM element dict, not {raw_model}"
244
305
  raise ValueError(msg) from e
245
- if "key" in raw_model:
246
- new_state.key = new_state.model.current["key"] = raw_model["key"]
306
+ key = raw_model.get("attributes", {}).get("key")
307
+ if key is not None:
308
+ new_state.key = key
247
309
  if "importSource" in raw_model:
248
310
  new_state.model.current["importSource"] = raw_model["importSource"]
249
311
  self._render_model_attributes(old_state, new_state, raw_model)
250
312
  await self._render_model_children(
251
313
  exit_stack, old_state, new_state, raw_model.get("children", [])
252
314
  )
315
+ return new_state
253
316
 
254
317
  def _render_model_attributes(
255
318
  self,
@@ -331,130 +394,48 @@ class Layout:
331
394
  else:
332
395
  raw_children = [raw_children]
333
396
 
334
- if old_state is None:
335
- if raw_children:
336
- await self._render_model_children_without_old_state(
337
- exit_stack, new_state, raw_children
338
- )
339
- return None
340
- elif not raw_children:
341
- await self._unmount_model_states(list(old_state.children_by_key.values()))
342
- return None
343
-
344
- children_info = _get_children_info(raw_children)
397
+ children_info, new_keys = _get_children_info(raw_children)
345
398
 
346
- new_keys = {k for _, _, k in children_info}
347
- if len(new_keys) != len(children_info):
399
+ if new_keys is None:
348
400
  key_counter = Counter(item[2] for item in children_info)
349
401
  duplicate_keys = [key for key, count in key_counter.items() if count > 1]
350
402
  msg = f"Duplicate keys {duplicate_keys} at {new_state.patch_path or '/'!r}"
351
403
  raise ValueError(msg)
352
404
 
353
- old_keys = set(old_state.children_by_key).difference(new_keys)
354
- if old_keys:
355
- await self._unmount_model_states(
356
- [old_state.children_by_key[key] for key in old_keys]
357
- )
405
+ if old_state is not None:
406
+ old_keys = set(old_state.children_by_key).difference(new_keys)
407
+ if old_keys:
408
+ await self._unmount_model_states(
409
+ [old_state.children_by_key[key] for key in old_keys]
410
+ )
358
411
 
359
- new_state.model.current["children"] = []
360
- for index, (child, child_type, key) in enumerate(children_info):
361
- old_child_state = old_state.children_by_key.get(key)
362
- if child_type is _DICT_TYPE:
363
- old_child_state = old_state.children_by_key.get(key)
364
- if old_child_state is None:
365
- new_child_state = _make_element_model_state(
366
- new_state,
367
- index,
368
- key,
369
- )
370
- elif old_child_state.is_component_state:
371
- await self._unmount_model_states([old_child_state])
372
- new_child_state = _make_element_model_state(
373
- new_state,
374
- index,
375
- key,
376
- )
377
- old_child_state = None
378
- else:
379
- new_child_state = _update_element_model_state(
380
- old_child_state,
381
- new_state,
382
- index,
383
- )
384
- await self._render_model(
385
- exit_stack, old_child_state, new_child_state, child
412
+ if raw_children:
413
+ new_state.model.current["children"] = []
414
+ for index, (child, child_type, key) in enumerate(children_info):
415
+ old_child_state = (
416
+ old_state.children_by_key.get(key)
417
+ if old_state is not None
418
+ else None
386
419
  )
387
- new_state.append_child(new_child_state.model.current)
388
- new_state.children_by_key[key] = new_child_state
389
- elif child_type is _COMPONENT_TYPE:
390
- child = cast(ComponentType, child)
391
- old_child_state = old_state.children_by_key.get(key)
392
- if old_child_state is None:
393
- new_child_state = _make_component_model_state(
394
- new_state,
395
- index,
396
- key,
397
- child,
398
- self._schedule_render_task,
420
+ if child_type is _DICT_TYPE:
421
+ new_child_state = await self._render_model(
422
+ exit_stack, old_child_state, new_state, index, key, child
399
423
  )
400
- elif old_child_state.is_component_state and (
401
- old_child_state.life_cycle_state.component.type != child.type
402
- ):
403
- await self._unmount_model_states([old_child_state])
404
- old_child_state = None
405
- new_child_state = _make_component_model_state(
406
- new_state,
407
- index,
408
- key,
409
- child,
410
- self._schedule_render_task,
424
+ elif child_type is _COMPONENT_TYPE:
425
+ child = cast(Component, child)
426
+ new_child_state = await self._render_component(
427
+ exit_stack, old_child_state, new_state, index, key, child
411
428
  )
412
429
  else:
413
- new_child_state = _update_component_model_state(
414
- old_child_state,
415
- new_state,
416
- index,
417
- child,
418
- self._schedule_render_task,
419
- )
420
- await self._render_component(
421
- exit_stack, old_child_state, new_child_state, child
422
- )
423
- else:
424
- old_child_state = old_state.children_by_key.get(key)
425
- if old_child_state is not None:
426
- await self._unmount_model_states([old_child_state])
427
- new_state.append_child(child)
430
+ if old_child_state is not None:
431
+ await self._unmount_model_states([old_child_state])
432
+ new_child_state = child
428
433
 
429
- async def _render_model_children_without_old_state(
430
- self,
431
- exit_stack: AsyncExitStack,
432
- new_state: _ModelState,
433
- raw_children: list[Any],
434
- ) -> None:
435
- children_info = _get_children_info(raw_children)
436
-
437
- new_keys = {k for _, _, k in children_info}
438
- if len(new_keys) != len(children_info):
439
- key_counter = Counter(k for _, _, k in children_info)
440
- duplicate_keys = [key for key, count in key_counter.items() if count > 1]
441
- msg = f"Duplicate keys {duplicate_keys} at {new_state.patch_path or '/'!r}"
442
- raise ValueError(msg)
443
-
444
- new_state.model.current["children"] = []
445
- for index, (child, child_type, key) in enumerate(children_info):
446
- if child_type is _DICT_TYPE:
447
- child_state = _make_element_model_state(new_state, index, key)
448
- await self._render_model(exit_stack, None, child_state, child)
449
- new_state.append_child(child_state.model.current)
450
- new_state.children_by_key[key] = child_state
451
- elif child_type is _COMPONENT_TYPE:
452
- child_state = _make_component_model_state(
453
- new_state, index, key, child, self._schedule_render_task
454
- )
455
- await self._render_component(exit_stack, None, child_state, child)
456
- else:
457
- new_state.append_child(child)
434
+ if isinstance(new_child_state, _ModelState):
435
+ new_state.append_child(new_child_state.model.current)
436
+ new_state.children_by_key[key] = new_child_state
437
+ else:
438
+ new_state.append_child(new_child_state)
458
439
 
459
440
  async def _unmount_model_states(self, old_states: list[_ModelState]) -> None:
460
441
  to_unmount = old_states[::-1] # unmount in reversed order of rendering
@@ -483,7 +464,9 @@ class Layout:
483
464
  f"{lcs_id!r} - component already unmounted"
484
465
  )
485
466
  else:
486
- self._render_tasks.add(create_task(self._create_layout_update(model_state)))
467
+ task = create_task(self._create_layout_update(model_state))
468
+ self._render_tasks.add(task)
469
+ self._render_tasks_by_id[lcs_id] = task
487
470
  self._render_tasks_ready.release()
488
471
 
489
472
  def __repr__(self) -> str:
@@ -491,7 +474,7 @@ class Layout:
491
474
 
492
475
 
493
476
  def _new_root_model_state(
494
- component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None]
477
+ component: Component, schedule_render: Callable[[_LifeCycleStateId], None]
495
478
  ) -> _ModelState:
496
479
  return _ModelState(
497
480
  parent=None,
@@ -506,10 +489,10 @@ def _new_root_model_state(
506
489
 
507
490
 
508
491
  def _make_component_model_state(
509
- parent: _ModelState,
492
+ parent: _ModelState | None,
510
493
  index: int,
511
494
  key: Any,
512
- component: ComponentType,
495
+ component: Component,
513
496
  schedule_render: Callable[[_LifeCycleStateId], None],
514
497
  ) -> _ModelState:
515
498
  return _ModelState(
@@ -517,7 +500,7 @@ def _make_component_model_state(
517
500
  index=index,
518
501
  key=key,
519
502
  model=Ref(),
520
- patch_path=f"{parent.patch_path}/children/{index}",
503
+ patch_path=f"{parent.patch_path}/children/{index}" if parent else "",
521
504
  children_by_key={},
522
505
  targets_by_event={},
523
506
  life_cycle_state=_make_life_cycle_state(component, schedule_render),
@@ -547,7 +530,7 @@ def _update_component_model_state(
547
530
  old_model_state: _ModelState,
548
531
  new_parent: _ModelState,
549
532
  new_index: int,
550
- new_component: ComponentType,
533
+ new_component: Component,
551
534
  schedule_render: Callable[[_LifeCycleStateId], None],
552
535
  ) -> _ModelState:
553
536
  return _ModelState(
@@ -673,7 +656,7 @@ class _ModelState:
673
656
 
674
657
 
675
658
  def _make_life_cycle_state(
676
- component: ComponentType,
659
+ component: Component,
677
660
  schedule_render: Callable[[_LifeCycleStateId], None],
678
661
  ) -> _LifeCycleState:
679
662
  life_cycle_state_id = _LifeCycleStateId(uuid4().hex)
@@ -686,7 +669,7 @@ def _make_life_cycle_state(
686
669
 
687
670
  def _update_life_cycle_state(
688
671
  old_life_cycle_state: _LifeCycleState,
689
- new_component: ComponentType,
672
+ new_component: Component,
690
673
  ) -> _LifeCycleState:
691
674
  return _LifeCycleState(
692
675
  old_life_cycle_state.id,
@@ -708,7 +691,7 @@ class _LifeCycleState(NamedTuple):
708
691
  hook: LifeCycleHook
709
692
  """The life cycle hook"""
710
693
 
711
- component: ComponentType
694
+ component: Component
712
695
  """The current component instance"""
713
696
 
714
697
 
@@ -732,15 +715,20 @@ class _ThreadSafeQueue(Generic[_Type]):
732
715
  return value
733
716
 
734
717
 
735
- def _get_children_info(children: list[VdomChild]) -> Sequence[_ChildInfo]:
718
+ def _get_children_info(
719
+ children: list[VdomChild],
720
+ ) -> tuple[list[_ChildInfo], set[Key] | None]:
736
721
  infos: list[_ChildInfo] = []
722
+ keys: set[Key] = set()
723
+ has_duplicates = False
724
+
737
725
  for index, child in enumerate(children):
738
726
  if child is None:
739
727
  continue
740
728
  elif isinstance(child, dict):
741
729
  child_type = _DICT_TYPE
742
- key = child.get("key")
743
- elif isinstance(child, ComponentType):
730
+ key = child.get("attributes", {}).get("key")
731
+ elif isinstance(child, Component):
744
732
  child_type = _COMPONENT_TYPE
745
733
  key = child.key
746
734
  else:
@@ -751,9 +739,13 @@ def _get_children_info(children: list[VdomChild]) -> Sequence[_ChildInfo]:
751
739
  if key is None:
752
740
  key = index
753
741
 
742
+ if key in keys:
743
+ has_duplicates = True
744
+ keys.add(key)
745
+
754
746
  infos.append((child, child_type, key))
755
747
 
756
- return infos
748
+ return infos, None if has_duplicates else keys
757
749
 
758
750
 
759
751
  _ChildInfo: TypeAlias = tuple[Any, "_ElementType", Key]
reactpy/core/serve.py CHANGED
@@ -1,15 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Awaitable
3
+ from collections.abc import Awaitable, Callable
4
4
  from logging import getLogger
5
- from typing import Any, Callable
5
+ from typing import Any
6
6
 
7
7
  from anyio import create_task_group
8
8
  from anyio.abc import TaskGroup
9
9
 
10
10
  from reactpy.config import REACTPY_DEBUG
11
- from reactpy.core._life_cycle_hook import HOOK_STACK
12
- from reactpy.types import LayoutEventMessage, LayoutType, LayoutUpdateMessage
11
+ from reactpy.types import BaseLayout, LayoutEventMessage, LayoutUpdateMessage
13
12
 
14
13
  logger = getLogger(__name__)
15
14
 
@@ -25,7 +24,7 @@ The event will then trigger an :class:`reactpy.core.proto.EventHandlerType` in a
25
24
 
26
25
 
27
26
  async def serve_layout(
28
- layout: LayoutType[
27
+ layout: BaseLayout[
29
28
  LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
30
29
  ],
31
30
  send: SendCoroutine,
@@ -39,33 +38,29 @@ async def serve_layout(
39
38
 
40
39
 
41
40
  async def _single_outgoing_loop(
42
- layout: LayoutType[
41
+ layout: BaseLayout[
43
42
  LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
44
43
  ],
45
44
  send: SendCoroutine,
46
45
  ) -> None:
47
46
  while True:
48
- token = HOOK_STACK.initialize()
47
+ update = await layout.render()
49
48
  try:
50
- update = await layout.render()
51
- try:
52
- await send(update)
53
- except Exception: # nocov
54
- if not REACTPY_DEBUG.current:
55
- msg = (
56
- "Failed to send update. More info may be available "
57
- "if you enabling debug mode by setting "
58
- "`reactpy.config.REACTPY_DEBUG.current = True`."
59
- )
60
- logger.error(msg)
61
- raise
62
- finally:
63
- HOOK_STACK.reset(token)
49
+ await send(update)
50
+ except Exception: # nocov
51
+ if not REACTPY_DEBUG.current:
52
+ msg = (
53
+ "Failed to send update. More info may be available "
54
+ "if you enabling debug mode by setting "
55
+ "`reactpy.config.REACTPY_DEBUG.current = True`."
56
+ )
57
+ logger.error(msg)
58
+ raise
64
59
 
65
60
 
66
61
  async def _single_incoming_loop(
67
62
  task_group: TaskGroup,
68
- layout: LayoutType[
63
+ layout: BaseLayout[
69
64
  LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
70
65
  ],
71
66
  recv: RecvCoroutine,