Skip to content

Commit cb2db7d

Browse files
committed
Require auto_mkdir for LocalFileSystem
1 parent 5d8e8ca commit cb2db7d

File tree

2 files changed

+82
-45
lines changed

2 files changed

+82
-45
lines changed

src/zarr/storage/_fsspec.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ def _make_async(fs: AbstractFileSystem) -> AsyncFileSystem:
4747
fs_dict = fs.to_dict()
4848
fs_dict["asynchronous"] = True
4949
return AbstractFileSystem.from_dict(fs_dict)
50+
from fsspec.implementations.local import LocalFileSystem
51+
52+
if type(fs) is LocalFileSystem and not fs.auto_mkdir:
53+
raise ValueError(
54+
f"LocalFilesystem {fs} was created with auto_mkdir=False but Zarr requires the filesystem to automatically create directories"
55+
)
5056
try:
5157
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
5258

tests/test_store/test_fsspec.py

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -108,35 +108,94 @@ async def test_basic() -> None:
108108
assert out[0].to_bytes() == data[1:]
109109

110110

111-
@pytest.mark.xfail(reason="See https://github.com/zarr-developers/zarr-python/issues/2808")
112-
def test_open_fsmap_file(tmp_path: pathlib.Path) -> None:
113-
fsspec = pytest.importorskip("fsspec")
114-
fs = fsspec.filesystem("file")
115-
mapper = fs.get_mapper(tmp_path)
116-
arr = zarr.open(store=mapper, mode="w", shape=(3, 3))
111+
def array_roundtrip(store):
112+
"""
113+
Round trip an array using a Zarr store
114+
115+
Args:
116+
store: Store-Like object (e.g., FSMap)
117+
"""
118+
arr = zarr.open(store=store, mode="w", shape=(3, 3))
117119
assert isinstance(arr, Array)
118120
# Set values
119121
arr[:] = 1
120122
# Read set values
121-
arr = zarr.open(store=mapper, mode="r", shape=(3, 3))
123+
arr = zarr.open(store=store, mode="r", shape=(3, 3))
122124
assert isinstance(arr, Array)
123125
np.testing.assert_array_equal(np.ones((3, 3)), arr[:])
124126

125127

128+
@pytest.mark.skipif(
129+
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
130+
reason="No AsyncFileSystemWrapper",
131+
)
132+
def test_wrap_sync_filesystem(tmp_path):
133+
"""The local fs is not async so we should expect it to be wrapped automatically"""
134+
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
135+
136+
store = FsspecStore.from_url(f"local://{tmp_path}", storage_options={"auto_mkdir": True})
137+
assert isinstance(store.fs, AsyncFileSystemWrapper)
138+
assert store.fs.async_impl
139+
array_roundtrip(store)
140+
141+
142+
@pytest.mark.skipif(
143+
parse_version(fsspec.__version__) >= parse_version("2024.12.0"),
144+
reason="No AsyncFileSystemWrapper",
145+
)
146+
def test_wrap_sync_filesystem_raises(tmp_path):
147+
"""The local fs is not async so we should expect it to be wrapped automatically"""
148+
with pytest.raises(ImportError, match="The filesystem .*"):
149+
FsspecStore.from_url(f"local://{tmp_path}", storage_options={"auto_mkdir": True})
150+
151+
152+
@pytest.mark.skipif(
153+
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
154+
reason="No AsyncFileSystemWrapper",
155+
)
156+
def test_no_wrap_async_filesystem():
157+
"""An async fs should not be wrapped automatically; fsspec's s3 filesystem is such an fs"""
158+
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
159+
160+
store = FsspecStore.from_url(
161+
f"s3://{test_bucket_name}/foo/spam/",
162+
storage_options={"endpoint_url": endpoint_url, "anon": False},
163+
)
164+
assert not isinstance(store.fs, AsyncFileSystemWrapper)
165+
assert store.fs.async_impl
166+
array_roundtrip(store)
167+
168+
169+
@pytest.mark.skipif(
170+
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
171+
reason="No AsyncFileSystemWrapper",
172+
)
173+
def test_open_fsmap_file(tmp_path: pathlib.Path) -> None:
174+
fsspec = pytest.importorskip("fsspec")
175+
fs = fsspec.filesystem("file", auto_mkdir=True)
176+
mapper = fs.get_mapper(tmp_path)
177+
array_roundtrip(mapper)
178+
179+
180+
@pytest.mark.skipif(
181+
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
182+
reason="No AsyncFileSystemWrapper",
183+
)
184+
def test_open_fsmap_file_raises(tmp_path: pathlib.Path) -> None:
185+
fsspec = pytest.importorskip("fsspec.implementations.local")
186+
fs = fsspec.LocalFileSystem(auto_mkdir=False)
187+
mapper = fs.get_mapper(tmp_path)
188+
with pytest.raises(ValueError, match="LocalFilesystem .*"):
189+
array_roundtrip(mapper)
190+
191+
126192
@pytest.mark.parametrize("asynchronous", [True, False])
127193
def test_open_fsmap_s3(asynchronous: bool) -> None:
128194
s3_filesystem = s3fs.S3FileSystem(
129195
asynchronous=asynchronous, endpoint_url=endpoint_url, anon=False
130196
)
131197
mapper = s3_filesystem.get_mapper(f"s3://{test_bucket_name}/map/foo/")
132-
arr = zarr.open(store=mapper, mode="w", shape=(3, 3))
133-
assert isinstance(arr, Array)
134-
# Set values
135-
arr[:] = 1
136-
# Read set values
137-
arr = zarr.open(store=mapper, mode="r", shape=(3, 3))
138-
assert isinstance(arr, Array)
139-
np.testing.assert_array_equal(np.ones((3, 3)), arr[:])
198+
array_roundtrip(mapper)
140199

141200

142201
def test_open_s3map_raises() -> None:
@@ -147,12 +206,12 @@ def test_open_s3map_raises() -> None:
147206
with pytest.raises(
148207
ValueError, match="'path' was provided but is not used for FSMap store_like objects"
149208
):
150-
zarr.open(store=mapper, mode="w", shape=(3, 3), path="foo")
209+
zarr.open(store=mapper, path="bar", mode="w", shape=(3, 3))
151210
with pytest.raises(
152211
ValueError,
153212
match="'storage_options was provided but is not used for FSMap store_like objects",
154213
):
155-
zarr.open(store=mapper, mode="w", shape=(3, 3), storage_options={"anon": True})
214+
zarr.open(store=mapper, storage_options={"anon": True}, mode="w", shape=(3, 3))
156215

157216

158217
@pytest.mark.parametrize("asynchronous", [True, False])
@@ -276,31 +335,3 @@ async def test_empty_nonexistent_path(self, store_kwargs) -> None:
276335
store_kwargs["path"] += "/abc"
277336
store = await self.store_cls.open(**store_kwargs)
278337
assert await store.is_empty("")
279-
280-
281-
@pytest.mark.skipif(
282-
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
283-
reason="No AsyncFileSystemWrapper",
284-
)
285-
def test_wrap_sync_filesystem():
286-
"""The local fs is not async so we should expect it to be wrapped automatically"""
287-
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
288-
289-
store = FsspecStore.from_url("local://test/path")
290-
291-
assert isinstance(store.fs, AsyncFileSystemWrapper)
292-
assert store.fs.async_impl
293-
294-
295-
@pytest.mark.skipif(
296-
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
297-
reason="No AsyncFileSystemWrapper",
298-
)
299-
def test_no_wrap_async_filesystem():
300-
"""An async fs should not be wrapped automatically; fsspec's https filesystem is such an fs"""
301-
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
302-
303-
store = FsspecStore.from_url("https://test/path")
304-
305-
assert not isinstance(store.fs, AsyncFileSystemWrapper)
306-
assert store.fs.async_impl

0 commit comments

Comments
 (0)