reactpy 2.0.0b4__py3-none-any.whl → 2.0.0b5__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.
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,7 +28,6 @@ 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,
@@ -38,15 +37,16 @@ from reactpy.config import (
38
37
  from reactpy.core._life_cycle_hook import 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,20 +141,55 @@ 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
165
+ component = old_state.life_cycle_state.component
166
+ try:
167
+ parent: _ModelState | None = old_state.parent
168
+ except AttributeError:
169
+ parent = None
169
170
 
170
171
  async with AsyncExitStack() as exit_stack:
171
- await self._render_component(exit_stack, old_state, new_state, component)
172
+ new_state = await self._render_component(
173
+ exit_stack,
174
+ old_state,
175
+ parent,
176
+ old_state.index,
177
+ old_state.key,
178
+ component,
179
+ )
180
+
181
+ if parent is not None:
182
+ parent.children_by_key[new_state.key] = new_state
183
+ old_parent_model = parent.model.current
184
+ old_parent_children = old_parent_model.setdefault("children", [])
185
+ parent.model.current = {
186
+ **old_parent_model,
187
+ "children": [
188
+ *old_parent_children[: new_state.index],
189
+ new_state.model.current,
190
+ *old_parent_children[new_state.index + 1 :],
191
+ ],
192
+ }
172
193
 
173
194
  if REACTPY_CHECK_VDOM_SPEC.current:
174
195
  validate_vdom_json(new_state.model.current)
@@ -183,14 +204,54 @@ class Layout:
183
204
  self,
184
205
  exit_stack: AsyncExitStack,
185
206
  old_state: _ModelState | None,
186
- new_state: _ModelState,
187
- component: ComponentType,
188
- ) -> None:
207
+ parent: _ModelState | None,
208
+ index: int,
209
+ key: Any,
210
+ component: Component,
211
+ ) -> _ModelState:
212
+ if old_state is None:
213
+ new_state = _make_component_model_state(
214
+ parent, index, key, component, self._schedule_render_task
215
+ )
216
+ elif (
217
+ old_state.is_component_state
218
+ and old_state.life_cycle_state.component.type != component.type
219
+ ):
220
+ await self._unmount_model_states([old_state])
221
+ new_state = _make_component_model_state(
222
+ parent, index, key, component, self._schedule_render_task
223
+ )
224
+ old_state = None
225
+ elif not old_state.is_component_state:
226
+ await self._unmount_model_states([old_state])
227
+ new_state = _make_component_model_state(
228
+ parent, index, key, component, self._schedule_render_task
229
+ )
230
+ old_state = None
231
+ elif parent is None:
232
+ new_state = _copy_component_model_state(old_state)
233
+ new_state.life_cycle_state = _update_life_cycle_state(
234
+ old_state.life_cycle_state, component
235
+ )
236
+ else:
237
+ new_state = _update_component_model_state(
238
+ old_state, parent, index, component, self._schedule_render_task
239
+ )
240
+
189
241
  life_cycle_state = new_state.life_cycle_state
190
242
  life_cycle_hook = life_cycle_state.hook
191
243
 
192
244
  self._model_states_by_life_cycle_state_id[life_cycle_state.id] = new_state
193
245
 
246
+ # If this component is scheduled to render, we can cancel that task since we are
247
+ # rendering it now.
248
+ if life_cycle_state.id in self._render_tasks_by_id:
249
+ task = self._render_tasks_by_id[life_cycle_state.id]
250
+ if task is not current_task():
251
+ del self._render_tasks_by_id[life_cycle_state.id]
252
+ task.cancel()
253
+ self._render_tasks.discard(task)
254
+
194
255
  await life_cycle_hook.affect_component_will_render(component)
195
256
  exit_stack.push_async_callback(life_cycle_hook.affect_layout_did_render)
196
257
  try:
@@ -198,8 +259,10 @@ class Layout:
198
259
  # wrap the model in a fragment (i.e. tagName="") to ensure components have
199
260
  # a separate node in the model state tree. This could be removed if this
200
261
  # 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)
262
+ new_state.model.current = {"tagName": ""}
263
+ await self._render_model_children(
264
+ exit_stack, old_state, new_state, [raw_model]
265
+ )
203
266
  except Exception as error:
204
267
  logger.exception(f"Failed to render {component}")
205
268
  new_state.model.current = {
@@ -211,32 +274,26 @@ class Layout:
211
274
  finally:
212
275
  await life_cycle_hook.affect_component_did_render()
213
276
 
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
- }
277
+ return new_state
232
278
 
233
279
  async def _render_model(
234
280
  self,
235
281
  exit_stack: AsyncExitStack,
236
282
  old_state: _ModelState | None,
237
- new_state: _ModelState,
283
+ parent: _ModelState,
284
+ index: int,
285
+ key: Any,
238
286
  raw_model: Any,
239
- ) -> None:
287
+ ) -> _ModelState:
288
+ if old_state is None:
289
+ new_state = _make_element_model_state(parent, index, key)
290
+ elif old_state.is_component_state:
291
+ await self._unmount_model_states([old_state])
292
+ new_state = _make_element_model_state(parent, index, key)
293
+ old_state = None
294
+ else:
295
+ new_state = _update_element_model_state(old_state, parent, index)
296
+
240
297
  try:
241
298
  new_state.model.current = {"tagName": raw_model["tagName"]}
242
299
  except Exception as e: # nocov
@@ -250,6 +307,7 @@ class Layout:
250
307
  await self._render_model_children(
251
308
  exit_stack, old_state, new_state, raw_model.get("children", [])
252
309
  )
310
+ return new_state
253
311
 
254
312
  def _render_model_attributes(
255
313
  self,
@@ -331,130 +389,48 @@ class Layout:
331
389
  else:
332
390
  raw_children = [raw_children]
333
391
 
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)
392
+ children_info, new_keys = _get_children_info(raw_children)
345
393
 
346
- new_keys = {k for _, _, k in children_info}
347
- if len(new_keys) != len(children_info):
394
+ if new_keys is None:
348
395
  key_counter = Counter(item[2] for item in children_info)
349
396
  duplicate_keys = [key for key, count in key_counter.items() if count > 1]
350
397
  msg = f"Duplicate keys {duplicate_keys} at {new_state.patch_path or '/'!r}"
351
398
  raise ValueError(msg)
352
399
 
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
- )
400
+ if old_state is not None:
401
+ old_keys = set(old_state.children_by_key).difference(new_keys)
402
+ if old_keys:
403
+ await self._unmount_model_states(
404
+ [old_state.children_by_key[key] for key in old_keys]
405
+ )
358
406
 
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
407
+ if raw_children:
408
+ new_state.model.current["children"] = []
409
+ for index, (child, child_type, key) in enumerate(children_info):
410
+ old_child_state = (
411
+ old_state.children_by_key.get(key)
412
+ if old_state is not None
413
+ else None
386
414
  )
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,
415
+ if child_type is _DICT_TYPE:
416
+ new_child_state = await self._render_model(
417
+ exit_stack, old_child_state, new_state, index, key, child
399
418
  )
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,
419
+ elif child_type is _COMPONENT_TYPE:
420
+ child = cast(Component, child)
421
+ new_child_state = await self._render_component(
422
+ exit_stack, old_child_state, new_state, index, key, child
411
423
  )
412
424
  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)
425
+ if old_child_state is not None:
426
+ await self._unmount_model_states([old_child_state])
427
+ new_child_state = child
428
428
 
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)
429
+ if isinstance(new_child_state, _ModelState):
430
+ new_state.append_child(new_child_state.model.current)
431
+ new_state.children_by_key[key] = new_child_state
432
+ else:
433
+ new_state.append_child(new_child_state)
458
434
 
459
435
  async def _unmount_model_states(self, old_states: list[_ModelState]) -> None:
460
436
  to_unmount = old_states[::-1] # unmount in reversed order of rendering
@@ -483,7 +459,9 @@ class Layout:
483
459
  f"{lcs_id!r} - component already unmounted"
484
460
  )
485
461
  else:
486
- self._render_tasks.add(create_task(self._create_layout_update(model_state)))
462
+ task = create_task(self._create_layout_update(model_state))
463
+ self._render_tasks.add(task)
464
+ self._render_tasks_by_id[lcs_id] = task
487
465
  self._render_tasks_ready.release()
488
466
 
489
467
  def __repr__(self) -> str:
@@ -491,7 +469,7 @@ class Layout:
491
469
 
492
470
 
493
471
  def _new_root_model_state(
494
- component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None]
472
+ component: Component, schedule_render: Callable[[_LifeCycleStateId], None]
495
473
  ) -> _ModelState:
496
474
  return _ModelState(
497
475
  parent=None,
@@ -506,10 +484,10 @@ def _new_root_model_state(
506
484
 
507
485
 
508
486
  def _make_component_model_state(
509
- parent: _ModelState,
487
+ parent: _ModelState | None,
510
488
  index: int,
511
489
  key: Any,
512
- component: ComponentType,
490
+ component: Component,
513
491
  schedule_render: Callable[[_LifeCycleStateId], None],
514
492
  ) -> _ModelState:
515
493
  return _ModelState(
@@ -517,7 +495,7 @@ def _make_component_model_state(
517
495
  index=index,
518
496
  key=key,
519
497
  model=Ref(),
520
- patch_path=f"{parent.patch_path}/children/{index}",
498
+ patch_path=f"{parent.patch_path}/children/{index}" if parent else "",
521
499
  children_by_key={},
522
500
  targets_by_event={},
523
501
  life_cycle_state=_make_life_cycle_state(component, schedule_render),
@@ -547,7 +525,7 @@ def _update_component_model_state(
547
525
  old_model_state: _ModelState,
548
526
  new_parent: _ModelState,
549
527
  new_index: int,
550
- new_component: ComponentType,
528
+ new_component: Component,
551
529
  schedule_render: Callable[[_LifeCycleStateId], None],
552
530
  ) -> _ModelState:
553
531
  return _ModelState(
@@ -673,7 +651,7 @@ class _ModelState:
673
651
 
674
652
 
675
653
  def _make_life_cycle_state(
676
- component: ComponentType,
654
+ component: Component,
677
655
  schedule_render: Callable[[_LifeCycleStateId], None],
678
656
  ) -> _LifeCycleState:
679
657
  life_cycle_state_id = _LifeCycleStateId(uuid4().hex)
@@ -686,7 +664,7 @@ def _make_life_cycle_state(
686
664
 
687
665
  def _update_life_cycle_state(
688
666
  old_life_cycle_state: _LifeCycleState,
689
- new_component: ComponentType,
667
+ new_component: Component,
690
668
  ) -> _LifeCycleState:
691
669
  return _LifeCycleState(
692
670
  old_life_cycle_state.id,
@@ -708,7 +686,7 @@ class _LifeCycleState(NamedTuple):
708
686
  hook: LifeCycleHook
709
687
  """The life cycle hook"""
710
688
 
711
- component: ComponentType
689
+ component: Component
712
690
  """The current component instance"""
713
691
 
714
692
 
@@ -732,15 +710,20 @@ class _ThreadSafeQueue(Generic[_Type]):
732
710
  return value
733
711
 
734
712
 
735
- def _get_children_info(children: list[VdomChild]) -> Sequence[_ChildInfo]:
713
+ def _get_children_info(
714
+ children: list[VdomChild],
715
+ ) -> tuple[list[_ChildInfo], set[Key] | None]:
736
716
  infos: list[_ChildInfo] = []
717
+ keys: set[Key] = set()
718
+ has_duplicates = False
719
+
737
720
  for index, child in enumerate(children):
738
721
  if child is None:
739
722
  continue
740
723
  elif isinstance(child, dict):
741
724
  child_type = _DICT_TYPE
742
725
  key = child.get("key")
743
- elif isinstance(child, ComponentType):
726
+ elif isinstance(child, Component):
744
727
  child_type = _COMPONENT_TYPE
745
728
  key = child.key
746
729
  else:
@@ -751,9 +734,13 @@ def _get_children_info(children: list[VdomChild]) -> Sequence[_ChildInfo]:
751
734
  if key is None:
752
735
  key = index
753
736
 
737
+ if key in keys:
738
+ has_duplicates = True
739
+ keys.add(key)
740
+
754
741
  infos.append((child, child_type, key))
755
742
 
756
- return infos
743
+ return infos, None if has_duplicates else keys
757
744
 
758
745
 
759
746
  _ChildInfo: TypeAlias = tuple[Any, "_ElementType", Key]
reactpy/core/serve.py CHANGED
@@ -1,15 +1,15 @@
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
11
  from reactpy.core._life_cycle_hook import HOOK_STACK
12
- from reactpy.types import LayoutEventMessage, LayoutType, LayoutUpdateMessage
12
+ from reactpy.types import BaseLayout, LayoutEventMessage, LayoutUpdateMessage
13
13
 
14
14
  logger = getLogger(__name__)
15
15
 
@@ -25,7 +25,7 @@ The event will then trigger an :class:`reactpy.core.proto.EventHandlerType` in a
25
25
 
26
26
 
27
27
  async def serve_layout(
28
- layout: LayoutType[
28
+ layout: BaseLayout[
29
29
  LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
30
30
  ],
31
31
  send: SendCoroutine,
@@ -39,7 +39,7 @@ async def serve_layout(
39
39
 
40
40
 
41
41
  async def _single_outgoing_loop(
42
- layout: LayoutType[
42
+ layout: BaseLayout[
43
43
  LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
44
44
  ],
45
45
  send: SendCoroutine,
@@ -65,7 +65,7 @@ async def _single_outgoing_loop(
65
65
 
66
66
  async def _single_incoming_loop(
67
67
  task_group: TaskGroup,
68
- layout: LayoutType[
68
+ layout: BaseLayout[
69
69
  LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
70
70
  ],
71
71
  recv: RecvCoroutine,
reactpy/core/vdom.py CHANGED
@@ -3,10 +3,9 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  import re
6
- from collections.abc import Mapping, Sequence
6
+ from collections.abc import Callable, Mapping, Sequence
7
7
  from typing import (
8
8
  Any,
9
- Callable,
10
9
  cast,
11
10
  overload,
12
11
  )
@@ -18,11 +17,11 @@ from reactpy.config import REACTPY_CHECK_JSON_ATTRS, REACTPY_DEBUG
18
17
  from reactpy.core._f_back import f_module_name
19
18
  from reactpy.core.events import EventHandler, to_event_handler_function
20
19
  from reactpy.types import (
21
- ComponentType,
20
+ BaseEventHandler,
21
+ Component,
22
22
  CustomVdomConstructor,
23
23
  EllipsisRepr,
24
24
  EventHandlerDict,
25
- EventHandlerType,
26
25
  ImportSourceDict,
27
26
  InlineJavaScript,
28
27
  InlineJavaScriptDict,
@@ -232,13 +231,13 @@ def separate_attributes_handlers_and_inline_javascript(
232
231
  attributes: Mapping[str, Any],
233
232
  ) -> tuple[VdomAttributes, EventHandlerDict, InlineJavaScriptDict]:
234
233
  _attributes: VdomAttributes = {}
235
- _event_handlers: dict[str, EventHandlerType] = {}
234
+ _event_handlers: dict[str, BaseEventHandler] = {}
236
235
  _inline_javascript: dict[str, InlineJavaScript] = {}
237
236
 
238
237
  for k, v in attributes.items():
239
238
  if callable(v):
240
239
  _event_handlers[k] = EventHandler(to_event_handler_function(v))
241
- elif isinstance(v, EventHandler):
240
+ elif isinstance(v, BaseEventHandler):
242
241
  _event_handlers[k] = v
243
242
  elif EVENT_ATTRIBUTE_PATTERN.match(k) and isinstance(v, str):
244
243
  _inline_javascript[k] = InlineJavaScript(v)
@@ -276,7 +275,7 @@ def _validate_child_key_integrity(value: Any) -> None:
276
275
  )
277
276
  else:
278
277
  for child in value:
279
- if isinstance(child, ComponentType) and child.key is None:
278
+ if isinstance(child, Component) and child.key is None:
280
279
  warn(f"Key not specified for child in list {child}", UserWarning)
281
280
  elif isinstance(child, Mapping) and "key" not in child:
282
281
  # remove 'children' to reduce log spam
@@ -1,5 +1,10 @@
1
- from reactpy.executors.asgi.middleware import ReactPyMiddleware
2
- from reactpy.executors.asgi.pyscript import ReactPyCsr
3
- from reactpy.executors.asgi.standalone import ReactPy
1
+ try:
2
+ from reactpy.executors.asgi.middleware import ReactPyMiddleware
3
+ from reactpy.executors.asgi.pyscript import ReactPyCsr
4
+ from reactpy.executors.asgi.standalone import ReactPy
4
5
 
5
- __all__ = ["ReactPy", "ReactPyCsr", "ReactPyMiddleware"]
6
+ __all__ = ["ReactPy", "ReactPyCsr", "ReactPyMiddleware"]
7
+ except ModuleNotFoundError as e:
8
+ raise ModuleNotFoundError(
9
+ "ASGI executors require the 'reactpy[asgi]' extra to be installed."
10
+ ) from e