Skip to content

Commit e331a3b

Browse files
committed
simplify storepath creation
1 parent 85af0bd commit e331a3b

File tree

5 files changed

+74
-41
lines changed

5 files changed

+74
-41
lines changed

src/zarr/api/asynchronous.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,7 @@ async def consolidate_metadata(
170170
The group, with the ``consolidated_metadata`` field set to include
171171
the metadata of each child node.
172172
"""
173-
store_path = await make_store_path(store)
174-
175-
if path is not None:
176-
store_path = store_path / path
173+
store_path = await make_store_path(store, path=path)
177174

178175
group = await AsyncGroup.open(store_path, zarr_format=zarr_format, use_consolidated=False)
179176
group.store_path.store._check_writable()
@@ -291,10 +288,7 @@ async def open(
291288
"""
292289
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
293290

294-
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
295-
296-
if path is not None:
297-
store_path = store_path / path
291+
store_path = await make_store_path(store, mode=mode, path=path, storage_options=storage_options)
298292

299293
if "shape" not in kwargs and mode in {"a", "w", "w-"}:
300294
try:
@@ -401,9 +395,7 @@ async def save_array(
401395
)
402396

403397
mode = kwargs.pop("mode", None)
404-
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
405-
if path is not None:
406-
store_path = store_path / path
398+
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
407399
new = await AsyncArray.create(
408400
store_path,
409401
zarr_format=zarr_format,
@@ -582,9 +574,7 @@ async def group(
582574

583575
mode = None if isinstance(store, Store) else cast(AccessModeLiteral, "a")
584576

585-
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
586-
if path is not None:
587-
store_path = store_path / path
577+
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
588578

589579
if chunk_store is not None:
590580
warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2)
@@ -697,9 +687,7 @@ async def open_group(
697687
if chunk_store is not None:
698688
warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2)
699689

700-
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
701-
if path is not None:
702-
store_path = store_path / path
690+
store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path)
703691

704692
if attributes is None:
705693
attributes = {}
@@ -883,9 +871,7 @@ async def create(
883871
if not isinstance(store, Store | StorePath):
884872
mode = "a"
885873

886-
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
887-
if path is not None:
888-
store_path = store_path / path
874+
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
889875

890876
return await AsyncArray.create(
891877
store_path,
@@ -925,6 +911,7 @@ async def empty(
925911
retrieve data from an empty Zarr array, any values may be returned,
926912
and these are not guaranteed to be stable from one access to the next.
927913
"""
914+
928915
return await create(shape=shape, fill_value=None, **kwargs)
929916

930917

@@ -1044,7 +1031,7 @@ async def open_array(
10441031
store: StoreLike | None = None,
10451032
zarr_version: ZarrFormat | None = None, # deprecated
10461033
zarr_format: ZarrFormat | None = None,
1047-
path: PathLike | None = None,
1034+
path: PathLike = "",
10481035
storage_options: dict[str, Any] | None = None,
10491036
**kwargs: Any, # TODO: type kwargs as valid args to save
10501037
) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]:
@@ -1071,9 +1058,7 @@ async def open_array(
10711058
"""
10721059

10731060
mode = kwargs.pop("mode", None)
1074-
store_path = await make_store_path(store, mode=mode)
1075-
if path is not None:
1076-
store_path = store_path / path
1061+
store_path = await make_store_path(store, path=path, mode=mode)
10771062

10781063
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
10791064

src/zarr/storage/_utils.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
34
from typing import TYPE_CHECKING
45

6+
from upath import UPath
7+
58
if TYPE_CHECKING:
69
from zarr.core.buffer import Buffer
710

811

9-
def normalize_path(path: str | bytes | object) -> str:
12+
def normalize_path(path: str | bytes | Path | UPath | None) -> str:
1013
# handle bytes
11-
if isinstance(path, bytes):
14+
if path is None:
15+
result = ""
16+
elif isinstance(path, bytes):
1217
result = str(path, "ascii")
13-
1418
# ensure str
15-
elif not isinstance(path, str):
19+
elif isinstance(path, Path | UPath):
1620
result = str(path)
1721

18-
else:
22+
elif isinstance(path, str):
1923
result = path
2024

25+
else:
26+
raise TypeError(f'Object {path} has an invalid type for "path": {type(path).__name__}')
27+
2128
# convert backslash to forward slash
2229
result = result.replace("\\", "/")
2330

@@ -44,7 +51,7 @@ def normalize_path(path: str | bytes | object) -> str:
4451
segments = result.split("/")
4552
if any(s in {".", ".."} for s in segments):
4653
raise ValueError(
47-
f"The path {path} is invalid because its string representation contains '.' or '..' segments."
54+
f"The path {path!r} is invalid because its string representation contains '.' or '..' segments."
4855
)
4956

5057
return result

src/zarr/storage/common.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from zarr.core.buffer import Buffer, default_buffer_prototype
99
from zarr.core.common import ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, ZarrFormat
1010
from zarr.errors import ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError
11+
from zarr.storage._utils import normalize_path
1112
from zarr.storage.local import LocalStore
1213
from zarr.storage.memory import MemoryStore
1314

@@ -41,9 +42,9 @@ class StorePath:
4142
store: Store
4243
path: str
4344

44-
def __init__(self, store: Store, path: str | None = None) -> None:
45+
def __init__(self, store: Store, path: str = "") -> None:
4546
self.store = store
46-
self.path = path or ""
47+
self.path = path
4748

4849
async def get(
4950
self,
@@ -159,6 +160,7 @@ def __eq__(self, other: object) -> bool:
159160
async def make_store_path(
160161
store_like: StoreLike | None,
161162
*,
163+
path: str | None = "",
162164
mode: AccessModeLiteral | None = None,
163165
storage_options: dict[str, Any] | None = None,
164166
) -> StorePath:
@@ -189,6 +191,9 @@ async def make_store_path(
189191
----------
190192
store_like : StoreLike | None
191193
The object to convert to a `StorePath` object.
194+
path: str | None, optional
195+
The path to use when creating the `StorePath` object. If None, the
196+
default path is the empty string.
192197
mode : AccessModeLiteral | None, optional
193198
The mode to use when creating the `StorePath` object. If None, the
194199
default mode is 'r'.
@@ -209,37 +214,44 @@ async def make_store_path(
209214
from zarr.storage.remote import RemoteStore # circular import
210215

211216
used_storage_options = False
212-
217+
path_normalized = normalize_path(path)
213218
if isinstance(store_like, StorePath):
214219
if mode is not None and mode != store_like.store.mode.str:
215220
_store = store_like.store.with_mode(mode)
216221
await _store._ensure_open()
217-
store_like = StorePath(_store)
218-
result = store_like
222+
store_like = StorePath(_store, path=store_like.path) / path_normalized
223+
result = store_like / path_normalized
219224
elif isinstance(store_like, Store):
220225
if mode is not None and mode != store_like.mode.str:
221226
store_like = store_like.with_mode(mode)
222227
await store_like._ensure_open()
223-
result = StorePath(store_like)
228+
result = StorePath(store_like, path=path_normalized)
224229
elif store_like is None:
225230
# mode = "w" is an exception to the default mode = 'r'
226-
result = StorePath(await MemoryStore.open(mode=mode or "w"))
231+
result = StorePath(await MemoryStore.open(mode=mode or "w"), path=path_normalized)
227232
elif isinstance(store_like, Path):
228-
result = StorePath(await LocalStore.open(root=store_like, mode=mode or "r"))
233+
result = StorePath(
234+
await LocalStore.open(root=store_like, mode=mode or "r"), path=path_normalized
235+
)
229236
elif isinstance(store_like, str):
230237
storage_options = storage_options or {}
231238

232239
if _is_fsspec_uri(store_like):
233240
used_storage_options = True
234241
result = StorePath(
235-
RemoteStore.from_url(store_like, storage_options=storage_options, mode=mode or "r")
242+
RemoteStore.from_url(store_like, storage_options=storage_options, mode=mode or "r"),
243+
path=path_normalized,
236244
)
237245
else:
238-
result = StorePath(await LocalStore.open(root=Path(store_like), mode=mode or "r"))
246+
result = StorePath(
247+
await LocalStore.open(root=Path(store_like), mode=mode or "r"), path=path_normalized
248+
)
239249
elif isinstance(store_like, dict):
240250
# We deliberate only consider dict[str, Buffer] here, and not arbitrary mutable mappings.
241251
# By only allowing dictionaries, which are in-memory, we know that MemoryStore appropriate.
242-
result = StorePath(await MemoryStore.open(store_dict=store_like, mode=mode or "r"))
252+
result = StorePath(
253+
await MemoryStore.open(store_dict=store_like, mode=mode or "r"), path=path_normalized
254+
)
243255
else:
244256
msg = f"Unsupported type for store_like: '{type(store_like).__name__}'" # type: ignore[unreachable]
245257
raise TypeError(msg)

tests/v3/test_group.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ def test_group_array_creation(
587587
assert empty_array.fill_value == 0
588588
assert empty_array.shape == shape
589589
assert empty_array.store_path.store == store
590+
assert empty_array.store_path.path == "empty"
590591

591592
empty_like_array = group.empty_like(name="empty_like", data=empty_array)
592593
assert isinstance(empty_like_array, Array)

tests/v3/test_store/test_core.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from pathlib import Path
33

44
import pytest
5+
from upath import UPath
56

7+
from zarr.storage._utils import normalize_path
68
from zarr.storage.common import StoreLike, StorePath, make_store_path
79
from zarr.storage.local import LocalStore
810
from zarr.storage.memory import MemoryStore
@@ -65,3 +67,29 @@ async def test_make_store_path_storage_options_raises(store_like: StoreLike) ->
6567
async def test_unsupported() -> None:
6668
with pytest.raises(TypeError, match="Unsupported type for store_like: 'int'"):
6769
await make_store_path(1) # type: ignore[arg-type]
70+
71+
72+
@pytest.mark.parametrize(
73+
"path",
74+
[
75+
"/foo/bar",
76+
"//foo/bar",
77+
"foo///bar",
78+
"foo/bar///",
79+
Path("foo/bar"),
80+
b"foo/bar",
81+
UPath("foo/bar"),
82+
],
83+
)
84+
def test_normalize_path_valid(path: str | bytes | Path | UPath) -> None:
85+
assert normalize_path(path) == "foo/bar"
86+
87+
88+
def test_normalize_path_none():
89+
assert normalize_path(None) == ""
90+
91+
92+
@pytest.mark.parametrize("path", [".", ".."])
93+
def test_normalize_path_invalid(path: str):
94+
with pytest.raises(ValueError):
95+
normalize_path(path)

0 commit comments

Comments
 (0)