22
33import abc
44import asyncio
5+ from collections import Counter
56from functools import wraps
67from logging import getLogger
78from typing import (
@@ -279,38 +280,25 @@ def _render_model_children(
279280 self ._unmount_model_states (list (old_state .children_by_key .values ()))
280281 return None
281282
282- DICT_TYPE , COMPONENT_TYPE , STRING_TYPE = 1 , 2 , 3 # noqa
283+ child_type_key_tuples = list ( _process_child_type_and_key ( raw_children ))
283284
284- raw_typed_children_by_key : Dict [str , Tuple [int , Any ]] = {}
285- for index , child in enumerate (raw_children ):
286- if isinstance (child , dict ):
287- child_type = DICT_TYPE
288- key = child .get ("key" ) or hex_id (child )
289- elif isinstance (child , AbstractComponent ):
290- child_type = COMPONENT_TYPE
291- key = getattr (child , "key" , None ) or hex_id (child )
292- else :
293- child = str (child )
294- child_type = STRING_TYPE
295- # The specific key doesn't matter here since we won't look it up - all
296- # we care about is that the key is unique, which object() can guarantee.
297- key = object ()
298- raw_typed_children_by_key [key ] = (child_type , child )
299-
300- if len (raw_typed_children_by_key ) != len (raw_children ):
301- raise ValueError (f"Duplicate keys in { raw_children } " )
302-
303- old_keys = set (old_state .children_by_key ).difference (raw_typed_children_by_key )
304- old_child_states = {key : old_state .children_by_key [key ] for key in old_keys }
285+ new_keys = {item [2 ] for item in child_type_key_tuples }
286+ if len (new_keys ) != len (raw_children ):
287+ key_counter = Counter (item [2 ] for item in child_type_key_tuples )
288+ duplicate_keys = [key for key , count in key_counter .items () if count > 1 ]
289+ raise ValueError (
290+ f"Duplicate keys { duplicate_keys } at { new_state .patch_path or '/' !r} "
291+ )
292+
293+ old_keys = set (old_state .children_by_key ).difference (new_keys )
305294 if old_keys :
306- self ._unmount_model_states (list (old_child_states .values ()))
295+ self ._unmount_model_states (
296+ [old_state .children_by_key [key ] for key in old_keys ]
297+ )
307298
308299 new_children = new_state .model ["children" ] = []
309- for index , (key , (child_type , child )) in (
310- # we can enumerate this because dict insertion order is preserved
311- enumerate (raw_typed_children_by_key .items ())
312- ):
313- if child_type is DICT_TYPE :
300+ for index , (child , child_type , key ) in enumerate (child_type_key_tuples ):
301+ if child_type is _DICT_TYPE :
314302 old_child_state = old_state .children_by_key .get (key )
315303 if old_child_state is not None :
316304 new_child_state = old_child_state .new (new_state , None )
@@ -319,7 +307,7 @@ def _render_model_children(
319307 self ._render_model (old_child_state , new_child_state , child )
320308 new_children .append (new_child_state .model )
321309 new_state .children_by_key [key ] = new_child_state
322- elif child_type is COMPONENT_TYPE :
310+ elif child_type is _COMPONENT_TYPE :
323311 old_child_state = old_state .children_by_key .get (key )
324312 if old_child_state is not None :
325313 new_child_state = old_child_state .new (new_state , child )
@@ -334,20 +322,20 @@ def _render_model_children_without_old_state(
334322 self , new_state : _ModelState , raw_children : List [Any ]
335323 ) -> None :
336324 new_children = new_state .model ["children" ] = []
337- for index , child in enumerate (raw_children ):
338- if isinstance (child , dict ):
339- key = child .get ("key" ) or hex_id (child )
325+ for index , (child , child_type , key ) in enumerate (
326+ _process_child_type_and_key (raw_children )
327+ ):
328+ if child_type is _DICT_TYPE :
340329 child_state = _ModelState (new_state , index , key , None )
341330 self ._render_model (None , child_state , child )
342331 new_children .append (child_state .model )
343332 new_state .children_by_key [key ] = child_state
344- elif isinstance (child , AbstractComponent ):
345- key = getattr (child , "key" , "" ) or hex_id (child )
333+ elif child_type is _COMPONENT_TYPE :
346334 life_cycle_hook = LifeCycleHook (child , self )
347335 child_state = _ModelState (new_state , index , key , life_cycle_hook )
348336 self ._render_component (None , child_state , child )
349337 else :
350- new_children .append (str ( child ) )
338+ new_children .append (child )
351339
352340 def _unmount_model_states (self , old_states : List [_ModelState ]) -> None :
353341 to_unmount = old_states [::- 1 ]
@@ -387,7 +375,7 @@ def __init__(
387375 self ,
388376 parent : Optional [_ModelState ],
389377 index : int ,
390- key : str ,
378+ key : Any ,
391379 life_cycle_hook : Optional [LifeCycleHook ],
392380 ) -> None :
393381 self .index = index
@@ -487,3 +475,30 @@ class _ModelVdomRequired(TypedDict, total=True):
487475
488476class _ModelVdom (_ModelVdomRequired , _ModelVdomOptional ):
489477 """A VDOM dictionary model specifically for use with a :class:`Layout`"""
478+
479+
480+ def _process_child_type_and_key (
481+ raw_children : List [Any ],
482+ ) -> Iterator [Tuple [Any , int , Any ]]:
483+ for index , child in enumerate (raw_children ):
484+ if isinstance (child , dict ):
485+ child_type = _DICT_TYPE
486+ key = child .get ("key" )
487+ elif isinstance (child , AbstractComponent ):
488+ child_type = _COMPONENT_TYPE
489+ key = getattr (child , "key" , None )
490+ else :
491+ child = f"{ child } "
492+ child_type = _STRING_TYPE
493+ key = None
494+
495+ if key is None :
496+ key = index
497+
498+ yield (child , child_type , key )
499+
500+
501+ # used in _process_child_type_and_key
502+ _DICT_TYPE = 1
503+ _COMPONENT_TYPE = 2
504+ _STRING_TYPE = 3
0 commit comments