Skip to content

Commit 8e03b56

Browse files
authored
Merge branch 'main' into release-improvements
2 parents f307e63 + 279d400 commit 8e03b56

19 files changed

Lines changed: 216 additions & 82 deletions

changes/3924.bugfix.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/zarr/core/buffer/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def all_equal(self, other: Any, equal_nan: bool = True) -> bool:
535535
and self._data.dtype.kind not in ("U", "S", "T", "O", "V")
536536
):
537537
_data, other = np.broadcast_arrays(self._data, np.asarray(other, self._data.dtype))
538-
void_dtype = "V" + str(_data.dtype.itemsize)
538+
void_dtype = f"V{_data.dtype.itemsize}"
539539
return np.array_equal(_data.view(void_dtype), other.view(void_dtype))
540540
# use array_equal to obtain equal_nan=True functionality
541541
# Since fill-value is a scalar, isn't there a faster path than allocating a new array for fill value

src/zarr/core/group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ def name(self) -> str:
840840
# follow h5py convention: add leading slash
841841
name = self.path
842842
if name[0] != "/":
843-
name = "/" + name
843+
name = f"/{name}"
844844
return name
845845
return "/"
846846

src/zarr/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def _reload_config() -> None:
133133

134134
def fully_qualified_name(cls: type) -> str:
135135
module = cls.__module__
136-
return module + "." + cls.__qualname__
136+
return f"{module}.{cls.__qualname__}"
137137

138138

139139
def register_codec(key: str, codec_cls: type[Codec], *, qualname: str | None = None) -> None:

src/zarr/storage/_fsspec.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from zarr.core.buffer import Buffer
1818
from zarr.errors import ZarrUserWarning
19-
from zarr.storage._utils import _join_paths, normalize_path
19+
from zarr.storage._utils import _dereference_path
2020

2121
if TYPE_CHECKING:
2222
from collections.abc import AsyncIterator, Iterable
@@ -127,7 +127,7 @@ def __init__(
127127
) -> None:
128128
super().__init__(read_only=read_only)
129129
self.fs = fs
130-
self.path = normalize_path(path)
130+
self.path = path
131131
self.allowed_exceptions = allowed_exceptions
132132

133133
if not self.fs.async_impl:
@@ -282,7 +282,7 @@ async def get(
282282
# docstring inherited
283283
if not self._is_open:
284284
await self._open()
285-
path = _join_paths([self.path, key])
285+
path = _dereference_path(self.path, key)
286286

287287
try:
288288
if byte_range is None:
@@ -329,7 +329,7 @@ async def set(
329329
raise TypeError(
330330
f"FsspecStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead."
331331
)
332-
path = _join_paths([self.path, key])
332+
path = _dereference_path(self.path, key)
333333
# write data
334334
if byte_range:
335335
raise NotImplementedError
@@ -338,7 +338,7 @@ async def set(
338338
async def delete(self, key: str) -> None:
339339
# docstring inherited
340340
self._check_writable()
341-
path = _join_paths([self.path, key])
341+
path = _dereference_path(self.path, key)
342342
try:
343343
await self.fs._rm(path)
344344
except FileNotFoundError:
@@ -354,14 +354,14 @@ async def delete_dir(self, prefix: str) -> None:
354354
)
355355
self._check_writable()
356356

357-
path_to_delete = _join_paths([self.path, prefix])
357+
path_to_delete = _dereference_path(self.path, prefix)
358358

359359
with suppress(*self.allowed_exceptions):
360360
await self.fs._rm(path_to_delete, recursive=True)
361361

362362
async def exists(self, key: str) -> bool:
363363
# docstring inherited
364-
path = _join_paths([self.path, key])
364+
path = _dereference_path(self.path, key)
365365
exists: bool = await self.fs._exists(path)
366366
return exists
367367

@@ -378,7 +378,7 @@ async def get_partial_values(
378378
starts: list[int | None] = []
379379
stops: list[int | None] = []
380380
for key, byte_range in key_ranges:
381-
paths.append(_join_paths([self.path, key]))
381+
paths.append(_dereference_path(self.path, key))
382382
if byte_range is None:
383383
starts.append(None)
384384
stops.append(None)
@@ -408,7 +408,7 @@ async def get_partial_values(
408408
async def list(self) -> AsyncIterator[str]:
409409
# docstring inherited
410410
allfiles = await self.fs._find(self.path, detail=False, withdirs=False)
411-
for onefile in (a.removeprefix(self.path + "/") for a in allfiles):
411+
for onefile in (a.removeprefix(f"{self.path}/") for a in allfiles):
412412
yield onefile
413413

414414
async def list_dir(self, prefix: str) -> AsyncIterator[str]:
@@ -418,7 +418,7 @@ async def list_dir(self, prefix: str) -> AsyncIterator[str]:
418418
allfiles = await self.fs._ls(prefix, detail=False)
419419
except FileNotFoundError:
420420
return
421-
for onefile in (a.replace(prefix + "/", "") for a in allfiles):
421+
for onefile in (a.replace(f"{prefix}/", "") for a in allfiles):
422422
yield onefile.removeprefix(self.path).removeprefix("/")
423423

424424
async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
@@ -429,7 +429,7 @@ async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
429429
yield onefile.removeprefix(f"{self.path}/")
430430

431431
async def getsize(self, key: str) -> int:
432-
path = _join_paths([self.path, key])
432+
path = _dereference_path(self.path, key)
433433
info = await self.fs._info(path)
434434

435435
size = info.get("size")

src/zarr/storage/_memory.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ async def list_dir(self, prefix: str) -> AsyncIterator[str]:
217217
# a pseudo directory when there's a nested item and we're listing an
218218
# intermediate level.
219219
keys_unique = {
220-
key.removeprefix(prefix + "/").split("/")[0]
220+
key.removeprefix(f"{prefix}/").split("/")[0]
221221
for key in self._store_dict
222-
if key.startswith(prefix + "/") and key != prefix
222+
if key.startswith(f"{prefix}/") and key != prefix
223223
}
224224

225225
for key in keys_unique:
@@ -822,7 +822,7 @@ async def delete(self, key: str) -> None:
822822

823823
async def list(self) -> AsyncIterator[str]:
824824
# docstring inherited
825-
prefix = self.path + "/" if self.path else ""
825+
prefix = f"{self.path}/" if self.path else ""
826826
async for key in super().list():
827827
if key.startswith(prefix):
828828
yield key.removeprefix(prefix)
@@ -832,7 +832,7 @@ async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
832832
# Manual concatenation instead of _join_paths because we need "path/"
833833
# as the prefix when prefix is empty (to list all keys under self.path)
834834
full_prefix = f"{self.path}/{prefix}" if self.path else prefix
835-
path_prefix = self.path + "/" if self.path else ""
835+
path_prefix = f"{self.path}/" if self.path else ""
836836
async for key in super().list_prefix(full_prefix):
837837
yield key.removeprefix(path_prefix)
838838

src/zarr/storage/_utils.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,53 @@ def _join_paths(paths: Iterable[str]) -> str:
185185
return "/".join(filter(lambda v: v != "", paths))
186186

187187

188+
def _dereference_path(root: str, path: str) -> str:
189+
"""
190+
Combine a store-side root with a key into a single fully-qualified path.
191+
192+
Unlike `_join_paths`, this is purpose-built for the case where `root` is
193+
an opaque backend-side prefix that may use `"/"` as a sentinel for "root
194+
of the filesystem" (notably for fsspec's `ReferenceFileSystem`). A
195+
trailing `"/"` is stripped from `root` before joining; if `root` is then
196+
empty, the bare `path` is returned so that joining `"/"` with `"key"`
197+
yields `"key"` rather than `"//key"`. A trailing `"/"` on the result is
198+
also stripped.
199+
200+
Leading slashes on `root` are preserved -- a backend-side path like
201+
`"/home/foo/data.zarr"` is an absolute filesystem path for
202+
`LocalFileSystem` and must not lose its leading separator.
203+
204+
Parameters
205+
----------
206+
root : str
207+
The backend-side root of a store. May be `""`, `"/"`, an absolute
208+
filesystem path, or a backend-specific prefix.
209+
path : str
210+
The key within the store, typically a zarr key like `"zarr.json"`
211+
or `"a/b/c/zarr.json"`.
212+
213+
Returns
214+
-------
215+
str
216+
`root` and `path` joined by a single `"/"`, with the `"/"` sentinel
217+
collapsed and trailing slashes removed.
218+
219+
Examples
220+
--------
221+
```python
222+
from zarr.storage._utils import _dereference_path
223+
_dereference_path("/", "zarr.json") # 'zarr.json'
224+
_dereference_path("", "zarr.json") # 'zarr.json'
225+
_dereference_path("/home/foo", "zarr.json") # '/home/foo/zarr.json'
226+
_dereference_path("/home/foo/", "zarr.json") # '/home/foo/zarr.json'
227+
_dereference_path("bucket/p", "zarr.json") # 'bucket/p/zarr.json'
228+
```
229+
"""
230+
root = root.rstrip("/")
231+
path = f"{root}/{path}" if root else path
232+
return path.rstrip("/")
233+
234+
188235
def _relativize_path(*, path: str, prefix: str) -> str:
189236
"""
190237
Make a "/"-delimited path relative to some prefix. If the prefix is '', then the path is
@@ -220,10 +267,10 @@ def _relativize_path(*, path: str, prefix: str) -> str:
220267
if prefix == "":
221268
return path
222269
else:
223-
_prefix = prefix + "/"
270+
_prefix = f"{prefix}/"
224271
if not path.startswith(_prefix):
225272
raise ValueError(f"The first component of {path} does not start with {prefix}.")
226-
return path.removeprefix(f"{prefix}/")
273+
return path.removeprefix(_prefix)
227274

228275

229276
def _normalize_paths(paths: Iterable[str]) -> tuple[str, ...]:

src/zarr/storage/_zip.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ async def list_dir(self, prefix: str) -> AsyncIterator[str]:
285285
yield key
286286
else:
287287
for key in keys:
288-
if key.startswith(prefix + "/") and key.strip("/") != prefix:
289-
k = key.removeprefix(prefix + "/").split("/")[0]
288+
if key.startswith(f"{prefix}/") and key.strip("/") != prefix:
289+
k = key.removeprefix(f"{prefix}/").split("/")[0]
290290
if k not in seen:
291291
seen.add(k)
292292
yield k

src/zarr/testing/store.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,8 @@ async def test_list(self, store: S) -> None:
449449
prefix = "foo"
450450
data = self.buffer_cls.from_bytes(b"")
451451
store_dict = {
452-
prefix + "/zarr.json": data,
453-
**{prefix + f"/c/{idx}": data for idx in range(10)},
452+
f"{prefix}/zarr.json": data,
453+
**{f"{prefix}/c/{idx}": data for idx in range(10)},
454454
}
455455
await store._set_many(store_dict.items())
456456
expected_sorted = sorted(store_dict.keys())
@@ -536,10 +536,10 @@ async def test_list_dir(self, store: S) -> None:
536536
await store._set_many(store_dict.items())
537537

538538
keys_observed = await _collect_aiterator(store.list_dir(root))
539-
keys_expected = {k.removeprefix(root + "/").split("/")[0] for k in store_dict}
539+
keys_expected = {k.removeprefix(f"{root}/").split("/")[0] for k in store_dict}
540540
assert sorted(keys_observed) == sorted(keys_expected)
541541

542-
keys_observed = await _collect_aiterator(store.list_dir(root + "/"))
542+
keys_observed = await _collect_aiterator(store.list_dir(f"{root}/"))
543543
assert sorted(keys_expected) == sorted(keys_observed)
544544

545545
async def test_set_if_not_exists(self, store: S) -> None:

src/zarr/testing/strategies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ def arrays(
323323
assert a.fill_value is not None
324324
assert a.name is not None
325325
assert a.path == normalize_path(array_path)
326-
assert a.name == "/" + a.path
326+
assert a.name == f"/{a.path}"
327327
assert isinstance(root[array_path], Array)
328328
assert nparray.shape == a.shape
329329

0 commit comments

Comments
 (0)