Skip to content

Commit b4e03d5

Browse files
committed
clean up index as default key logic
1 parent cb02b1b commit b4e03d5

File tree

3 files changed

+59
-41
lines changed

3 files changed

+59
-41
lines changed

src/idom/core/component.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import abc
44
import inspect
55
from functools import wraps
6-
from typing import Any, Callable, Dict, Tuple, Union
6+
from typing import Any, Callable, Dict, Optional, Tuple, Union
77

88
from .utils import hex_id
99
from .vdom import VdomDict
@@ -22,7 +22,7 @@ def component(function: ComponentRenderFunction) -> Callable[..., "Component"]:
2222
"""
2323

2424
@wraps(function)
25-
def constructor(*args: Any, key: str = "", **kwargs: Any) -> Component:
25+
def constructor(*args: Any, key: Optional[Any] = None, **kwargs: Any) -> Component:
2626
return Component(function, key, args, kwargs)
2727

2828
return constructor
@@ -49,15 +49,15 @@ class Component(AbstractComponent):
4949
def __init__(
5050
self,
5151
function: ComponentRenderFunction,
52-
key: str,
52+
key: Optional[Any],
5353
args: Tuple[Any, ...],
5454
kwargs: Dict[str, Any],
5555
) -> None:
5656
self.key = key
5757
self._function = function
5858
self._args = args
5959
self._kwargs = kwargs
60-
if key:
60+
if key is not None:
6161
kwargs["key"] = key
6262

6363
def render(self) -> VdomDict:
@@ -66,6 +66,9 @@ def render(self) -> VdomDict:
6666
model = {"tagName": "div", "children": [model]}
6767
return model
6868

69+
def __eq__(self, other: Any) -> bool:
70+
return isinstance(other, Component) and other._func == self._func
71+
6972
def __repr__(self) -> str:
7073
sig = inspect.signature(self._function)
7174
try:

src/idom/core/layout.py

+51-36
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import abc
44
import asyncio
5+
from collections import Counter
56
from functools import wraps
67
from logging import getLogger
78
from 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

488476
class _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

tests/test_core/test_layout.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ def ComponentReturnsDuplicateKeys():
443443
async with idom.Layout(ComponentReturnsDuplicateKeys()) as layout:
444444
await layout.render()
445445

446-
with pytest.raises(ValueError, match="Duplicate keys in"):
446+
with pytest.raises(ValueError, match=r"Duplicate keys \['duplicate'\] at '/'"):
447447
raise next(iter(caplog.records)).exc_info[1]
448448

449449

0 commit comments

Comments
 (0)