Skip to content

Commit 6fa3fa2

Browse files
committed
fix tests
1 parent acda65b commit 6fa3fa2

File tree

4 files changed

+175
-67
lines changed

4 files changed

+175
-67
lines changed

src/py/reactpy/reactpy/core/hooks.py

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import inspect
5+
import warnings
46
from collections.abc import Coroutine, Sequence
57
from dataclasses import dataclass
68
from logging import getLogger
@@ -164,16 +166,47 @@ async def create_effect_task() -> _EffectInfo:
164166

165167

166168
def _cast_async_effect(function: Callable[..., Any]) -> _AsyncEffectFunc:
167-
if asyncio.iscoroutinefunction(function):
168-
return function
169+
if inspect.iscoroutinefunction(function):
170+
if len(inspect.signature(function).parameters):
171+
return function
169172

170-
async def wrapper(stop: asyncio.Event) -> None:
171-
cleanup = function()
172-
await stop.wait()
173-
if cleanup is not None:
174-
cleanup()
173+
warnings.warn(
174+
'Async effect functions should accept a "stop" asyncio.Event as their first argument',
175+
stacklevel=3,
176+
)
177+
178+
async def wrapper(stop: asyncio.Event) -> None:
179+
task = asyncio.create_task(function())
180+
await stop.wait()
181+
if not task.cancel():
182+
try:
183+
cleanup = await task
184+
except Exception:
185+
logger.exception("Error while applying effect")
186+
return
187+
if cleanup is not None:
188+
try:
189+
cleanup()
190+
except Exception:
191+
logger.exception("Error while cleaning up effect")
192+
193+
return wrapper
194+
else:
195+
196+
async def wrapper(stop: asyncio.Event) -> None:
197+
try:
198+
cleanup = function()
199+
except Exception:
200+
logger.exception("Error while applying effect")
201+
return
202+
await stop.wait()
203+
try:
204+
if cleanup is not None:
205+
cleanup()
206+
except Exception:
207+
logger.exception("Error while cleaning up effect")
175208

176-
return wrapper
209+
return wrapper
177210

178211

179212
def use_debug_value(

src/py/reactpy/tests/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
capture_reactpy_logs,
1616
clear_reactpy_web_modules_dir,
1717
)
18-
from tests.tooling.loop import open_event_loop
18+
from tests.tooling.concurrency import open_event_loop
1919

2020

2121
def pytest_addoption(parser: Parser) -> None:

src/py/reactpy/tests/test_core/test_hooks.py

+35-58
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@
55
import reactpy
66
from reactpy import html
77
from reactpy.config import REACTPY_DEBUG_MODE
8-
from reactpy.core.hooks import (
9-
COMPONENT_DID_RENDER_EFFECT,
10-
LifeCycleHook,
11-
current_hook,
12-
strictly_equal,
13-
)
8+
from reactpy.core.hooks import LifeCycleHook, strictly_equal
149
from reactpy.core.layout import Layout
1510
from reactpy.testing import DisplayFixture, HookCatcher, assert_reactpy_did_log, poll
1611
from reactpy.testing.logs import assert_reactpy_did_not_log
1712
from reactpy.utils import Ref
1813
from tests.tooling.common import DEFAULT_TYPE_DELAY, update_message
14+
from tests.tooling.concurrency import WaitForEvent
1915

2016

2117
async def test_must_be_rendering_in_layout_to_use_hooks():
@@ -327,15 +323,15 @@ def CheckNoEffectYet():
327323
async def test_use_effect_cleanup_occurs_before_next_effect():
328324
component_hook = HookCatcher()
329325
cleanup_triggered = reactpy.Ref(False)
330-
cleanup_triggered_before_next_effect = reactpy.Ref(False)
326+
cleanup_triggered_before_next_effect = WaitForEvent()
331327

332328
@reactpy.component
333329
@component_hook.capture
334330
def ComponentWithEffect():
335331
@reactpy.hooks.use_effect(dependencies=None)
336332
def effect():
337333
if cleanup_triggered.current:
338-
cleanup_triggered_before_next_effect.current = True
334+
cleanup_triggered_before_next_effect.set()
339335

340336
def cleanup():
341337
cleanup_triggered.current = True
@@ -353,7 +349,7 @@ def cleanup():
353349
await layout.render()
354350

355351
assert cleanup_triggered.current
356-
assert cleanup_triggered_before_next_effect.current
352+
await cleanup_triggered_before_next_effect.wait()
357353

358354

359355
async def test_use_effect_cleanup_occurs_on_will_unmount():
@@ -395,10 +391,11 @@ def cleanup():
395391
assert cleanup_triggered_before_next_render.current
396392

397393

398-
async def test_memoized_effect_on_recreated_if_dependencies_change():
394+
async def test_memoized_effect_is_recreated_if_dependencies_change():
399395
component_hook = HookCatcher()
400396
set_state_callback = reactpy.Ref(None)
401-
effect_run_count = reactpy.Ref(0)
397+
effect_ran = WaitForEvent()
398+
run_count = 0
402399

403400
first_value = 1
404401
second_value = 2
@@ -410,29 +407,31 @@ def ComponentWithMemoizedEffect():
410407

411408
@reactpy.hooks.use_effect(dependencies=[state])
412409
def effect():
413-
effect_run_count.current += 1
410+
nonlocal run_count
411+
effect_ran.set()
412+
run_count += 1
414413

415414
return reactpy.html.div()
416415

417416
async with reactpy.Layout(ComponentWithMemoizedEffect()) as layout:
418417
await layout.render()
419418

420-
assert effect_run_count.current == 1
419+
await effect_ran.wait()
420+
effect_ran.clear()
421421

422422
component_hook.latest.schedule_render()
423423
await layout.render()
424424

425-
assert effect_run_count.current == 1
426-
427425
set_state_callback.current(second_value)
428426
await layout.render()
429427

430-
assert effect_run_count.current == 2
428+
await effect_ran.wait()
429+
effect_ran.clear()
431430

432431
component_hook.latest.schedule_render()
433432
await layout.render()
434433

435-
assert effect_run_count.current == 2
434+
assert run_count == 2
436435

437436

438437
async def test_memoized_effect_cleanup_only_triggered_before_new_effect():
@@ -474,7 +473,7 @@ def cleanup():
474473

475474

476475
async def test_use_async_effect():
477-
effect_ran = asyncio.Event()
476+
effect_ran = WaitForEvent()
478477

479478
@reactpy.component
480479
def ComponentWithAsyncEffect():
@@ -486,13 +485,13 @@ async def effect():
486485

487486
async with reactpy.Layout(ComponentWithAsyncEffect()) as layout:
488487
await layout.render()
489-
await asyncio.wait_for(effect_ran.wait(), 1)
488+
await effect_ran.wait()
490489

491490

492491
async def test_use_async_effect_cleanup():
493492
component_hook = HookCatcher()
494-
effect_ran = asyncio.Event()
495-
cleanup_ran = asyncio.Event()
493+
effect_ran = WaitForEvent()
494+
cleanup_ran = WaitForEvent()
496495

497496
@reactpy.component
498497
@component_hook.capture
@@ -516,10 +515,10 @@ async def effect():
516515

517516
async def test_use_async_effect_cancel(caplog):
518517
component_hook = HookCatcher()
519-
effect_ran = asyncio.Event()
520-
effect_was_cancelled = asyncio.Event()
518+
effect_ran = WaitForEvent()
519+
effect_was_cancelled = WaitForEvent()
521520

522-
event_that_never_occurs = asyncio.Event()
521+
event_that_never_occurs = WaitForEvent()
523522

524523
@reactpy.component
525524
@component_hook.capture
@@ -562,7 +561,7 @@ def bad_effect():
562561

563562
return reactpy.html.div()
564563

565-
with assert_reactpy_did_log(match_message=r"Layout post-render effect .* failed"):
564+
with assert_reactpy_did_log(match_message=r"Error while applying effect"):
566565
async with reactpy.Layout(ComponentWithEffect()) as layout:
567566
await layout.render() # no error
568567

@@ -588,7 +587,7 @@ def bad_cleanup():
588587
return reactpy.html.div()
589588

590589
with assert_reactpy_did_log(
591-
match_message=r"Pre-unmount effect .*? failed",
590+
match_message=r"Error while cleaning up effect",
592591
error_type=ValueError,
593592
):
594593
async with reactpy.Layout(OuterComponent()) as layout:
@@ -845,7 +844,7 @@ def bad_callback():
845844

846845
async def test_use_effect_automatically_infers_closure_values():
847846
set_count = reactpy.Ref()
848-
did_effect = asyncio.Event()
847+
did_effect = WaitForEvent()
849848

850849
@reactpy.component
851850
def CounterWithEffect():
@@ -873,7 +872,7 @@ def some_effect_that_uses_count():
873872

874873
async def test_use_memo_automatically_infers_closure_values():
875874
set_count = reactpy.Ref()
876-
did_memo = asyncio.Event()
875+
did_memo = WaitForEvent()
877876

878877
@reactpy.component
879878
def CounterWithEffect():
@@ -1001,13 +1000,16 @@ async def test_error_in_layout_effect_cleanup_is_gracefully_handled():
10011000
def ComponentWithEffect():
10021001
@reactpy.hooks.use_effect(dependencies=None) # always run
10031002
def bad_effect():
1004-
msg = "The error message"
1005-
raise ValueError(msg)
1003+
def bad_cleanup():
1004+
msg = "The error message"
1005+
raise ValueError(msg)
1006+
1007+
return bad_cleanup
10061008

10071009
return reactpy.html.div()
10081010

10091011
with assert_reactpy_did_log(
1010-
match_message=r"post-render effect .*? failed",
1012+
match_message=r"Error while cleaning up effect",
10111013
error_type=ValueError,
10121014
match_error="The error message",
10131015
):
@@ -1211,12 +1213,12 @@ def incr_effect_count():
12111213

12121214
async with reactpy.Layout(SomeComponent()) as layout:
12131215
await layout.render()
1214-
assert effect_count.current == 1
1216+
await poll(lambda: effect_count.current).until_equals(1)
12151217
value.current = "string" # new string instance but same value
12161218
hook.latest.schedule_render()
12171219
await layout.render()
12181220
# effect does not trigger
1219-
assert effect_count.current == 1
1221+
await poll(lambda: effect_count.current).until_equals(1)
12201222

12211223

12221224
async def test_use_state_named_tuple():
@@ -1232,28 +1234,3 @@ def some_component():
12321234
state.current.set_value(2)
12331235
await layout.render()
12341236
assert state.current.value == 2
1235-
1236-
1237-
async def test_error_in_component_effect_cleanup_is_gracefully_handled():
1238-
component_hook = HookCatcher()
1239-
1240-
@reactpy.component
1241-
@component_hook.capture
1242-
def ComponentWithEffect():
1243-
hook = current_hook()
1244-
1245-
def bad_effect():
1246-
raise ValueError("The error message")
1247-
1248-
hook.add_effect(COMPONENT_DID_RENDER_EFFECT, bad_effect)
1249-
return reactpy.html.div()
1250-
1251-
with assert_reactpy_did_log(
1252-
match_message="Component post-render effect .*? failed",
1253-
error_type=ValueError,
1254-
match_error="The error message",
1255-
):
1256-
async with reactpy.Layout(ComponentWithEffect()) as layout:
1257-
await layout.render()
1258-
component_hook.latest.schedule_render()
1259-
await layout.render() # no error

0 commit comments

Comments
 (0)