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.
- reactpy/__init__.py +3 -2
- reactpy/_console/rewrite_props.py +2 -2
- reactpy/_html.py +11 -9
- reactpy/_option.py +2 -1
- reactpy/config.py +2 -2
- reactpy/core/_life_cycle_hook.py +12 -10
- 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 +193 -201
- reactpy/core/serve.py +17 -22
- reactpy/core/vdom.py +9 -12
- 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/reactjs/__init__.py +353 -0
- reactpy/reactjs/module.py +203 -0
- reactpy/reactjs/types.py +7 -0
- reactpy/reactjs/utils.py +183 -0
- reactpy/static/index-h31022cd.js +5 -0
- reactpy/static/index-h31022cd.js.map +11 -0
- reactpy/static/index-sbddj6ms.js +5 -0
- reactpy/static/index-sbddj6ms.js.map +10 -0
- reactpy/static/index-y71bxs88.js +5 -0
- reactpy/static/index-y71bxs88.js.map +10 -0
- reactpy/static/index.js +2 -2
- reactpy/static/index.js.map +6 -10
- reactpy/static/react-dom.js +4 -0
- reactpy/static/react-dom.js.map +11 -0
- reactpy/static/react-jsx-runtime.js +4 -0
- reactpy/static/react-jsx-runtime.js.map +9 -0
- reactpy/static/react.js +4 -0
- reactpy/static/react.js.map +10 -0
- reactpy/testing/backend.py +6 -5
- reactpy/testing/common.py +3 -5
- reactpy/testing/display.py +2 -1
- reactpy/testing/logs.py +1 -1
- reactpy/transforms.py +2 -2
- reactpy/types.py +117 -58
- reactpy/utils.py +8 -8
- reactpy/web/__init__.py +0 -6
- reactpy/web/module.py +37 -470
- reactpy/web/utils.py +2 -158
- reactpy/widgets.py +2 -2
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/METADATA +4 -7
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/RECORD +54 -39
- reactpy/web/templates/react.js +0 -61
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/WHEEL +0 -0
- {reactpy-2.0.0b4.dist-info → reactpy-2.0.0b6.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
202
|
-
await self.
|
|
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
|
-
|
|
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
|
-
|
|
287
|
+
parent: _ModelState,
|
|
288
|
+
index: int,
|
|
289
|
+
key: Any,
|
|
238
290
|
raw_model: Any,
|
|
239
|
-
) ->
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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,
|
|
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
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
47
|
+
update = await layout.render()
|
|
49
48
|
try:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
63
|
+
layout: BaseLayout[
|
|
69
64
|
LayoutUpdateMessage | dict[str, Any], LayoutEventMessage | dict[str, Any]
|
|
70
65
|
],
|
|
71
66
|
recv: RecvCoroutine,
|