Skip to content

Commit ff02d80

Browse files
authored
Convert async custom message handlers to sync (#506)
* Convert async custom message handlers to sync * Lint * Update
1 parent d1bf74e commit ff02d80

4 files changed

Lines changed: 86 additions & 24 deletions

File tree

.coveragerc_omit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ omit =
88
src/vitessce/wrappers.py
99
src/vitessce/repr.py
1010
src/vitessce/utils.py
11+
src/vitessce/sync_store.py
1112
src/vitessce/data_utils/anndata.py
1213
src/vitessce/data_utils/ome.py
1314
src/vitessce/data_utils/entities.py

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "vitessce"
7-
version = "3.9.1"
7+
version = "3.9.2"
88
authors = [
99
{ name="Mark Keller", email="mark_keller@hms.harvard.edu" },
1010
]

src/vitessce/sync_store.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copied from zarr-python
2+
# Reference: https://github.com/zarr-developers/zarr-python/blob/fe229107f9915f05817f7a664d3550695ff9ca44/src/zarr/testing/stateful.py#L438
3+
4+
import builtins
5+
from typing import Any
6+
import zarr
7+
from zarr.abc.store import Store
8+
from zarr.core.buffer import Buffer, BufferPrototype
9+
10+
11+
class SyncStoreWrapper(zarr.core.sync.SyncMixin):
12+
def __init__(self, store: Store) -> None:
13+
"""Synchronous Store wrapper
14+
15+
This class holds synchronous methods that map to async methods of Store classes.
16+
The synchronous wrapper is needed because hypothesis' stateful testing infra does
17+
not support asyncio so we redefine sync versions of the Store API.
18+
https://github.com/HypothesisWorks/hypothesis/issues/3712#issuecomment-1668999041
19+
"""
20+
self.store = store
21+
22+
@property
23+
def read_only(self) -> bool:
24+
return self.store.read_only
25+
26+
def set(self, key: str, data_buffer: Buffer) -> None:
27+
return self._sync(self.store.set(key, data_buffer))
28+
29+
def list(self) -> builtins.list[str]:
30+
return self._sync_iter(self.store.list())
31+
32+
def get(self, key: str, prototype: BufferPrototype, **kwargs) -> Buffer | None:
33+
return self._sync(self.store.get(key, prototype=prototype, **kwargs))
34+
35+
def get_partial_values(
36+
self, key_ranges: builtins.list[Any], prototype: BufferPrototype
37+
) -> builtins.list[Buffer | None]:
38+
return self._sync(self.store.get_partial_values(prototype=prototype, key_ranges=key_ranges))
39+
40+
def delete(self, path: str) -> None:
41+
return self._sync(self.store.delete(path))
42+
43+
def is_empty(self, prefix: str) -> bool:
44+
return self._sync(self.store.is_empty(prefix=prefix))
45+
46+
def clear(self) -> None:
47+
return self._sync(self.store.clear())
48+
49+
def exists(self, key: str) -> bool:
50+
return self._sync(self.store.exists(key))
51+
52+
def list_dir(self, prefix: str) -> None:
53+
raise NotImplementedError
54+
55+
def list_prefix(self, prefix: str) -> None:
56+
raise NotImplementedError
57+
58+
@property
59+
def supports_listing(self) -> bool:
60+
return self.store.supports_listing
61+
62+
@property
63+
def supports_writes(self) -> bool:
64+
return self.store.supports_writes
65+
66+
@property
67+
def supports_deletes(self) -> bool:
68+
return self.store.supports_deletes

src/vitessce/widget.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import anywidget
88
from traitlets import Unicode, Dict, List, Int, Bool
99

10-
import asyncio
1110
from zarr.abc.store import RangeByteRequest, SuffixByteRequest
1211
from zarr.core.buffer.core import default_buffer_prototype
12+
from .sync_store import SyncStoreWrapper
1313

1414

1515
MAX_PORT_TRIES = 1000
@@ -912,11 +912,11 @@ def close(self):
912912
super().close()
913913

914914
# @anywidget.experimental.command
915-
async def _zarr_get(self, params, buffers):
915+
def _zarr_get(self, params, buffers):
916916
[store_url, key] = params
917-
store = self._stores[store_url]
917+
store = SyncStoreWrapper(self._stores[store_url])
918918
try:
919-
result = await store.get(key.lstrip("/"), prototype=default_buffer_prototype())
919+
result = store.get(key.lstrip("/"), prototype=default_buffer_prototype())
920920
if result is None:
921921
buffers = []
922922
else:
@@ -927,9 +927,9 @@ async def _zarr_get(self, params, buffers):
927927
return {"success": len(buffers) == 1}, buffers
928928

929929
# @anywidget.experimental.command
930-
async def _zarr_get_range(self, params, buffers):
930+
def _zarr_get_range(self, params, buffers):
931931
[store_url, key, range_query] = params
932-
store = self._stores[store_url]
932+
store = SyncStoreWrapper(self._stores[store_url])
933933
try:
934934
range_param = None
935935
# Reference: https://github.com/manzt/zarrita.js/blob/f63a2521e2b46b22aa26af4146822e4d827dff83/packages/%40zarrita-storage/src/types.ts#L3
@@ -943,7 +943,7 @@ async def _zarr_get_range(self, params, buffers):
943943
else:
944944
raise ValueError(f"Invalid range query: {range_query}. Must contain either 'suffixLength' or both 'offset' and 'length'.")
945945

946-
result = await store.get(key, byte_range=range_param, prototype=default_buffer_prototype())
946+
result = store.get(key, byte_range=range_param, prototype=default_buffer_prototype())
947947
if result is None:
948948
buffers = []
949949
else:
@@ -954,16 +954,16 @@ async def _zarr_get_range(self, params, buffers):
954954
return {"success": len(buffers) == 1}, buffers
955955

956956
# @anywidget.experimental.command
957-
async def _zarr_get_multi(self, params_arr, buffers):
957+
def _zarr_get_multi(self, params_arr, buffers):
958958
# This variant of _zarr_get and _zarr_get_range supports batching.
959959
result_dicts = []
960960
result_buffers = []
961961
for params in params_arr:
962962
result_dict, result_buffer_arr = {}, []
963963
if len(params) == 2:
964-
result_dict, result_buffer_arr = await self._zarr_get(params, buffers)
964+
result_dict, result_buffer_arr = self._zarr_get(params, buffers)
965965
elif len(params) == 3:
966-
result_dict, result_buffer_arr = await self._zarr_get_range(params, buffers)
966+
result_dict, result_buffer_arr = self._zarr_get_range(params, buffers)
967967
else:
968968
raise ValueError("Expected params to have len 2 or 3 in _zarr_get_multi")
969969
if result_dict["success"] and len(result_buffer_arr) == 1:
@@ -986,28 +986,21 @@ def _handle_msg(self, msg: dict) -> None:
986986
if content.get("kind") != "anywidget-command":
987987
super()._handle_msg(msg)
988988
return
989-
try:
990-
loop = asyncio.get_event_loop()
991-
except RuntimeError:
992-
return
993-
if loop.is_running():
994-
loop.create_task(self._dispatch_command(content, buffers))
995-
else:
996-
loop.run_until_complete(self._dispatch_command(content, buffers))
989+
self._dispatch_command(content, buffers)
997990

998-
async def _dispatch_command(self, msg: dict, buffers: list[bytes]) -> None:
991+
def _dispatch_command(self, msg: dict, buffers: list[bytes]) -> None:
999992
name = msg.get("name")
1000993
params = msg.get("msg")
1001994
msg_id = msg.get("id")
1002995
try:
1003996
if name == "_zarr_get":
1004-
response, result_buffers = await self._zarr_get(params, buffers)
997+
response, result_buffers = self._zarr_get(params, buffers)
1005998
elif name == "_zarr_get_range":
1006-
response, result_buffers = await self._zarr_get_range(params, buffers)
999+
response, result_buffers = self._zarr_get_range(params, buffers)
10071000
elif name == "_zarr_get_multi":
1008-
response, result_buffers = await self._zarr_get_multi(params, buffers)
1001+
response, result_buffers = self._zarr_get_multi(params, buffers)
10091002
elif name == "_plugin_command":
1010-
response, result_buffers = await self._plugin_command(params, buffers)
1003+
response, result_buffers = self._plugin_command(params, buffers)
10111004
else:
10121005
return
10131006
except Exception as exc: # noqa: BLE001

0 commit comments

Comments
 (0)