Skip to content

Commit bb85f37

Browse files
committed
automatically infer closure arguments
1 parent a0c3740 commit bb85f37

File tree

2 files changed

+77
-16
lines changed

2 files changed

+77
-16
lines changed

src/idom/core/hooks.py

+38-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import asyncio
99
from logging import getLogger
1010
from threading import get_ident as get_thread_id
11+
from types import FunctionType
1112
from typing import (
1213
Any,
1314
Awaitable,
@@ -101,21 +102,23 @@ def dispatch(
101102

102103
@overload
103104
def use_effect(
104-
function: None = None, args: Optional[Sequence[Any]] = None
105+
function: None = None,
106+
args: Sequence[Any] | "ellipsis" | None = ...,
105107
) -> Callable[[_EffectApplyFunc], None]:
106108
...
107109

108110

109111
@overload
110112
def use_effect(
111-
function: _EffectApplyFunc, args: Optional[Sequence[Any]] = None
113+
function: _EffectApplyFunc,
114+
args: Sequence[Any] | "ellipsis" | None = ...,
112115
) -> None:
113116
...
114117

115118

116119
def use_effect(
117120
function: Optional[_EffectApplyFunc] = None,
118-
args: Optional[Sequence[Any]] = None,
121+
args: Sequence[Any] | "ellipsis" | None = ...,
119122
) -> Optional[Callable[[_EffectApplyFunc], None]]:
120123
"""See the full :ref:`Use Effect` docs for details
121124
@@ -130,6 +133,8 @@ def use_effect(
130133
If not function is provided, a decorator. Otherwise ``None``.
131134
"""
132135
hook = current_hook()
136+
137+
args = _try_to_infer_closure_args(function, args)
133138
memoize = use_memo(args=args)
134139
last_clean_callback: Ref[Optional[_EffectCleanFunc]] = use_ref(None)
135140

@@ -209,21 +214,23 @@ def dispatch(action: _ActionType) -> None:
209214

210215
@overload
211216
def use_callback(
212-
function: None = None, args: Optional[Sequence[Any]] = None
217+
function: None = None,
218+
args: Sequence[Any] | "ellipsis" | None = ...,
213219
) -> Callable[[_CallbackFunc], _CallbackFunc]:
214220
...
215221

216222

217223
@overload
218224
def use_callback(
219-
function: _CallbackFunc, args: Optional[Sequence[Any]] = None
225+
function: _CallbackFunc,
226+
args: Sequence[Any] | "ellipsis" | None = ...,
220227
) -> _CallbackFunc:
221228
...
222229

223230

224231
def use_callback(
225232
function: Optional[_CallbackFunc] = None,
226-
args: Optional[Sequence[Any]] = (),
233+
args: Sequence[Any] | "ellipsis" | None = ...,
227234
) -> Union[_CallbackFunc, Callable[[_CallbackFunc], _CallbackFunc]]:
228235
"""See the full :ref:`Use Callback` docs for details
229236
@@ -234,6 +241,7 @@ def use_callback(
234241
Returns:
235242
The current function
236243
"""
244+
args = _try_to_infer_closure_args(function, args)
237245
memoize = use_memo(args=args)
238246

239247
def setup(function: _CallbackFunc) -> _CallbackFunc:
@@ -254,21 +262,23 @@ def __call__(self, func: Callable[[], _StateType]) -> _StateType:
254262

255263
@overload
256264
def use_memo(
257-
function: None = None, args: Optional[Sequence[Any]] = None
265+
function: None = None,
266+
args: Sequence[Any] | "ellipsis" | None = ...,
258267
) -> _LambdaCaller:
259268
...
260269

261270

262271
@overload
263272
def use_memo(
264-
function: Callable[[], _StateType], args: Optional[Sequence[Any]] = None
273+
function: Callable[[], _StateType],
274+
args: Sequence[Any] | "ellipsis" | None = ...,
265275
) -> _StateType:
266276
...
267277

268278

269279
def use_memo(
270280
function: Optional[Callable[[], _StateType]] = None,
271-
args: Optional[Sequence[Any]] = None,
281+
args: Sequence[Any] | "ellipsis" | None = ...,
272282
) -> Union[_StateType, Callable[[Callable[[], _StateType]], _StateType]]:
273283
"""See the full :ref:`Use Memo` docs for details
274284
@@ -279,6 +289,8 @@ def use_memo(
279289
Returns:
280290
The current state
281291
"""
292+
args = _try_to_infer_closure_args(function, args)
293+
282294
memo: _Memo[_StateType] = _use_const(_Memo)
283295

284296
if memo.empty():
@@ -350,6 +362,23 @@ def _use_const(function: Callable[[], _StateType]) -> _StateType:
350362
return current_hook().use_state(function)
351363

352364

365+
def _try_to_infer_closure_args(
366+
func: Callable[..., Any] | None,
367+
args: Sequence[Any] | "ellipsis" | None,
368+
) -> Sequence[Any] | None:
369+
if args is ...:
370+
if isinstance(func, FunctionType):
371+
return (
372+
[cell.cell_contents for cell in func.__closure__]
373+
if func.__closure__
374+
else []
375+
)
376+
else:
377+
return None
378+
else:
379+
return cast("Sequence[Any] | None", args)
380+
381+
353382
_current_life_cycle_hook: Dict[int, "LifeCycleHook"] = {}
354383

355384

tests/test_core/test_hooks.py

+39-7
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ async def test_use_effect_cleanup_occurs_before_next_effect():
331331
@idom.component
332332
@component_hook.capture
333333
def ComponentWithEffect():
334-
@idom.hooks.use_effect
334+
@idom.hooks.use_effect(args=None)
335335
def effect():
336336
if cleanup_triggered.current:
337337
cleanup_triggered_before_next_effect.current = True
@@ -485,7 +485,7 @@ async def effect():
485485

486486
with idom.Layout(ComponentWithAsyncEffect()) as layout:
487487
await layout.render()
488-
await effect_ran.wait()
488+
await asyncio.wait_for(effect_ran.wait(), 1)
489489

490490

491491
async def test_use_async_effect_cleanup():
@@ -496,7 +496,7 @@ async def test_use_async_effect_cleanup():
496496
@idom.component
497497
@component_hook.capture
498498
def ComponentWithAsyncEffect():
499-
@idom.hooks.use_effect
499+
@idom.hooks.use_effect(args=None) # force this to run every time
500500
async def effect():
501501
effect_ran.set()
502502
return cleanup_ran.set
@@ -506,7 +506,7 @@ async def effect():
506506
with idom.Layout(ComponentWithAsyncEffect()) as layout:
507507
await layout.render()
508508

509-
await effect_ran.wait()
509+
cleanup_ran.wait()
510510
component_hook.latest.schedule_render()
511511

512512
await layout.render()
@@ -524,7 +524,7 @@ async def test_use_async_effect_cancel(caplog):
524524
@idom.component
525525
@component_hook.capture
526526
def ComponentWithLongWaitingEffect():
527-
@idom.hooks.use_effect
527+
@idom.hooks.use_effect(args=None) # force this to run every time
528528
async def effect():
529529
effect_ran.set()
530530
try:
@@ -575,7 +575,7 @@ async def test_error_in_effect_cleanup_is_gracefully_handled(caplog):
575575
@idom.component
576576
@component_hook.capture
577577
def ComponentWithEffect():
578-
@idom.hooks.use_effect
578+
@idom.hooks.use_effect(args=None) # force this to run every time
579579
def ok_effect():
580580
def bad_cleanup():
581581
raise ValueError("Something went wong :(")
@@ -765,7 +765,7 @@ async def test_use_memo_always_runs_if_args_are_none():
765765
@idom.component
766766
@component_hook.capture
767767
def ComponentWithMemo():
768-
value = idom.hooks.use_memo(lambda: next(iter_values))
768+
value = idom.hooks.use_memo(lambda: next(iter_values), args=None)
769769
used_values.append(value)
770770
return idom.html.div()
771771

@@ -860,3 +860,35 @@ def bad_callback():
860860

861861
first_log_line = next(iter(caplog.records)).msg.split("\n", 1)[0]
862862
assert re.match(f"Failed to schedule render via {bad_callback}", first_log_line)
863+
864+
865+
async def test_use_effect_automatically_infers_closure_args():
866+
set_count = idom.Ref()
867+
did_effect = asyncio.Event()
868+
869+
@idom.component
870+
def CounterWithEffect():
871+
count, set_count.current = idom.hooks.use_state(0)
872+
873+
@idom.hooks.use_effect
874+
def some_effect_that_uses_count():
875+
"""should automatically trigger on count change"""
876+
count # use count in this closure
877+
did_effect.set()
878+
879+
return idom.html.div()
880+
881+
with idom.Layout(CounterWithEffect()) as layout:
882+
await layout.render()
883+
await did_effect.wait()
884+
did_effect.clear()
885+
886+
for i in range(1, 3):
887+
set_count.current(i)
888+
await layout.render()
889+
await did_effect.wait()
890+
did_effect.clear()
891+
892+
893+
def test_use_memo_automatically_infers_closure_args():
894+
...

0 commit comments

Comments
 (0)