1010 wait ,
1111)
1212from collections import Counter
13- from collections .abc import Callable , Sequence
13+ from collections .abc import Callable
1414from contextlib import AsyncExitStack , suppress
1515from logging import getLogger
1616from types import TracebackType
4646 LayoutEventMessage ,
4747 LayoutUpdateMessage ,
4848 VdomChild ,
49- VdomDict ,
5049 VdomJson ,
5150)
5251from reactpy .utils import Ref
@@ -146,11 +145,34 @@ async def _parallel_render(self) -> LayoutUpdateMessage:
146145 async def _create_layout_update (
147146 self , old_state : _ModelState
148147 ) -> LayoutUpdateMessage :
149- new_state = _copy_component_model_state (old_state )
150- component = new_state .life_cycle_state .component
148+ component = old_state .life_cycle_state .component
149+ try :
150+ parent : _ModelState | None = old_state .parent
151+ except AttributeError :
152+ parent = None
151153
152154 async with AsyncExitStack () as exit_stack :
153- await self ._render_component (exit_stack , old_state , new_state , component )
155+ new_state = await self ._render_component (
156+ exit_stack ,
157+ old_state ,
158+ parent ,
159+ old_state .index ,
160+ old_state .key ,
161+ component ,
162+ )
163+
164+ if parent is not None :
165+ parent .children_by_key [new_state .key ] = new_state
166+ old_parent_model = parent .model .current
167+ old_parent_children = old_parent_model .setdefault ("children" , [])
168+ parent .model .current = {
169+ ** old_parent_model ,
170+ "children" : [
171+ * old_parent_children [: new_state .index ],
172+ new_state .model .current ,
173+ * old_parent_children [new_state .index + 1 :],
174+ ],
175+ }
154176
155177 if REACTPY_CHECK_VDOM_SPEC .current :
156178 validate_vdom_json (new_state .model .current )
@@ -165,9 +187,40 @@ async def _render_component(
165187 self ,
166188 exit_stack : AsyncExitStack ,
167189 old_state : _ModelState | None ,
168- new_state : _ModelState ,
190+ parent : _ModelState | None ,
191+ index : int ,
192+ key : Any ,
169193 component : Component ,
170- ) -> None :
194+ ) -> _ModelState :
195+ if old_state is None :
196+ new_state = _make_component_model_state (
197+ parent , index , key , component , self ._schedule_render_task
198+ )
199+ elif (
200+ old_state .is_component_state
201+ and old_state .life_cycle_state .component .type != component .type
202+ ):
203+ await self ._unmount_model_states ([old_state ])
204+ new_state = _make_component_model_state (
205+ parent , index , key , component , self ._schedule_render_task
206+ )
207+ old_state = None
208+ elif not old_state .is_component_state :
209+ await self ._unmount_model_states ([old_state ])
210+ new_state = _make_component_model_state (
211+ parent , index , key , component , self ._schedule_render_task
212+ )
213+ old_state = None
214+ elif parent is None :
215+ new_state = _copy_component_model_state (old_state )
216+ new_state .life_cycle_state = _update_life_cycle_state (
217+ old_state .life_cycle_state , component
218+ )
219+ else :
220+ new_state = _update_component_model_state (
221+ old_state , parent , index , component , self ._schedule_render_task
222+ )
223+
171224 life_cycle_state = new_state .life_cycle_state
172225 life_cycle_hook = life_cycle_state .hook
173226
@@ -180,8 +233,10 @@ async def _render_component(
180233 # wrap the model in a fragment (i.e. tagName="") to ensure components have
181234 # a separate node in the model state tree. This could be removed if this
182235 # components are given a node in the tree some other way
183- wrapper_model = VdomDict (tagName = "" , children = [raw_model ])
184- await self ._render_model (exit_stack , old_state , new_state , wrapper_model )
236+ new_state .model .current = {"tagName" : "" }
237+ await self ._render_model_children (
238+ exit_stack , old_state , new_state , [raw_model ]
239+ )
185240 except Exception as error :
186241 logger .exception (f"Failed to render { component } " )
187242 new_state .model .current = {
@@ -193,32 +248,26 @@ async def _render_component(
193248 finally :
194249 await life_cycle_hook .affect_component_did_render ()
195250
196- try :
197- parent = new_state .parent
198- except AttributeError :
199- pass # only happens for root component
200- else :
201- key , index = new_state .key , new_state .index
202- parent .children_by_key [key ] = new_state
203- # need to add this model to parent's children without mutating parent model
204- old_parent_model = parent .model .current
205- old_parent_children = old_parent_model .setdefault ("children" , [])
206- parent .model .current = {
207- ** old_parent_model ,
208- "children" : [
209- * old_parent_children [:index ],
210- new_state .model .current ,
211- * old_parent_children [index + 1 :],
212- ],
213- }
251+ return new_state
214252
215253 async def _render_model (
216254 self ,
217255 exit_stack : AsyncExitStack ,
218256 old_state : _ModelState | None ,
219- new_state : _ModelState ,
257+ parent : _ModelState ,
258+ index : int ,
259+ key : Any ,
220260 raw_model : Any ,
221- ) -> None :
261+ ) -> _ModelState :
262+ if old_state is None :
263+ new_state = _make_element_model_state (parent , index , key )
264+ elif old_state .is_component_state :
265+ await self ._unmount_model_states ([old_state ])
266+ new_state = _make_element_model_state (parent , index , key )
267+ old_state = None
268+ else :
269+ new_state = _update_element_model_state (old_state , parent , index )
270+
222271 try :
223272 new_state .model .current = {"tagName" : raw_model ["tagName" ]}
224273 except Exception as e : # nocov
@@ -232,6 +281,7 @@ async def _render_model(
232281 await self ._render_model_children (
233282 exit_stack , old_state , new_state , raw_model .get ("children" , [])
234283 )
284+ return new_state
235285
236286 def _render_model_attributes (
237287 self ,
@@ -313,130 +363,48 @@ async def _render_model_children(
313363 else :
314364 raw_children = [raw_children ]
315365
316- if old_state is None :
317- if raw_children :
318- await self ._render_model_children_without_old_state (
319- exit_stack , new_state , raw_children
320- )
321- return None
322- elif not raw_children :
323- await self ._unmount_model_states (list (old_state .children_by_key .values ()))
324- return None
325-
326- children_info = _get_children_info (raw_children )
366+ children_info , new_keys = _get_children_info (raw_children )
327367
328- new_keys = {k for _ , _ , k in children_info }
329- if len (new_keys ) != len (children_info ):
368+ if new_keys is None :
330369 key_counter = Counter (item [2 ] for item in children_info )
331370 duplicate_keys = [key for key , count in key_counter .items () if count > 1 ]
332371 msg = f"Duplicate keys { duplicate_keys } at { new_state .patch_path or '/' !r} "
333372 raise ValueError (msg )
334373
335- old_keys = set (old_state .children_by_key ).difference (new_keys )
336- if old_keys :
337- await self ._unmount_model_states (
338- [old_state .children_by_key [key ] for key in old_keys ]
339- )
374+ if old_state is not None :
375+ old_keys = set (old_state .children_by_key ).difference (new_keys )
376+ if old_keys :
377+ await self ._unmount_model_states (
378+ [old_state .children_by_key [key ] for key in old_keys ]
379+ )
340380
341- new_state .model .current ["children" ] = []
342- for index , (child , child_type , key ) in enumerate (children_info ):
343- old_child_state = old_state .children_by_key .get (key )
344- if child_type is _DICT_TYPE :
345- old_child_state = old_state .children_by_key .get (key )
346- if old_child_state is None :
347- new_child_state = _make_element_model_state (
348- new_state ,
349- index ,
350- key ,
351- )
352- elif old_child_state .is_component_state :
353- await self ._unmount_model_states ([old_child_state ])
354- new_child_state = _make_element_model_state (
355- new_state ,
356- index ,
357- key ,
358- )
359- old_child_state = None
360- else :
361- new_child_state = _update_element_model_state (
362- old_child_state ,
363- new_state ,
364- index ,
365- )
366- await self ._render_model (
367- exit_stack , old_child_state , new_child_state , child
381+ if raw_children :
382+ new_state .model .current ["children" ] = []
383+ for index , (child , child_type , key ) in enumerate (children_info ):
384+ old_child_state = (
385+ old_state .children_by_key .get (key )
386+ if old_state is not None
387+ else None
368388 )
369- new_state .append_child (new_child_state .model .current )
370- new_state .children_by_key [key ] = new_child_state
371- elif child_type is _COMPONENT_TYPE :
372- child = cast (Component , child )
373- old_child_state = old_state .children_by_key .get (key )
374- if old_child_state is None :
375- new_child_state = _make_component_model_state (
376- new_state ,
377- index ,
378- key ,
379- child ,
380- self ._schedule_render_task ,
389+ if child_type is _DICT_TYPE :
390+ new_child_state = await self ._render_model (
391+ exit_stack , old_child_state , new_state , index , key , child
381392 )
382- elif old_child_state .is_component_state and (
383- old_child_state .life_cycle_state .component .type != child .type
384- ):
385- await self ._unmount_model_states ([old_child_state ])
386- old_child_state = None
387- new_child_state = _make_component_model_state (
388- new_state ,
389- index ,
390- key ,
391- child ,
392- self ._schedule_render_task ,
393+ elif child_type is _COMPONENT_TYPE :
394+ child = cast (Component , child )
395+ new_child_state = await self ._render_component (
396+ exit_stack , old_child_state , new_state , index , key , child
393397 )
394398 else :
395- new_child_state = _update_component_model_state (
396- old_child_state ,
397- new_state ,
398- index ,
399- child ,
400- self ._schedule_render_task ,
401- )
402- await self ._render_component (
403- exit_stack , old_child_state , new_child_state , child
404- )
405- else :
406- old_child_state = old_state .children_by_key .get (key )
407- if old_child_state is not None :
408- await self ._unmount_model_states ([old_child_state ])
409- new_state .append_child (child )
410-
411- async def _render_model_children_without_old_state (
412- self ,
413- exit_stack : AsyncExitStack ,
414- new_state : _ModelState ,
415- raw_children : list [Any ],
416- ) -> None :
417- children_info = _get_children_info (raw_children )
418-
419- new_keys = {k for _ , _ , k in children_info }
420- if len (new_keys ) != len (children_info ):
421- key_counter = Counter (k for _ , _ , k in children_info )
422- duplicate_keys = [key for key , count in key_counter .items () if count > 1 ]
423- msg = f"Duplicate keys { duplicate_keys } at { new_state .patch_path or '/' !r} "
424- raise ValueError (msg )
399+ if old_child_state is not None :
400+ await self ._unmount_model_states ([old_child_state ])
401+ new_child_state = child
425402
426- new_state .model .current ["children" ] = []
427- for index , (child , child_type , key ) in enumerate (children_info ):
428- if child_type is _DICT_TYPE :
429- child_state = _make_element_model_state (new_state , index , key )
430- await self ._render_model (exit_stack , None , child_state , child )
431- new_state .append_child (child_state .model .current )
432- new_state .children_by_key [key ] = child_state
433- elif child_type is _COMPONENT_TYPE :
434- child_state = _make_component_model_state (
435- new_state , index , key , child , self ._schedule_render_task
436- )
437- await self ._render_component (exit_stack , None , child_state , child )
438- else :
439- new_state .append_child (child )
403+ if isinstance (new_child_state , _ModelState ):
404+ new_state .append_child (new_child_state .model .current )
405+ new_state .children_by_key [key ] = new_child_state
406+ else :
407+ new_state .append_child (new_child_state )
440408
441409 async def _unmount_model_states (self , old_states : list [_ModelState ]) -> None :
442410 to_unmount = old_states [::- 1 ] # unmount in reversed order of rendering
@@ -488,7 +456,7 @@ def _new_root_model_state(
488456
489457
490458def _make_component_model_state (
491- parent : _ModelState ,
459+ parent : _ModelState | None ,
492460 index : int ,
493461 key : Any ,
494462 component : Component ,
@@ -499,7 +467,7 @@ def _make_component_model_state(
499467 index = index ,
500468 key = key ,
501469 model = Ref (),
502- patch_path = f"{ parent .patch_path } /children/{ index } " ,
470+ patch_path = f"{ parent .patch_path } /children/{ index } " if parent else "" ,
503471 children_by_key = {},
504472 targets_by_event = {},
505473 life_cycle_state = _make_life_cycle_state (component , schedule_render ),
@@ -714,8 +682,13 @@ async def get(self) -> _Type:
714682 return value
715683
716684
717- def _get_children_info (children : list [VdomChild ]) -> Sequence [_ChildInfo ]:
685+ def _get_children_info (
686+ children : list [VdomChild ],
687+ ) -> tuple [list [_ChildInfo ], set [Key ] | None ]:
718688 infos : list [_ChildInfo ] = []
689+ keys : set [Key ] = set ()
690+ has_duplicates = False
691+
719692 for index , child in enumerate (children ):
720693 if child is None :
721694 continue
@@ -733,9 +706,13 @@ def _get_children_info(children: list[VdomChild]) -> Sequence[_ChildInfo]:
733706 if key is None :
734707 key = index
735708
709+ if key in keys :
710+ has_duplicates = True
711+ keys .add (key )
712+
736713 infos .append ((child , child_type , key ))
737714
738- return infos
715+ return infos , None if has_duplicates else keys
739716
740717
741718_ChildInfo : TypeAlias = tuple [Any , "_ElementType" , Key ]
0 commit comments