Skip to content

Commit 899dc0e

Browse files
authored
Merge pull request #62 from rstudio/reactive-async-api
2 parents e51af30 + 08e8571 commit 899dc0e

File tree

3 files changed

+81
-83
lines changed

3 files changed

+81
-83
lines changed

examples/bind_event/app.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,22 @@ def btn_value():
5454
# -----------------------------------------------------------------------------
5555
# Async
5656
# -----------------------------------------------------------------------------
57-
@reactive.effect_async()
57+
@reactive.effect()
5858
@event(input.btn_async)
5959
async def _():
6060
await asyncio.sleep(0)
61-
print("@effect_async() event: ", str(input.btn_async()))
61+
print("async @effect() event: ", str(input.btn_async()))
6262

63-
@reactive.calc_async()
63+
@reactive.calc()
6464
@event(input.btn_async)
6565
async def btn_async_r() -> int:
6666
await asyncio.sleep(0)
6767
return input.btn_async()
6868

69-
@reactive.effect_async()
69+
@reactive.effect()
7070
async def _():
7171
val = await btn_async_r()
72-
print("@calc_async() event: ", str(val))
72+
print("async @calc() event: ", str(val))
7373

7474
@output()
7575
@render_ui()

shiny/reactive.py

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
"Calc",
66
"CalcAsync",
77
"calc",
8-
"calc_async",
98
"Effect",
109
"EffectAsync",
1110
"effect",
12-
"effect_async",
1311
"isolate",
1412
"invalidate_later",
1513
)
@@ -25,6 +23,7 @@
2523
TypeVar,
2624
Union,
2725
Generic,
26+
cast,
2827
)
2928
import typing
3029
import inspect
@@ -70,17 +69,25 @@ def set(self, value: T) -> bool:
7069
# ==============================================================================
7170
# Calc
7271
# ==============================================================================
72+
73+
CalcFunction = Callable[[], T]
74+
CalcFunctionAsync = Callable[[], Awaitable[T]]
75+
76+
7377
class Calc(Generic[T]):
7478
def __init__(
7579
self,
76-
func: Callable[[], T],
80+
fn: CalcFunction[T],
7781
*,
7882
session: Union[MISSING_TYPE, "Session", None] = MISSING,
7983
) -> None:
80-
if inspect.iscoroutinefunction(func):
81-
raise TypeError("Reactive requires a non-async function")
84+
self.__name__ = fn.__name__
85+
self.__doc__ = fn.__doc__
8286

83-
self._func: Callable[[], Awaitable[T]] = utils.wrap_async(func)
87+
# The CalcAsync subclass will pass in an async function, but it tells the
88+
# static type checker that it's synchronous. wrap_async() is smart -- if is
89+
# passed an async function, it will not change it.
90+
self._fn: CalcFunctionAsync[T] = utils.wrap_async(fn)
8491
self._is_async: bool = False
8592

8693
self._dependents: Dependents = Dependents()
@@ -152,26 +159,22 @@ def _on_invalidate_cb(self) -> None:
152159
async def _run_func(self) -> None:
153160
self._error.clear()
154161
try:
155-
self._value.append(await self._func())
162+
self._value.append(await self._fn())
156163
except Exception as err:
157164
self._error.append(err)
158165

159166

160167
class CalcAsync(Calc[T]):
161168
def __init__(
162169
self,
163-
func: Callable[[], Awaitable[T]],
170+
fn: CalcFunctionAsync[T],
164171
*,
165172
session: Union[MISSING_TYPE, "Session", None] = MISSING,
166173
) -> None:
167-
if not inspect.iscoroutinefunction(func):
168-
raise TypeError("CalcAsync requires an async function")
169-
170-
# Init the Calc base class with a placeholder synchronous function so it won't
171-
# throw an error, then replace it with the async function. Need the `cast` to
172-
# satisfy the type checker.
173-
super().__init__(lambda: typing.cast(T, None), session=session)
174-
self._func: Callable[[], Awaitable[T]] = func
174+
if not inspect.iscoroutinefunction(fn):
175+
raise TypeError(self.__class__.__name__ + " requires an async function")
176+
177+
super().__init__(cast(CalcFunction[T], fn), session=session)
175178
self._is_async = True
176179

177180
async def __call__(self) -> T:
@@ -180,37 +183,41 @@ async def __call__(self) -> T:
180183

181184
def calc(
182185
*, session: Union[MISSING_TYPE, "Session", None] = MISSING
183-
) -> Callable[[Callable[[], T]], Calc[T]]:
184-
def create_calc(fn: Callable[[], T]) -> Calc[T]:
185-
return Calc(fn, session=session)
186+
) -> Callable[[Union[CalcFunction[T], CalcFunctionAsync[T]]], Calc[T]]:
187+
def create_calc(fn: Union[CalcFunction[T], CalcFunctionAsync[T]]) -> Calc[T]:
188+
if inspect.iscoroutinefunction(fn):
189+
fn = cast(CalcFunctionAsync[T], fn)
190+
return CalcAsync(fn, session=session)
191+
else:
192+
fn = cast(CalcFunction[T], fn)
193+
return Calc(fn, session=session)
186194

187195
return create_calc
188196

189197

190-
def calc_async(
191-
*, session: Union[MISSING_TYPE, "Session", None] = MISSING
192-
) -> Callable[[Callable[[], Awaitable[T]]], CalcAsync[T]]:
193-
def create_calc_async(fn: Callable[[], Awaitable[T]]) -> CalcAsync[T]:
194-
return CalcAsync(fn, session=session)
195-
196-
return create_calc_async
197-
198-
199198
# ==============================================================================
200199
# Effect
201200
# ==============================================================================
201+
202+
EffectFunction = Callable[[], None]
203+
EffectFunctionAsync = Callable[[], Awaitable[None]]
204+
205+
202206
class Effect:
203207
def __init__(
204208
self,
205-
func: Callable[[], None],
209+
fn: EffectFunction,
206210
*,
207211
priority: int = 0,
208212
session: Union[MISSING_TYPE, "Session", None] = MISSING,
209213
) -> None:
210-
if inspect.iscoroutinefunction(func):
211-
raise TypeError("Effect requires a non-async function")
214+
self.__name__ = fn.__name__
215+
self.__doc__ = fn.__doc__
212216

213-
self._func: Callable[[], Awaitable[None]] = utils.wrap_async(func)
217+
# The EffectAsync subclass will pass in an async function, but it tells the
218+
# static type checker that it's synchronous. wrap_async() is smart -- if is
219+
# passed an async function, it will not change it.
220+
self._fn: EffectFunctionAsync = utils.wrap_async(fn)
214221
self._is_async: bool = False
215222

216223
self._priority: int = priority
@@ -270,7 +277,7 @@ async def run(self) -> None:
270277
with shiny_session.session_context(self._session):
271278
try:
272279
with ctx():
273-
await self._func()
280+
await self._fn()
274281
except SilentException:
275282
# It's OK for SilentException to cause an Effect to stop running
276283
pass
@@ -297,24 +304,21 @@ def _on_session_ended_cb(self) -> None:
297304
class EffectAsync(Effect):
298305
def __init__(
299306
self,
300-
func: Callable[[], Awaitable[None]],
307+
fn: EffectFunctionAsync,
301308
*,
302309
priority: int = 0,
303310
session: Union[MISSING_TYPE, "Session", None] = MISSING,
304311
) -> None:
305-
if not inspect.iscoroutinefunction(func):
306-
raise TypeError("EffectAsync requires an async function")
312+
if not inspect.iscoroutinefunction(fn):
313+
raise TypeError(self.__class__.__name__ + " requires an async function")
307314

308-
# Init the Efect base class with a placeholder synchronous function
309-
# so it won't throw an error, then replace it with the async function.
310-
super().__init__(lambda: None, session=session, priority=priority)
311-
self._func: Callable[[], Awaitable[None]] = func
315+
super().__init__(cast(EffectFunction, fn), session=session, priority=priority)
312316
self._is_async = True
313317

314318

315319
def effect(
316320
*, priority: int = 0, session: Union[MISSING_TYPE, "Session", None] = MISSING
317-
) -> Callable[[Callable[[], None]], Effect]:
321+
) -> Callable[[Union[EffectFunction, EffectFunctionAsync]], Effect]:
318322
"""[summary]
319323
320324
Args:
@@ -325,23 +329,17 @@ def effect(
325329
[description]
326330
"""
327331

328-
def create_effect(fn: Callable[[], None]) -> Effect:
329-
return Effect(fn, priority=priority, session=session)
332+
def create_effect(fn: Union[EffectFunction, EffectFunctionAsync]) -> Effect:
333+
if inspect.iscoroutinefunction(fn):
334+
fn = cast(EffectFunctionAsync, fn)
335+
return EffectAsync(fn, priority=priority, session=session)
336+
else:
337+
fn = cast(EffectFunction, fn)
338+
return Effect(fn, priority=priority, session=session)
330339

331340
return create_effect
332341

333342

334-
def effect_async(
335-
*,
336-
priority: int = 0,
337-
session: Union[MISSING_TYPE, "Session", None] = MISSING,
338-
) -> Callable[[Callable[[], Awaitable[None]]], EffectAsync]:
339-
def create_effect_async(fn: Callable[[], Awaitable[None]]) -> EffectAsync:
340-
return EffectAsync(fn, priority=priority, session=session)
341-
342-
return create_effect_async
343-
344-
345343
# ==============================================================================
346344
# Miscellaneous functions
347345
# ==============================================================================

0 commit comments

Comments
 (0)