Skip to content

Commit 657f806

Browse files
committed
test async effect
1 parent 52faa4f commit 657f806

File tree

3 files changed

+107
-61
lines changed

3 files changed

+107
-61
lines changed

docs/source/life-cycle-hooks.rst

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ the effect will only occur when the given state changes:
160160
Now a new connection will only be estalished if a new ``url`` is provided.
161161

162162

163+
Async Effects
164+
.............
165+
166+
under construction...
167+
168+
163169
**Supplementary Hooks**
164170
-----------------------
165171

@@ -272,18 +278,6 @@ hook alongside :ref:`use_effect` or in response to element event handlers.
272278
:ref:`The Game Snake` provides a good use case for ``use_ref``.
273279

274280

275-
**Unique Hooks**
276-
----------------
277-
278-
Hooks which are specific to IDOM and not present in React.
279-
280-
281-
use_async
282-
---------
283-
284-
...
285-
286-
287281
**Rules of Hooks**
288282
------------------
289283

idom/core/hooks.py

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
NamedTuple,
1515
List,
1616
overload,
17+
cast,
1718
)
1819
from typing_extensions import Protocol
1920

@@ -87,7 +88,9 @@ def dispatch(
8788

8889

8990
_EffectCleanFunc = Callable[[], None]
90-
_EffectApplyFunc = Callable[[], Optional[_EffectCleanFunc]]
91+
_SyncEffectFunc = Callable[[], Optional[_EffectCleanFunc]]
92+
_AsyncEffectFunc = Callable[[], Awaitable[Optional[_EffectCleanFunc]]]
93+
_EffectApplyFunc = Union[_SyncEffectFunc, _AsyncEffectFunc]
9194

9295

9396
@overload
@@ -124,10 +127,32 @@ def use_effect(
124127
memoize = use_memo(args=args)
125128

126129
def add_effect(function: _EffectApplyFunc) -> None:
130+
131+
if not asyncio.iscoroutinefunction(function):
132+
sync_function = cast(_SyncEffectFunc, function)
133+
else:
134+
135+
async_function = cast(_AsyncEffectFunc, function)
136+
137+
def sync_function() -> Optional[_EffectCleanFunc]:
138+
future = asyncio.ensure_future(async_function())
139+
140+
def clean_future() -> None:
141+
try:
142+
clean = future.result()
143+
except asyncio.InvalidStateError:
144+
future.cancel()
145+
else:
146+
if clean is not None:
147+
clean()
148+
149+
return clean_future
150+
127151
def effect() -> None:
128-
clean = function()
152+
clean = sync_function()
129153
if clean is not None:
130154
hook.add_effect("will_render will_unmount", clean)
155+
return None
131156

132157
return memoize(lambda: hook.add_effect("did_render", effect))
133158

@@ -313,53 +338,6 @@ def use_ref(initial_value: _StateType) -> Ref[_StateType]:
313338
return _use_const(lambda: Ref(initial_value))
314339

315340

316-
_AsyncCleanFunc = Callable[[], None]
317-
_AsyncApplyFunc = Callable[[], Awaitable[Optional[_EffectCleanFunc]]]
318-
319-
320-
@overload
321-
def use_async(
322-
function: None = None, args: Optional[Sequence[Any]] = None
323-
) -> Callable[[_AsyncApplyFunc], None]:
324-
...
325-
326-
327-
@overload
328-
def use_async(function: _AsyncApplyFunc, args: Optional[Sequence[Any]] = None) -> None:
329-
...
330-
331-
332-
def use_async(
333-
function: Optional[_AsyncApplyFunc] = None,
334-
args: Optional[Sequence[Any]] = None,
335-
) -> Optional[Callable[[_AsyncApplyFunc], None]]:
336-
effect = use_effect(args=args)
337-
338-
def add_asyc_effect(function: _AsyncApplyFunc) -> None:
339-
def create_future() -> Callable[[], None]:
340-
future = asyncio.ensure_future(function())
341-
342-
def cleanup_future() -> None:
343-
try:
344-
cleanup = future.result()
345-
except asyncio.InvalidStateError:
346-
future.cancel()
347-
else:
348-
if cleanup is not None:
349-
cleanup()
350-
351-
return cleanup_future
352-
353-
effect(create_future)
354-
return None
355-
356-
if function is not None:
357-
add_asyc_effect(function)
358-
return None
359-
else:
360-
return add_asyc_effect
361-
362-
363341
def _use_const(function: Callable[[], _StateType]) -> _StateType:
364342
return current_hook().use_state(function)
365343

tests/test_core/test_hooks.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import asyncio
23

34
import pytest
45

@@ -391,6 +392,79 @@ def effect():
391392
assert effect_run_count.current == 2
392393

393394

395+
async def test_use_async_effect():
396+
effect_ran = asyncio.Event()
397+
398+
@idom.element
399+
def ElementWithAsyncEffect():
400+
@idom.hooks.use_effect
401+
async def effect():
402+
effect_ran.set()
403+
404+
return idom.html.div()
405+
406+
async with idom.Layout(ElementWithAsyncEffect()) as layout:
407+
await layout.render()
408+
await effect_ran.wait()
409+
410+
411+
async def test_use_async_effect_cleanup():
412+
element_hook = HookCatcher()
413+
effect_ran = asyncio.Event()
414+
cleanup_ran = asyncio.Event()
415+
416+
@idom.element
417+
@element_hook.capture
418+
def ElementWithAsyncEffect():
419+
@idom.hooks.use_effect
420+
async def effect():
421+
effect_ran.set()
422+
return cleanup_ran.set
423+
424+
return idom.html.div()
425+
426+
async with idom.Layout(ElementWithAsyncEffect()) as layout:
427+
await layout.render()
428+
429+
await effect_ran.wait()
430+
element_hook.schedule_render()
431+
432+
await layout.render()
433+
434+
await asyncio.wait_for(cleanup_ran.wait(), 1)
435+
436+
437+
async def test_use_async_effect_cancel():
438+
element_hook = HookCatcher()
439+
effect_ran = asyncio.Event()
440+
effect_was_cancelled = asyncio.Event()
441+
442+
event_that_is_never_set = asyncio.Event()
443+
444+
@idom.element
445+
@element_hook.capture
446+
def ElementWithLongWaitingEffect():
447+
@idom.hooks.use_effect
448+
async def effect():
449+
effect_ran.set()
450+
try:
451+
await event_that_is_never_set.wait()
452+
except asyncio.CancelledError:
453+
effect_was_cancelled.set()
454+
455+
return idom.html.div()
456+
457+
async with idom.Layout(ElementWithLongWaitingEffect()) as layout:
458+
await layout.render()
459+
460+
await effect_ran.wait()
461+
element_hook.schedule_render()
462+
463+
await layout.render()
464+
465+
await asyncio.wait_for(effect_was_cancelled.wait(), 1)
466+
467+
394468
async def test_error_in_effect_is_gracefully_handled(caplog):
395469
@idom.element
396470
def ElementWithEffect():

0 commit comments

Comments
 (0)