Skip to content

Commit 7bebc4d

Browse files
authoredJul 14, 2022
Track contexts in hooks as state (#787)
* fix use_context components which are newly added to the layout after the initial render do not have access to contexts. we solve this by tracking life cycle hooks in stack and copying the context providers from parent to child hooks. we also change how contexts are implemented - instead of needing a create_context function which returns a new Context class, we just return a Context object that constructs a ContextProvider component. the earlier implementation was too clever and did not add anything for it. * add test * add changelog entry * fix mypy * fix tornado dev server * remove unused func * improve test * remove unused method * fix style issues * get clever with functions * fix tests * update changelog * get coverage
1 parent 4500d55 commit 7bebc4d

File tree

12 files changed

+184
-145
lines changed

12 files changed

+184
-145
lines changed
 

‎docs/source/about/changelog.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,19 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
2323
Unreleased
2424
----------
2525

26-
**Added**
26+
**Fixed**
2727

28-
- :pull:`123` - ``asgiref`` as a dependency
28+
- :issue:`789` - Conditionally rendered components cannot use contexts
2929

3030
**Changed**
3131

3232
- :pull:`123` - set default timeout on playwright page for testing
33+
- :pull:`787` - Track contexts in hooks as state
34+
- :pull:`787` - remove non-standard ``name`` argument from ``create_context``
35+
36+
**Added**
37+
38+
- :pull:`123` - ``asgiref`` as a dependency
3339

3440

3541
v0.39.0

‎src/idom/backend/flask.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@
3737

3838
logger = logging.getLogger(__name__)
3939

40-
ConnectionContext: type[Context[Connection | None]] = create_context(
41-
None, "ConnectionContext"
42-
)
40+
ConnectionContext: Context[Connection | None] = create_context(None)
4341

4442

4543
def configure(

‎src/idom/backend/sanic.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@
3232

3333
logger = logging.getLogger(__name__)
3434

35-
ConnectionContext: type[Context[Connection | None]] = create_context(
36-
None, "ConnectionContext"
37-
)
35+
ConnectionContext: Context[Connection | None] = create_context(None)
3836

3937

4038
def configure(

‎src/idom/backend/starlette.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030

3131
logger = logging.getLogger(__name__)
3232

33-
WebSocketContext: type[Context[WebSocket | None]] = create_context(
34-
None, "WebSocketContext"
35-
)
33+
WebSocketContext: Context[WebSocket | None] = create_context(None)
3634

3735

3836
def configure(

‎src/idom/backend/tornado.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
from .utils import CLIENT_BUILD_DIR, safe_client_build_dir_path
2727

2828

29-
ConnectionContext: type[Context[Connection | None]] = create_context(
30-
None, "ConnectionContext"
31-
)
29+
ConnectionContext: Context[Connection | None] = create_context(None)
3230

3331

3432
def configure(
@@ -67,8 +65,7 @@ async def serve_development_app(
6765
) -> None:
6866
enable_pretty_logging()
6967

70-
# setup up tornado to use asyncio
71-
AsyncIOMainLoop().install()
68+
AsyncIOMainLoop.current().install()
7269

7370
server = HTTPServer(app)
7471
server.listen(port, host)

‎src/idom/core/_f_back.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
from types import FrameType
5+
6+
7+
def f_module_name(index: int = 0) -> str:
8+
frame = f_back(index + 1)
9+
if frame is None:
10+
return "" # pragma: no cover
11+
name = frame.f_globals.get("__name__", "")
12+
assert isinstance(name, str), "Expected module name to be a string"
13+
return name
14+
15+
16+
def f_back(index: int = 0) -> FrameType | None:
17+
frame = inspect.currentframe()
18+
while frame is not None:
19+
if index < 0:
20+
return frame
21+
frame = frame.f_back
22+
index -= 1
23+
return None # pragma: no cover

‎src/idom/core/_thread_local.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,3 @@ def get(self) -> _StateType:
2020
else:
2121
state = self._state[thread]
2222
return state
23-
24-
def set(self, state: _StateType) -> None:
25-
self._state[current_thread()] = state

‎src/idom/core/hooks.py

Lines changed: 78 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
Any,
99
Awaitable,
1010
Callable,
11-
ClassVar,
1211
Dict,
1312
Generic,
1413
List,
@@ -239,108 +238,94 @@ def use_debug_value(
239238
logger.debug(f"{current_hook().component} {new}")
240239

241240

242-
def create_context(
243-
default_value: _StateType, name: str | None = None
244-
) -> type[Context[_StateType]]:
241+
def create_context(default_value: _StateType) -> Context[_StateType]:
245242
"""Return a new context type for use in :func:`use_context`"""
246243

247-
class _Context(Context[_StateType]):
248-
_default_value = default_value
244+
def context(
245+
*children: Any,
246+
value: _StateType = default_value,
247+
key: Key | None = None,
248+
) -> ContextProvider[_StateType]:
249+
return ContextProvider(
250+
*children,
251+
value=value,
252+
key=key,
253+
type=context,
254+
)
255+
256+
context.__qualname__ = "context"
257+
258+
return context
249259

250-
_Context.__name__ = name or "Context"
251260

252-
return _Context
261+
class Context(Protocol[_StateType]):
262+
"""Returns a :class:`ContextProvider` component"""
253263

264+
def __call__(
265+
self,
266+
*children: Any,
267+
value: _StateType = ...,
268+
key: Key | None = ...,
269+
) -> ContextProvider[_StateType]:
270+
...
254271

255-
def use_context(context_type: type[Context[_StateType]]) -> _StateType:
272+
273+
def use_context(context: Context[_StateType]) -> _StateType:
256274
"""Get the current value for the given context type.
257275
258276
See the full :ref:`Use Context` docs for more information.
259277
"""
260-
# We have to use a Ref here since, if initially context_type._current is None, and
261-
# then on a subsequent render it is present, we need to be able to dynamically adopt
262-
# that newly present current context. When we update it though, we don't need to
263-
# schedule a new render since we're already rending right now. Thus we can't do this
264-
# with use_state() since we'd incur an extra render when calling set_state.
265-
context_ref: Ref[Context[_StateType] | None] = use_ref(None)
266-
267-
if context_ref.current is None:
268-
provided_context = context_type._current.get()
269-
if provided_context is None:
270-
# Cast required because of: https://github.com/python/mypy/issues/5144
271-
return cast(_StateType, context_type._default_value)
272-
context_ref.current = provided_context
273-
274-
# We need the hook now so that we can schedule an update when
275278
hook = current_hook()
279+
provider = hook.get_context_provider(context)
280+
281+
if provider is None:
282+
# force type checker to realize this is just a normal function
283+
assert isinstance(context, FunctionType), f"{context} is not a Context"
284+
# __kwdefault__ can be None if no kwarg only parameters exist
285+
assert context.__kwdefaults__ is not None, f"{context} has no 'value' kwarg"
286+
# lastly check that 'value' kwarg exists
287+
assert "value" in context.__kwdefaults__, f"{context} has no 'value' kwarg"
288+
# then we can safely access the context's default value
289+
return cast(_StateType, context.__kwdefaults__["value"])
276290

277-
context = context_ref.current
291+
subscribers = provider._subscribers
278292

279293
@use_effect
280294
def subscribe_to_context_change() -> Callable[[], None]:
281-
def set_context(new: Context[_StateType]) -> None:
282-
# We don't need to check if `new is not context_ref.current` because we only
283-
# trigger this callback when the value of a context, and thus the context
284-
# itself changes. Therefore we can always schedule a render.
285-
context_ref.current = new
286-
hook.schedule_render()
287-
288-
context.subscribers.add(set_context)
289-
return lambda: context.subscribers.remove(set_context)
290-
291-
return context.value
292-
295+
subscribers.add(hook)
296+
return lambda: subscribers.remove(hook)
293297

294-
_UNDEFINED: Any = object()
298+
return provider._value
295299

296300

297-
class Context(Generic[_StateType]):
298-
299-
# This should be _StateType instead of Any, but it can't due to this limitation:
300-
# https://github.com/python/mypy/issues/5144
301-
_default_value: ClassVar[Any]
302-
303-
_current: ClassVar[ThreadLocal[Context[Any] | None]]
304-
305-
def __init_subclass__(cls) -> None:
306-
# every context type tracks which of its instances are currently in use
307-
cls._current = ThreadLocal(lambda: None)
308-
301+
class ContextProvider(Generic[_StateType]):
309302
def __init__(
310303
self,
311304
*children: Any,
312-
value: _StateType = _UNDEFINED,
313-
key: Key | None = None,
305+
value: _StateType,
306+
key: Key | None,
307+
type: Context[_StateType],
314308
) -> None:
315309
self.children = children
316-
self.value: _StateType = self._default_value if value is _UNDEFINED else value
317310
self.key = key
318-
self.subscribers: set[Callable[[Context[_StateType]], None]] = set()
319-
self.type = self.__class__
311+
self.type = type
312+
self._subscribers: set[LifeCycleHook] = set()
313+
self._value = value
320314

321315
def render(self) -> VdomDict:
322-
current_ctx = self.__class__._current
323-
324-
prior_ctx = current_ctx.get()
325-
current_ctx.set(self)
326-
327-
def reset_ctx() -> None:
328-
current_ctx.set(prior_ctx)
329-
330-
current_hook().add_effect(COMPONENT_DID_RENDER_EFFECT, reset_ctx)
331-
316+
current_hook().set_context_provider(self)
332317
return vdom("", *self.children)
333318

334-
def should_render(self, new: Context[_StateType]) -> bool:
335-
if self.value is not new.value:
336-
new.subscribers.update(self.subscribers)
337-
for set_context in self.subscribers:
338-
set_context(new)
319+
def should_render(self, new: ContextProvider[_StateType]) -> bool:
320+
if self._value is not new._value:
321+
for hook in self._subscribers:
322+
hook.set_context_provider(new)
323+
hook.schedule_render()
339324
return True
340325
return False
341326

342327
def __repr__(self) -> str:
343-
return f"{type(self).__name__}({id(self)})"
328+
return f"{type(self).__name__}({self.type})"
344329

345330

346331
_ActionType = TypeVar("_ActionType")
@@ -558,14 +543,14 @@ def _try_to_infer_closure_values(
558543

559544
def current_hook() -> LifeCycleHook:
560545
"""Get the current :class:`LifeCycleHook`"""
561-
hook = _current_hook.get()
562-
if hook is None:
546+
hook_stack = _hook_stack.get()
547+
if not hook_stack:
563548
msg = "No life cycle hook is active. Are you rendering in a layout?"
564549
raise RuntimeError(msg)
565-
return hook
550+
return hook_stack[-1]
566551

567552

568-
_current_hook: ThreadLocal[LifeCycleHook | None] = ThreadLocal(lambda: None)
553+
_hook_stack: ThreadLocal[list[LifeCycleHook]] = ThreadLocal(list)
569554

570555

571556
EffectType = NewType("EffectType", str)
@@ -630,9 +615,8 @@ class LifeCycleHook:
630615
631616
hook.affect_component_did_render()
632617
633-
# This should only be called after any child components yielded by
634-
# component_instance.render() have also been rendered because effects of
635-
# this type must run after the full set of changes have been resolved.
618+
# This should only be called after the full set of changes associated with a
619+
# given render have been completed.
636620
hook.affect_layout_did_render()
637621
638622
# Typically an event occurs and a new render is scheduled, thus begining
@@ -650,6 +634,7 @@ class LifeCycleHook:
650634

651635
__slots__ = (
652636
"__weakref__",
637+
"_context_providers",
653638
"_current_state_index",
654639
"_event_effects",
655640
"_is_rendering",
@@ -666,6 +651,7 @@ def __init__(
666651
self,
667652
schedule_render: Callable[[], None],
668653
) -> None:
654+
self._context_providers: dict[Context[Any], ContextProvider[Any]] = {}
669655
self._schedule_render_callback = schedule_render
670656
self._schedule_render_later = False
671657
self._is_rendering = False
@@ -700,6 +686,14 @@ def add_effect(self, effect_type: EffectType, function: Callable[[], None]) -> N
700686
"""Trigger a function on the occurance of the given effect type"""
701687
self._event_effects[effect_type].append(function)
702688

689+
def set_context_provider(self, provider: ContextProvider[Any]) -> None:
690+
self._context_providers[provider.type] = provider
691+
692+
def get_context_provider(
693+
self, context: Context[_StateType]
694+
) -> ContextProvider[_StateType] | None:
695+
return self._context_providers.get(context)
696+
703697
def affect_component_will_render(self, component: ComponentType) -> None:
704698
"""The component is about to render"""
705699
self.component = component
@@ -753,13 +747,16 @@ def set_current(self) -> None:
753747
This method is called by a layout before entering the render method
754748
of this hook's associated component.
755749
"""
756-
_current_hook.set(self)
750+
hook_stack = _hook_stack.get()
751+
if hook_stack:
752+
parent = hook_stack[-1]
753+
self._context_providers.update(parent._context_providers)
754+
hook_stack.append(self)
757755

758756
def unset_current(self) -> None:
759757
"""Unset this hook as the active hook in this thread"""
760758
# this assertion should never fail - primarilly useful for debug
761-
assert _current_hook.get() is self
762-
_current_hook.set(None)
759+
assert _hook_stack.get().pop() is self
763760

764761
def _schedule_render(self) -> None:
765762
try:

‎src/idom/core/layout.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import abc
44
import asyncio
55
from collections import Counter
6+
from contextlib import ExitStack
67
from functools import wraps
78
from logging import getLogger
89
from typing import (
@@ -158,14 +159,10 @@ async def render(self) -> LayoutUpdate:
158159

159160
def _create_layout_update(self, old_state: _ModelState) -> LayoutUpdate:
160161
new_state = _copy_component_model_state(old_state)
161-
162162
component = new_state.life_cycle_state.component
163-
self._render_component(old_state, new_state, component)
164163

165-
# hook effects must run after the update is complete
166-
for model_state in _iter_model_state_children(new_state):
167-
if model_state.is_component_state:
168-
model_state.life_cycle_state.hook.affect_layout_did_render()
164+
with ExitStack() as exit_stack:
165+
self._render_component(exit_stack, old_state, new_state, component)
169166

170167
old_model: Optional[VdomJson]
171168
try:
@@ -181,6 +178,7 @@ def _create_layout_update(self, old_state: _ModelState) -> LayoutUpdate:
181178

182179
def _render_component(
183180
self,
181+
exit_stack: ExitStack,
184182
old_state: Optional[_ModelState],
185183
new_state: _ModelState,
186184
component: ComponentType,
@@ -200,19 +198,17 @@ def _render_component(
200198
else:
201199
life_cycle_hook = life_cycle_state.hook
202200
life_cycle_hook.affect_component_will_render(component)
201+
exit_stack.callback(life_cycle_hook.affect_layout_did_render)
202+
life_cycle_hook.set_current()
203203
try:
204-
life_cycle_hook.set_current()
205-
try:
206-
raw_model = component.render()
207-
finally:
208-
life_cycle_hook.unset_current()
204+
raw_model = component.render()
209205
# wrap the model in a fragment (i.e. tagName="") to ensure components have
210206
# a separate node in the model state tree. This could be removed if this
211207
# components are given a node in the tree some other way
212208
wrapper_model: VdomDict = {"tagName": ""}
213209
if raw_model is not None:
214210
wrapper_model["children"] = [raw_model]
215-
self._render_model(old_state, new_state, wrapper_model)
211+
self._render_model(exit_stack, old_state, new_state, wrapper_model)
216212
except Exception as error:
217213
logger.exception(f"Failed to render {component}")
218214
new_state.model.current = {
@@ -224,6 +220,7 @@ def _render_component(
224220
),
225221
}
226222
finally:
223+
life_cycle_hook.unset_current()
227224
life_cycle_hook.affect_component_did_render()
228225

229226
try:
@@ -243,6 +240,7 @@ def _render_component(
243240

244241
def _render_model(
245242
self,
243+
exit_stack: ExitStack,
246244
old_state: Optional[_ModelState],
247245
new_state: _ModelState,
248246
raw_model: Any,
@@ -253,7 +251,9 @@ def _render_model(
253251
if "importSource" in raw_model:
254252
new_state.model.current["importSource"] = raw_model["importSource"]
255253
self._render_model_attributes(old_state, new_state, raw_model)
256-
self._render_model_children(old_state, new_state, raw_model.get("children", []))
254+
self._render_model_children(
255+
exit_stack, old_state, new_state, raw_model.get("children", [])
256+
)
257257

258258
def _render_model_attributes(
259259
self,
@@ -320,6 +320,7 @@ def _render_model_event_handlers_without_old_state(
320320

321321
def _render_model_children(
322322
self,
323+
exit_stack: ExitStack,
323324
old_state: Optional[_ModelState],
324325
new_state: _ModelState,
325326
raw_children: Any,
@@ -329,7 +330,9 @@ def _render_model_children(
329330

330331
if old_state is None:
331332
if raw_children:
332-
self._render_model_children_without_old_state(new_state, raw_children)
333+
self._render_model_children_without_old_state(
334+
exit_stack, new_state, raw_children
335+
)
333336
return None
334337
elif not raw_children:
335338
self._unmount_model_states(list(old_state.children_by_key.values()))
@@ -377,7 +380,7 @@ def _render_model_children(
377380
new_state,
378381
index,
379382
)
380-
self._render_model(old_child_state, new_child_state, child)
383+
self._render_model(exit_stack, old_child_state, new_child_state, child)
381384
new_children.append(new_child_state.model.current)
382385
new_state.children_by_key[key] = new_child_state
383386
elif child_type is _COMPONENT_TYPE:
@@ -411,15 +414,20 @@ def _render_model_children(
411414
child,
412415
self._rendering_queue.put,
413416
)
414-
self._render_component(old_child_state, new_child_state, child)
417+
self._render_component(
418+
exit_stack, old_child_state, new_child_state, child
419+
)
415420
else:
416421
old_child_state = old_state.children_by_key.get(key)
417422
if old_child_state is not None:
418423
self._unmount_model_states([old_child_state])
419424
new_children.append(child)
420425

421426
def _render_model_children_without_old_state(
422-
self, new_state: _ModelState, raw_children: List[Any]
427+
self,
428+
exit_stack: ExitStack,
429+
new_state: _ModelState,
430+
raw_children: List[Any],
423431
) -> None:
424432
child_type_key_tuples = list(_process_child_type_and_key(raw_children))
425433

@@ -435,14 +443,14 @@ def _render_model_children_without_old_state(
435443
for index, (child, child_type, key) in enumerate(child_type_key_tuples):
436444
if child_type is _DICT_TYPE:
437445
child_state = _make_element_model_state(new_state, index, key)
438-
self._render_model(None, child_state, child)
446+
self._render_model(exit_stack, None, child_state, child)
439447
new_children.append(child_state.model.current)
440448
new_state.children_by_key[key] = child_state
441449
elif child_type is _COMPONENT_TYPE:
442450
child_state = _make_component_model_state(
443451
new_state, index, key, child, self._rendering_queue.put
444452
)
445-
self._render_component(None, child_state, child)
453+
self._render_component(exit_stack, None, child_state, child)
446454
else:
447455
new_children.append(child)
448456

@@ -473,12 +481,6 @@ def _check_should_render(old: ComponentType, new: ComponentType) -> bool:
473481
return False
474482

475483

476-
def _iter_model_state_children(model_state: _ModelState) -> Iterator[_ModelState]:
477-
yield model_state
478-
for child in model_state.children_by_key.values():
479-
yield from _iter_model_state_children(child)
480-
481-
482484
def _new_root_model_state(
483485
component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None]
484486
) -> _ModelState:

‎src/idom/core/vdom.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import inspect
43
import logging
54
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, cast
65

@@ -23,6 +22,8 @@
2322
VdomJson,
2423
)
2524

25+
from ._f_back import f_module_name
26+
2627

2728
logger = logging.getLogger()
2829

@@ -223,13 +224,10 @@ def constructor(
223224
"element represented by a :class:`VdomDict`."
224225
)
225226

226-
frame = inspect.currentframe()
227-
if frame is not None and frame.f_back is not None and frame.f_back is not None:
228-
module = frame.f_back.f_globals.get("__name__") # module in outer frame
229-
if module is not None:
230-
qualname = module + "." + tag
231-
constructor.__module__ = module
232-
constructor.__qualname__ = qualname
227+
module_name = f_module_name(1)
228+
if module_name:
229+
constructor.__module__ = module_name
230+
constructor.__qualname__ = f"{module_name}.{tag}"
233231

234232
return constructor
235233

‎tests/test_core/test_hooks.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
import re
32

43
import pytest
54

@@ -935,11 +934,8 @@ def ComponentUsesContext():
935934

936935

937936
def test_context_repr():
938-
Context = idom.create_context(None)
939-
assert re.match(r"Context\(.*\)", repr(Context()))
940-
941-
MyContext = idom.create_context(None, name="MyContext")
942-
assert re.match(r"MyContext\(.*\)", repr(MyContext()))
937+
sample_context = idom.create_context(None)
938+
assert repr(sample_context()) == f"ContextProvider({sample_context})"
943939

944940

945941
async def test_use_context_only_renders_for_value_change():
@@ -1068,8 +1064,8 @@ def Inner():
10681064

10691065

10701066
async def test_neighboring_contexts_do_not_conflict():
1071-
LeftContext = idom.create_context(None, name="Left")
1072-
RightContext = idom.create_context(None, name="Right")
1067+
LeftContext = idom.create_context(None)
1068+
RightContext = idom.create_context(None)
10731069

10741070
set_left = idom.Ref()
10751071
set_right = idom.Ref()
@@ -1247,3 +1243,32 @@ def SomeComponent():
12471243

12481244
with assert_idom_did_not_log(r"SomeComponent\(.*?\) message is 'bye'"):
12491245
await layout.render()
1246+
1247+
1248+
async def test_conditionally_rendered_components_can_use_context():
1249+
set_state = idom.Ref()
1250+
used_context_values = []
1251+
some_context = idom.create_context(None)
1252+
1253+
@idom.component
1254+
def SomeComponent():
1255+
state, set_state.current = idom.use_state(True)
1256+
if state:
1257+
return FirstCondition()
1258+
else:
1259+
return SecondCondition()
1260+
1261+
@idom.component
1262+
def FirstCondition():
1263+
used_context_values.append(idom.use_context(some_context) + "-1")
1264+
1265+
@idom.component
1266+
def SecondCondition():
1267+
used_context_values.append(idom.use_context(some_context) + "-2")
1268+
1269+
async with idom.Layout(some_context(SomeComponent(), value="the-value")) as layout:
1270+
await layout.render()
1271+
assert used_context_values == ["the-value-1"]
1272+
set_state.current(False)
1273+
await layout.render()
1274+
assert used_context_values == ["the-value-1", "the-value-2"]

‎tests/test_core/test_layout.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,7 +1027,7 @@ async def test_element_keys_inside_components_do_not_reset_state_of_component():
10271027
reset in any `Child()` components but there was a bug where that happened.
10281028
"""
10291029

1030-
effect_calls_without_state = []
1030+
effect_calls_without_state = set()
10311031
set_child_key_num = StaticEventHandler()
10321032
did_call_effect = asyncio.Event()
10331033

@@ -1051,7 +1051,7 @@ def Child(child_key):
10511051
async def record_if_state_is_reset():
10521052
if state:
10531053
return
1054-
effect_calls_without_state.append(child_key)
1054+
effect_calls_without_state.add(child_key)
10551055
set_state(1)
10561056
did_call_effect.set()
10571057

@@ -1063,13 +1063,13 @@ async def record_if_state_is_reset():
10631063
async with idom.Layout(Parent()) as layout:
10641064
await layout.render()
10651065
await did_call_effect.wait()
1066-
assert effect_calls_without_state == ["some-key", "key-0"]
1066+
assert effect_calls_without_state == {"some-key", "key-0"}
10671067
did_call_effect.clear()
10681068

10691069
for i in range(1, 5):
10701070
await layout.deliver(LayoutEvent(set_child_key_num.target, []))
10711071
await layout.render()
1072-
assert effect_calls_without_state == ["some-key", "key-0"]
1072+
assert effect_calls_without_state == {"some-key", "key-0"}
10731073
did_call_effect.clear()
10741074

10751075

0 commit comments

Comments
 (0)
Please sign in to comment.