Closed
Description
Zarr version
3.0.0-beta
Numcodecs version
0.13
Python Version
3.11
Operating System
Mac
Installation
pip
Description
In pydata/xarray#9552, we have noticed that setting keys with leading slashes ends poorly for some stores (namely the local store).
We should probably be normalizing keys in the Array/Group APIs before the store even sees them.
Steps to reproduce
import zarr
group = zarr.open_group('/Users/jhamman/workdir/zarr/foo', mode='a')
group.create_group('/foo', attributes={'a': 'b'})
---------------------------------------------------------------------------
OSError Traceback (most recent call last)
Cell In[4], line 1
----> 1 group.create_group('[/foo](http://localhost:8888/foo)', attributes={'a': 'b'})
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py:1480](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py#line=1479), in Group.create_group(self, name, **kwargs)
1479 def create_group(self, name: str, **kwargs: Any) -> Group:
-> 1480 return Group(self._sync(self._async_group.create_group(name, **kwargs)))
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/sync.py:185](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/sync.py#line=184), in SyncMixin._sync(self, coroutine)
182 def _sync(self, coroutine: Coroutine[Any, Any, T]) -> T:
183 # TODO: refactor this to to take *args and **kwargs and pass those to the method
184 # this should allow us to better type the sync wrapper
--> 185 return sync(
186 coroutine,
187 timeout=config.get("async.timeout"),
188 )
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/sync.py:141](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/sync.py#line=140), in sync(coro, loop, timeout)
138 return_result = next(iter(finished)).result()
140 if isinstance(return_result, BaseException):
--> 141 raise return_result
142 else:
143 return return_result
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/sync.py:100](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/sync.py#line=99), in _runner(coro)
95 """
96 Await a coroutine and return the result of running it. If awaiting the coroutine raises an
97 exception, the exception will be returned.
98 """
99 try:
--> 100 return await coro
101 except Exception as ex:
102 return ex
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py:799](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py#line=798), in AsyncGroup.create_group(self, name, exists_ok, attributes)
791 async def create_group(
792 self,
793 name: str,
(...)
796 attributes: dict[str, Any] | None = None,
797 ) -> AsyncGroup:
798 attributes = attributes or {}
--> 799 return await type(self).from_store(
800 self.store_path [/](http://localhost:8888/) name,
801 attributes=attributes,
802 exists_ok=exists_ok,
803 zarr_format=self.metadata.zarr_format,
804 )
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py:413](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py#line=412), in AsyncGroup.from_store(cls, store, attributes, exists_ok, zarr_format)
408 attributes = attributes or {}
409 group = cls(
410 metadata=GroupMetadata(attributes=attributes, zarr_format=zarr_format),
411 store_path=store_path,
412 )
--> 413 await group._save_metadata(ensure_parents=True)
414 return group
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py:745](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/core/group.py#line=744), in AsyncGroup._save_metadata(self, ensure_parents)
735 for parent in parents:
736 awaitables.extend(
737 [
738 (parent.store_path [/](http://localhost:8888/) key).set_if_not_exists(value)
(...)
742 ]
743 )
--> 745 await asyncio.gather(*awaitables)
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/abc/store.py:426](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/abc/store.py#line=425), in set_or_delete(byte_setter, value)
424 await byte_setter.delete()
425 else:
--> 426 await byte_setter.set(value)
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/common.py:90](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/common.py#line=89), in StorePath.set(self, value, byte_range)
88 if byte_range is not None:
89 raise NotImplementedError("Store.set does not have partial writes yet")
---> 90 await self.store.set(self.path, value)
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/local.py:172](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/local.py#line=171), in LocalStore.set(self, key, value)
170 async def set(self, key: str, value: Buffer) -> None:
171 # docstring inherited
--> 172 return await self._set(key, value)
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/local.py:189](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/local.py#line=188), in LocalStore._set(self, key, value, exclusive)
187 raise TypeError("LocalStore.set(): `value` must a Buffer instance")
188 path = self.root [/](http://localhost:8888/) key
--> 189 await asyncio.to_thread(_put, path, value, start=None, exclusive=exclusive)
File [~/miniforge3/envs/icechunk-demo/lib/python3.12/asyncio/threads.py:25](http://localhost:8888/lab/tree/~/miniforge3/envs/icechunk-demo/lib/python3.12/asyncio/threads.py#line=24), in to_thread(func, *args, **kwargs)
23 ctx = contextvars.copy_context()
24 func_call = functools.partial(ctx.run, func, *args, **kwargs)
---> 25 return await loop.run_in_executor(None, func_call)
File [~/miniforge3/envs/icechunk-demo/lib/python3.12/concurrent/futures/thread.py:58](http://localhost:8888/lab/tree/~/miniforge3/envs/icechunk-demo/lib/python3.12/concurrent/futures/thread.py#line=57), in _WorkItem.run(self)
55 return
57 try:
---> 58 result = self.fn(*self.args, **self.kwargs)
59 except BaseException as exc:
60 self.future.set_exception(exc)
File [~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/local.py:53](http://localhost:8888/lab/tree/~/Library/CloudStorage/Dropbox/src/zarr-python/src/zarr/storage/local.py#line=52), in _put(path, value, start, exclusive)
47 def _put(
48 path: Path,
49 value: Buffer,
50 start: int | None = None,
51 exclusive: bool = False,
52 ) -> int | None:
---> 53 path.parent.mkdir(parents=True, exist_ok=True)
54 if start is not None:
55 with path.open("r+b") as f:
File [~/miniforge3/envs/icechunk-demo/lib/python3.12/pathlib.py:1312](http://localhost:8888/lab/tree/~/miniforge3/envs/icechunk-demo/lib/python3.12/pathlib.py#line=1311), in Path.mkdir(self, mode, parents, exist_ok)
1308 """
1309 Create a new directory at this given path.
1310 """
1311 try:
-> 1312 os.mkdir(self, mode)
1313 except FileNotFoundError:
1314 if not parents or self.parent == self:
OSError: [Errno 30] Read-only file system: '[/foo](http://localhost:8888/foo)'
Additional output
No response