2
2
3
3
import abc
4
4
import asyncio
5
+ from collections import Counter
5
6
from functools import wraps
6
7
from logging import getLogger
7
8
from typing import (
@@ -279,38 +280,25 @@ def _render_model_children(
279
280
self ._unmount_model_states (list (old_state .children_by_key .values ()))
280
281
return None
281
282
282
- DICT_TYPE , COMPONENT_TYPE , STRING_TYPE = 1 , 2 , 3 # noqa
283
+ child_type_key_tuples = list ( _process_child_type_and_key ( raw_children ))
283
284
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 )
305
294
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
+ )
307
298
308
299
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 :
314
302
old_child_state = old_state .children_by_key .get (key )
315
303
if old_child_state is not None :
316
304
new_child_state = old_child_state .new (new_state , None )
@@ -319,7 +307,7 @@ def _render_model_children(
319
307
self ._render_model (old_child_state , new_child_state , child )
320
308
new_children .append (new_child_state .model )
321
309
new_state .children_by_key [key ] = new_child_state
322
- elif child_type is COMPONENT_TYPE :
310
+ elif child_type is _COMPONENT_TYPE :
323
311
old_child_state = old_state .children_by_key .get (key )
324
312
if old_child_state is not None :
325
313
new_child_state = old_child_state .new (new_state , child )
@@ -334,20 +322,20 @@ def _render_model_children_without_old_state(
334
322
self , new_state : _ModelState , raw_children : List [Any ]
335
323
) -> None :
336
324
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 :
340
329
child_state = _ModelState (new_state , index , key , None )
341
330
self ._render_model (None , child_state , child )
342
331
new_children .append (child_state .model )
343
332
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 :
346
334
life_cycle_hook = LifeCycleHook (child , self )
347
335
child_state = _ModelState (new_state , index , key , life_cycle_hook )
348
336
self ._render_component (None , child_state , child )
349
337
else :
350
- new_children .append (str ( child ) )
338
+ new_children .append (child )
351
339
352
340
def _unmount_model_states (self , old_states : List [_ModelState ]) -> None :
353
341
to_unmount = old_states [::- 1 ]
@@ -387,7 +375,7 @@ def __init__(
387
375
self ,
388
376
parent : Optional [_ModelState ],
389
377
index : int ,
390
- key : str ,
378
+ key : Any ,
391
379
life_cycle_hook : Optional [LifeCycleHook ],
392
380
) -> None :
393
381
self .index = index
@@ -487,3 +475,30 @@ class _ModelVdomRequired(TypedDict, total=True):
487
475
488
476
class _ModelVdom (_ModelVdomRequired , _ModelVdomOptional ):
489
477
"""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