Skip to content

Commit 05b2a8e

Browse files
Merge pull request #225 from labthings/call_async_task
Add `call_async_task` to `thing_server_interface` allowing access to `BlockingPortal.call`
2 parents 2cd3eaf + c8085b0 commit 05b2a8e

File tree

2 files changed

+47
-0
lines changed

2 files changed

+47
-0
lines changed

src/labthings_fastapi/thing_server_interface.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,31 @@ def start_async_task_soon(
106106
raise ServerNotRunningError("Can't run async code without an event loop.")
107107
return portal.start_task_soon(async_function, *args)
108108

109+
def call_async_task(
110+
self, async_function: Callable[Params, Awaitable[ReturnType]], *args: Any
111+
) -> ReturnType:
112+
r"""Run an asynchronous task in the server's event loop in a blocking manner.
113+
114+
This function wraps `anyio.from_thread.BlockingPortal.call` to
115+
provide a way of calling asynchronous code from threaded code. It will
116+
block the current thread while it calls the provided async function in the
117+
server's event loop.
118+
119+
Do not call this from the event loop or it may lead to a deadlock.
120+
121+
:param async_function: the asynchronous function to call.
122+
:param \*args: positional arguments to be provided to the function.
123+
124+
:returns: The return value from the asynchronous function.
125+
126+
:raises ServerNotRunningError: if the server is not running
127+
(i.e. there is no event loop).
128+
"""
129+
portal = self._get_server().blocking_portal
130+
if portal is None:
131+
raise ServerNotRunningError("Can't run async code without an event loop.")
132+
return portal.call(async_function, *args)
133+
109134
@property
110135
def settings_folder(self) -> str:
111136
"""The path to a folder where persistent files may be saved."""

tests/test_thing_server_interface.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,28 @@ async def set_mutable(val):
133133
assert mutable[0] is True
134134

135135

136+
def test_call_async_task(server, interface):
137+
"""Check async tasks may be run in a blocking fashion."""
138+
139+
async def async_shout(input: str):
140+
"""A function that shouts back at you.
141+
142+
This is only async to check it can be called.
143+
"""
144+
return input.upper()
145+
146+
with pytest.raises(ServerNotRunningError):
147+
# You can't run async code unless the server
148+
# is running: this should raise a helpful
149+
# error.
150+
interface.call_async_task(async_shout, "foobar")
151+
152+
with TestClient(server.app) as _:
153+
# TestClient starts an event loop in the background
154+
# so this should work
155+
assert interface.call_async_task(async_shout, "foobar") == "FOOBAR"
156+
157+
136158
def test_settings_folder(server, interface):
137159
"""Check the interface returns the right settings folder."""
138160
assert interface.settings_folder == os.path.join(server.settings_folder, NAME)

0 commit comments

Comments
 (0)