Skip to content

Paths with leading slashes do bad things #2357

Closed
@jhamman

Description

@jhamman

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugPotential issues with the zarr-python library

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions