Skip to content

Commit 896771c

Browse files
committed
add tests for coverage
1 parent 009cea2 commit 896771c

File tree

6 files changed

+145
-53
lines changed

6 files changed

+145
-53
lines changed

src/idom/core/component.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,65 +8,60 @@
88

99

1010
def component(
11-
function: Callable[..., Union[ComponentType, VdomDict]]
11+
function: Callable[..., Union[ComponentType, VdomDict | None]]
1212
) -> Callable[..., "Component"]:
13-
"""A decorator for defining an :class:`Component`.
13+
"""A decorator for defining a new component.
1414
1515
Parameters:
16-
function: The function that will render a :class:`VdomDict`.
16+
function: The component's :meth:`idom.core.proto.ComponentType.render` function.
1717
"""
1818
sig = inspect.signature(function)
19-
key_is_kwarg = "key" in sig.parameters and sig.parameters["key"].kind in (
19+
20+
if "key" in sig.parameters and sig.parameters["key"].kind in (
2021
inspect.Parameter.KEYWORD_ONLY,
2122
inspect.Parameter.POSITIONAL_OR_KEYWORD,
22-
)
23-
if key_is_kwarg:
23+
):
2424
raise TypeError(
2525
f"Component render function {function} uses reserved parameter 'key'"
2626
)
2727

2828
@wraps(function)
2929
def constructor(*args: Any, key: Optional[Any] = None, **kwargs: Any) -> Component:
30-
if key_is_kwarg:
31-
kwargs["key"] = key
32-
return Component(function, key, args, kwargs)
30+
return Component(function, key, args, kwargs, sig)
3331

3432
return constructor
3533

3634

3735
class Component:
3836
"""An object for rending component models."""
3937

40-
__slots__ = "__weakref__", "_func", "_args", "_kwargs", "key"
38+
__slots__ = "__weakref__", "_func", "_args", "_kwargs", "_sig", "key", "type"
4139

4240
def __init__(
4341
self,
4442
function: Callable[..., Union[ComponentType, VdomDict]],
4543
key: Optional[Any],
4644
args: Tuple[Any, ...],
4745
kwargs: Dict[str, Any],
46+
sig: inspect.Signature,
4847
) -> None:
48+
self.key = key
49+
self.type = function
4950
self._args = args
50-
self._func = function
5151
self._kwargs = kwargs
52-
self.key = key
53-
54-
@property
55-
def definition_id(self) -> int:
56-
return id(self._func)
52+
self._sig = sig
5753

58-
def render(self) -> VdomDict:
59-
return self._func(*self._args, **self._kwargs)
54+
def render(self) -> VdomDict | ComponentType | None:
55+
return self.type(*self._args, **self._kwargs)
6056

6157
def __repr__(self) -> str:
62-
sig = inspect.signature(self._func)
6358
try:
64-
args = sig.bind(*self._args, **self._kwargs).arguments
59+
args = self._sig.bind(*self._args, **self._kwargs).arguments
6560
except TypeError:
66-
return f"{self._func.__name__}(...)"
61+
return f"{self.type.__name__}(...)"
6762
else:
6863
items = ", ".join(f"{k}={v!r}" for k, v in args.items())
6964
if items:
70-
return f"{self._func.__name__}({id(self):02x}, {items})"
65+
return f"{self.type.__name__}({id(self):02x}, {items})"
7166
else:
72-
return f"{self._func.__name__}({id(self):02x})"
67+
return f"{self.type.__name__}({id(self):02x})"

src/idom/core/layout.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Set,
1919
Tuple,
2020
TypeVar,
21+
cast,
2122
)
2223
from uuid import uuid4
2324
from weakref import ref as weakref
@@ -203,12 +204,11 @@ def _render_component(
203204
# wrap the model in a fragment (i.e. tagName="") to ensure components have
204205
# a separate node in the model state tree. This could be removed if this
205206
# components are given a node in the tree some other way
206-
raw_model = {
207-
"tagName": "",
208-
"children": [] if raw_model is None else [raw_model],
209-
}
207+
wrapper_model = {"tagName": ""}
208+
if raw_model is not None:
209+
wrapper_model["children"] = [raw_model]
210210

211-
self._render_model(old_state, new_state, raw_model)
211+
self._render_model(old_state, new_state, wrapper_model)
212212
except Exception as error:
213213
logger.exception(f"Failed to render {component}")
214214
new_state.model.current = {
@@ -242,15 +242,6 @@ def _render_model(
242242
new_state.key = new_state.model.current["key"] = raw_model["key"]
243243
if "importSource" in raw_model:
244244
new_state.model.current["importSource"] = raw_model["importSource"]
245-
246-
if old_state is not None and old_state.key != new_state.key:
247-
self._unmount_model_states([old_state])
248-
if new_state.is_component_state:
249-
self._model_states_by_life_cycle_state_id[
250-
new_state.life_cycle_state.id
251-
] = new_state
252-
old_state = None
253-
254245
self._render_model_attributes(old_state, new_state, raw_model)
255246
self._render_model_children(old_state, new_state, raw_model.get("children", []))
256247

@@ -380,6 +371,7 @@ def _render_model_children(
380371
new_children.append(new_child_state.model.current)
381372
new_state.children_by_key[key] = new_child_state
382373
elif child_type is _COMPONENT_TYPE:
374+
child = cast(ComponentType, child)
383375
old_child_state = old_state.children_by_key.get(key)
384376
if old_child_state is None:
385377
new_child_state = _make_component_model_state(
@@ -390,8 +382,7 @@ def _render_model_children(
390382
self._rendering_queue.put,
391383
)
392384
elif old_child_state.is_component_state and (
393-
old_child_state.life_cycle_state.component.definition_id
394-
!= child.definition_id
385+
old_child_state.life_cycle_state.component.type != child.type
395386
):
396387
self._unmount_model_states([old_child_state])
397388
old_child_state = None

src/idom/core/proto.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@ class ComponentType(Protocol):
3232
key: Key | None
3333
"""An identifier which is unique amongst a component's immediate siblings"""
3434

35-
@property
36-
def definition_id(self) -> int:
37-
"""A globally unique identifier for this component definition.
35+
type: type[Any] | Callable[..., Any]
36+
"""The function or class defining the behavior of this component
3837
39-
Usually the :func:`id` of this class or an underlying function.
40-
"""
38+
This is used to see if two component instances share the same definition.
39+
"""
4140

4241
def render(self) -> VdomDict | ComponentType | None:
43-
"""Render the component's :class:`VdomDict`."""
42+
"""Render the component's view model."""
4443

4544

4645
_Self = TypeVar("_Self")

src/idom/html.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def _(*children: Any) -> VdomDict:
168168
"""An HTML fragment - this element will not appear in the DOM"""
169169
attributes, children = coalesce_attributes_and_children(children)
170170
if attributes:
171-
raise ValueError("Fragments cannot have attributes")
171+
raise TypeError("Fragments cannot have attributes")
172172
return {"tagName": "", "children": children}
173173

174174

tests/test_core/test_layout.py

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
from idom.core.component import component
1414
from idom.core.dispatcher import render_json_patch
1515
from idom.core.hooks import use_effect, use_state
16-
from idom.core.layout import LayoutEvent
16+
from idom.core.layout import Layout, LayoutEvent
1717
from idom.testing import (
1818
HookCatcher,
1919
StaticEventHandler,
2020
assert_idom_logged,
2121
capture_idom_logs,
2222
)
23+
from idom.utils import Ref
2324
from tests.assert_utils import assert_same_items
2425

2526

@@ -96,6 +97,15 @@ def SimpleComponent():
9697
]
9798

9899

100+
async def test_component_can_return_none():
101+
@idom.component
102+
def SomeComponent():
103+
return None
104+
105+
with idom.Layout(SomeComponent()) as layout:
106+
assert (await layout.render()).new == {"tagName": ""}
107+
108+
99109
async def test_nested_component_layout():
100110
parent_set_state = idom.Ref(None)
101111
child_set_state = idom.Ref(None)
@@ -716,14 +726,18 @@ def HasNestedEventHandler():
716726

717727
async def test_duplicate_sibling_keys_causes_error(caplog):
718728
hook = HookCatcher()
729+
should_error = True
719730

720731
@idom.component
721732
@hook.capture
722733
def ComponentReturnsDuplicateKeys():
723-
return idom.html.div(
724-
idom.html.div(key="duplicate"),
725-
idom.html.div(key="duplicate"),
726-
)
734+
if should_error:
735+
return idom.html.div(
736+
idom.html.div(key="duplicate"),
737+
idom.html.div(key="duplicate"),
738+
)
739+
else:
740+
return idom.html.div()
727741

728742
with idom.Layout(ComponentReturnsDuplicateKeys()) as layout:
729743
with assert_idom_logged(
@@ -735,6 +749,11 @@ def ComponentReturnsDuplicateKeys():
735749

736750
hook.latest.schedule_render()
737751

752+
should_error = False
753+
await layout.render()
754+
755+
should_error = True
756+
hook.latest.schedule_render()
738757
with assert_idom_logged(
739758
error_type=ValueError,
740759
match_error=r"Duplicate keys \['duplicate'\] at '/children/0'",
@@ -838,9 +857,9 @@ def use_toggle():
838857
def Root():
839858
toggle, set_toggle.current = use_toggle()
840859
if toggle:
841-
return idom.html.div(SomeComponent("x"))
860+
return SomeComponent("x")
842861
else:
843-
return idom.html.div(idom.html.div(SomeComponent("y")))
862+
return idom.html.div(SomeComponent("y"))
844863

845864
@idom.component
846865
def SomeComponent(name):
@@ -1063,3 +1082,82 @@ async def record_if_state_is_reset():
10631082
await layout.render()
10641083
assert effect_calls_without_state == ["some-key", "key-0"]
10651084
did_call_effect.clear()
1085+
1086+
1087+
async def test_changing_key_of_component_resets_state():
1088+
set_key = Ref()
1089+
did_init_state = Ref(0)
1090+
hook = HookCatcher()
1091+
1092+
@component
1093+
@hook.capture
1094+
def Root():
1095+
key, set_key.current = use_state("key-1")
1096+
return Child(key=key)
1097+
1098+
@component
1099+
def Child():
1100+
use_state(lambda: did_init_state.set_current(did_init_state.current + 1))
1101+
1102+
with Layout(Root()) as layout:
1103+
await layout.render()
1104+
assert did_init_state.current == 1
1105+
1106+
set_key.current("key-2")
1107+
await layout.render()
1108+
assert did_init_state.current == 2
1109+
1110+
hook.latest.schedule_render()
1111+
await layout.render()
1112+
assert did_init_state.current == 2
1113+
1114+
1115+
async def test_changing_event_handlers_in_the_next_render():
1116+
set_event_name = Ref()
1117+
event_handler = StaticEventHandler()
1118+
did_trigger = Ref(False)
1119+
1120+
@component
1121+
def Root():
1122+
event_name, set_event_name.current = use_state("first")
1123+
return html.button(
1124+
{event_name: event_handler.use(lambda: did_trigger.set_current(True))}
1125+
)
1126+
1127+
with Layout(Root()) as layout:
1128+
await layout.render()
1129+
await layout.deliver(LayoutEvent(event_handler.target, []))
1130+
assert did_trigger.current
1131+
did_trigger.current = False
1132+
1133+
set_event_name.current("second")
1134+
await layout.render()
1135+
await layout.deliver(LayoutEvent(event_handler.target, []))
1136+
assert did_trigger.current
1137+
did_trigger.current = False
1138+
1139+
1140+
async def test_change_element_to_string_causes_unmount():
1141+
set_toggle = Ref()
1142+
did_unmount = Ref(False)
1143+
1144+
@component
1145+
def Root():
1146+
toggle, set_toggle.current = use_toggle(True)
1147+
if toggle:
1148+
return html.div(Child())
1149+
else:
1150+
return html.div("some-string")
1151+
1152+
@component
1153+
def Child():
1154+
use_effect(lambda: lambda: did_unmount.set_current(True))
1155+
1156+
with Layout(Root()) as layout:
1157+
await layout.render()
1158+
1159+
set_toggle.current()
1160+
1161+
await layout.render()
1162+
1163+
assert did_unmount.current

tests/test_html.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,12 @@ def test_script_may_only_have_one_child():
142142
def test_child_of_script_must_be_string():
143143
with pytest.raises(ValueError, match="The child of a 'script' must be a string"):
144144
html.script(1)
145+
146+
147+
def test_simple_fragment():
148+
assert html._(1, 2, 3) == {"tagName": "", "children": [1, 2, 3]}
149+
150+
151+
def test_fragment_can_have_no_attributes():
152+
with pytest.raises(TypeError, match="Fragments cannot have attributes"):
153+
html._({"some-attribute": 1})

0 commit comments

Comments
 (0)