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/__init__.py +1 -1
- reactpy/_console/rewrite_props.py +2 -2
- reactpy/_option.py +2 -1
- reactpy/config.py +2 -2
- reactpy/core/_life_cycle_hook.py +8 -9
- reactpy/core/_thread_local.py +2 -1
- reactpy/core/component.py +4 -38
- reactpy/core/events.py +61 -36
- reactpy/core/hooks.py +25 -35
- reactpy/core/layout.py +174 -187
- reactpy/core/serve.py +6 -6
- reactpy/core/vdom.py +6 -7
- reactpy/executors/asgi/__init__.py +9 -4
- reactpy/executors/asgi/middleware.py +1 -2
- reactpy/executors/asgi/pyscript.py +3 -7
- reactpy/executors/asgi/standalone.py +4 -6
- reactpy/executors/asgi/types.py +2 -2
- reactpy/pyscript/components.py +3 -3
- reactpy/pyscript/utils.py +49 -46
- reactpy/static/index.js +2 -2
- reactpy/static/index.js.map +4 -4
- reactpy/testing/backend.py +2 -1
- reactpy/testing/common.py +3 -5
- reactpy/types.py +100 -47
- reactpy/utils.py +7 -7
- reactpy/web/module.py +13 -10
- reactpy/widgets.py +2 -2
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b5.dist-info}/METADATA +4 -7
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b5.dist-info}/RECORD +32 -32
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b5.dist-info}/WHEEL +0 -0
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b5.dist-info}/entry_points.txt +0 -0
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b5.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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
76
|
-
msg = f"Expected a
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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(
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
202
|
-
await self.
|
|
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
|
-
|
|
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
|
-
|
|
283
|
+
parent: _ModelState,
|
|
284
|
+
index: int,
|
|
285
|
+
key: Any,
|
|
238
286
|
raw_model: Any,
|
|
239
|
-
) ->
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
414
|
-
old_child_state
|
|
415
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
2
|
-
from reactpy.executors.asgi.
|
|
3
|
-
from reactpy.executors.asgi.
|
|
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
|