55import re
66from typing import TYPE_CHECKING , Any
77
8+ import numpy as np
89import pytest
910from packaging .version import parse as parse_version
1011
1112import zarr .api .asynchronous
13+ from zarr import Array
1214from zarr .abc .store import OffsetByteRequest
1315from zarr .core .buffer import Buffer , cpu , default_buffer_prototype
1416from zarr .core .sync import _collect_aiterator , sync
1517from zarr .storage import FsspecStore
18+ from zarr .storage ._fsspec import _make_async
1619from zarr .testing .store import StoreTests
1720
1821if TYPE_CHECKING :
22+ import pathlib
1923 from collections .abc import Generator
2024 from pathlib import Path
2125
@@ -191,7 +195,11 @@ async def test_fsspec_store_from_uri(self, store: FsspecStore) -> None:
191195 )
192196 assert dict (group .attrs ) == {"key" : "value" }
193197
194- meta ["attributes" ]["key" ] = "value-2" # type: ignore[index]
198+ meta = {
199+ "attributes" : {"key" : "value-2" },
200+ "zarr_format" : 3 ,
201+ "node_type" : "group" ,
202+ }
195203 await store .set (
196204 "directory-2/zarr.json" ,
197205 self .buffer_cls .from_bytes (json .dumps (meta ).encode ()),
@@ -201,7 +209,11 @@ async def test_fsspec_store_from_uri(self, store: FsspecStore) -> None:
201209 )
202210 assert dict (group .attrs ) == {"key" : "value-2" }
203211
204- meta ["attributes" ]["key" ] = "value-3" # type: ignore[index]
212+ meta = {
213+ "attributes" : {"key" : "value-3" },
214+ "zarr_format" : 3 ,
215+ "node_type" : "group" ,
216+ }
205217 await store .set (
206218 "directory-3/zarr.json" ,
207219 self .buffer_cls .from_bytes (json .dumps (meta ).encode ()),
@@ -264,32 +276,131 @@ async def test_delete_dir_unsupported_deletes(self, store: FsspecStore) -> None:
264276 await store .delete_dir ("test_prefix" )
265277
266278
279+ def array_roundtrip (store : FsspecStore ) -> None :
280+ """
281+ Round trip an array using a Zarr store
282+
283+ Args:
284+ store: FsspecStore
285+ """
286+ data = np .ones ((3 , 3 ))
287+ arr = zarr .create_array (store = store , overwrite = True , data = data )
288+ assert isinstance (arr , Array )
289+ # Read set values
290+ arr2 = zarr .open_array (store = store )
291+ assert isinstance (arr2 , Array )
292+ np .testing .assert_array_equal (arr [:], data )
293+
294+
267295@pytest .mark .skipif (
268296 parse_version (fsspec .__version__ ) < parse_version ("2024.12.0" ),
269297 reason = "No AsyncFileSystemWrapper" ,
270298)
271- def test_wrap_sync_filesystem () -> None :
299+ def test_wrap_sync_filesystem (tmp_path : pathlib . Path ) -> None :
272300 """The local fs is not async so we should expect it to be wrapped automatically"""
273301 from fsspec .implementations .asyn_wrapper import AsyncFileSystemWrapper
274302
275- store = FsspecStore .from_url ("local://test/path" )
276-
303+ store = FsspecStore .from_url (f"file://{ tmp_path } " , storage_options = {"auto_mkdir" : True })
277304 assert isinstance (store .fs , AsyncFileSystemWrapper )
278305 assert store .fs .async_impl
306+ array_roundtrip (store )
307+
308+
309+ @pytest .mark .skipif (
310+ parse_version (fsspec .__version__ ) >= parse_version ("2024.12.0" ),
311+ reason = "No AsyncFileSystemWrapper" ,
312+ )
313+ def test_wrap_sync_filesystem_raises (tmp_path : pathlib .Path ) -> None :
314+ """The local fs is not async so we should expect it to be wrapped automatically"""
315+ with pytest .raises (ImportError , match = "The filesystem .*" ):
316+ FsspecStore .from_url (f"file://{ tmp_path } " , storage_options = {"auto_mkdir" : True })
279317
280318
281319@pytest .mark .skipif (
282320 parse_version (fsspec .__version__ ) < parse_version ("2024.12.0" ),
283321 reason = "No AsyncFileSystemWrapper" ,
284322)
285323def test_no_wrap_async_filesystem () -> None :
286- """An async fs should not be wrapped automatically; fsspec's https filesystem is such an fs"""
324+ """An async fs should not be wrapped automatically; fsspec's s3 filesystem is such an fs"""
287325 from fsspec .implementations .asyn_wrapper import AsyncFileSystemWrapper
288326
289- store = FsspecStore .from_url ("https://test/path" )
290-
327+ store = FsspecStore .from_url (
328+ f"s3://{ test_bucket_name } /foo/spam/" ,
329+ storage_options = {"endpoint_url" : endpoint_url , "anon" : False , "asynchronous" : True },
330+ read_only = False ,
331+ )
291332 assert not isinstance (store .fs , AsyncFileSystemWrapper )
292333 assert store .fs .async_impl
334+ array_roundtrip (store )
335+
336+
337+ @pytest .mark .skipif (
338+ parse_version (fsspec .__version__ ) < parse_version ("2024.12.0" ),
339+ reason = "No AsyncFileSystemWrapper" ,
340+ )
341+ def test_open_fsmap_file (tmp_path : pathlib .Path ) -> None :
342+ min_fsspec_with_async_wrapper = parse_version ("2024.12.0" )
343+ current_version = parse_version (fsspec .__version__ )
344+
345+ fs = fsspec .filesystem ("file" , auto_mkdir = True )
346+ mapper = fs .get_mapper (tmp_path )
347+
348+ if current_version < min_fsspec_with_async_wrapper :
349+ # Expect ImportError for older versions
350+ with pytest .raises (
351+ ImportError ,
352+ match = r"The filesystem .* is synchronous, and the required AsyncFileSystemWrapper is not available.*" ,
353+ ):
354+ array_roundtrip (mapper )
355+ else :
356+ # Newer versions should work
357+ array_roundtrip (mapper )
358+
359+
360+ @pytest .mark .skipif (
361+ parse_version (fsspec .__version__ ) < parse_version ("2024.12.0" ),
362+ reason = "No AsyncFileSystemWrapper" ,
363+ )
364+ def test_open_fsmap_file_raises (tmp_path : pathlib .Path ) -> None :
365+ fsspec = pytest .importorskip ("fsspec.implementations.local" )
366+ fs = fsspec .LocalFileSystem (auto_mkdir = False )
367+ mapper = fs .get_mapper (tmp_path )
368+ with pytest .raises (ValueError , match = "LocalFilesystem .*" ):
369+ array_roundtrip (mapper )
370+
371+
372+ @pytest .mark .parametrize ("asynchronous" , [True , False ])
373+ def test_open_fsmap_s3 (asynchronous : bool ) -> None :
374+ s3_filesystem = s3fs .S3FileSystem (
375+ asynchronous = asynchronous , endpoint_url = endpoint_url , anon = False
376+ )
377+ mapper = s3_filesystem .get_mapper (f"s3://{ test_bucket_name } /map/foo/" )
378+ array_roundtrip (mapper )
379+
380+
381+ def test_open_s3map_raises () -> None :
382+ with pytest .raises (TypeError , match = "Unsupported type for store_like:.*" ):
383+ zarr .open (store = 0 , mode = "w" , shape = (3 , 3 ))
384+ s3_filesystem = s3fs .S3FileSystem (asynchronous = True , endpoint_url = endpoint_url , anon = False )
385+ mapper = s3_filesystem .get_mapper (f"s3://{ test_bucket_name } /map/foo/" )
386+ with pytest .raises (
387+ ValueError , match = "'path' was provided but is not used for FSMap store_like objects"
388+ ):
389+ zarr .open (store = mapper , path = "bar" , mode = "w" , shape = (3 , 3 ))
390+ with pytest .raises (
391+ ValueError ,
392+ match = "'storage_options was provided but is not used for FSMap store_like objects" ,
393+ ):
394+ zarr .open (store = mapper , storage_options = {"anon" : True }, mode = "w" , shape = (3 , 3 ))
395+
396+
397+ @pytest .mark .parametrize ("asynchronous" , [True , False ])
398+ def test_make_async (asynchronous : bool ) -> None :
399+ s3_filesystem = s3fs .S3FileSystem (
400+ asynchronous = asynchronous , endpoint_url = endpoint_url , anon = False
401+ )
402+ fs = _make_async (s3_filesystem )
403+ assert fs .asynchronous
293404
294405
295406@pytest .mark .skipif (
0 commit comments