|
23 | 23 | ) |
24 | 24 | from zarr.errors import ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError |
25 | 25 | from zarr.storage._local import LocalStore |
26 | | -from zarr.storage._memory import MemoryStore |
27 | | -from zarr.storage._utils import normalize_path |
| 26 | +from zarr.storage._memory import ManagedMemoryStore, MemoryStore |
| 27 | +from zarr.storage._utils import _join_paths, normalize_path, parse_store_url |
28 | 28 |
|
29 | 29 | _has_fsspec = importlib.util.find_spec("fsspec") |
30 | 30 | if _has_fsspec: |
|
36 | 36 | from zarr.core.buffer import BufferPrototype |
37 | 37 |
|
38 | 38 |
|
39 | | -def _dereference_path(root: str, path: str) -> str: |
40 | | - if not isinstance(root, str): |
41 | | - msg = f"{root=} is not a string ({type(root)=})" # type: ignore[unreachable] |
42 | | - raise TypeError(msg) |
43 | | - if not isinstance(path, str): |
44 | | - msg = f"{path=} is not a string ({type(path)=})" # type: ignore[unreachable] |
45 | | - raise TypeError(msg) |
46 | | - root = root.rstrip("/") |
47 | | - path = f"{root}/{path}" if root else path |
48 | | - return path.rstrip("/") |
49 | | - |
50 | | - |
51 | 39 | class StorePath: |
52 | 40 | """ |
53 | 41 | Path-like interface for a Store. |
@@ -267,10 +255,10 @@ def delete_sync(self) -> None: |
267 | 255 |
|
268 | 256 | def __truediv__(self, other: str) -> StorePath: |
269 | 257 | """Combine this store path with another path""" |
270 | | - return self.__class__(self.store, _dereference_path(self.path, other)) |
| 258 | + return self.__class__(self.store, _join_paths([self.path, other])) |
271 | 259 |
|
272 | 260 | def __str__(self) -> str: |
273 | | - return _dereference_path(str(self.store), self.path) |
| 261 | + return _join_paths([str(self.store), self.path]) |
274 | 262 |
|
275 | 263 | def __repr__(self) -> str: |
276 | 264 | return f"StorePath({self.store.__class__.__name__}, '{self}')" |
@@ -342,14 +330,17 @@ async def make_store( |
342 | 330 | """ |
343 | 331 | from zarr.storage._fsspec import FsspecStore # circular import |
344 | 332 |
|
345 | | - if ( |
346 | | - not (isinstance(store_like, str) and _is_fsspec_uri(store_like)) |
347 | | - and storage_options is not None |
348 | | - ): |
349 | | - raise TypeError( |
350 | | - "'storage_options' was provided but unused. " |
351 | | - "'storage_options' is only used when the store is passed as an FSSpec URI string.", |
352 | | - ) |
| 333 | + # Parse URL early so we can reuse the result for both validation and routing |
| 334 | + parsed = parse_store_url(store_like) if isinstance(store_like, str) else None |
| 335 | + |
| 336 | + # Check if storage_options is valid for this store_like |
| 337 | + if storage_options is not None: |
| 338 | + is_fsspec_uri = parsed is not None and parsed.scheme not in ("", "memory", "file") |
| 339 | + if not is_fsspec_uri: |
| 340 | + raise TypeError( |
| 341 | + "'storage_options' was provided but unused. " |
| 342 | + "'storage_options' is only used when the store is passed as an FSSpec URI string.", |
| 343 | + ) |
353 | 344 |
|
354 | 345 | assert mode in (None, "r", "r+", "a", "w", "w-") |
355 | 346 | _read_only = mode == "r" |
@@ -377,15 +368,18 @@ async def make_store( |
377 | 368 | # Create a new LocalStore |
378 | 369 | return await LocalStore.open(root=store_like, mode=mode, read_only=_read_only) |
379 | 370 |
|
380 | | - elif isinstance(store_like, str): |
381 | | - # Either an FSSpec URI or a local filesystem path |
382 | | - if _is_fsspec_uri(store_like): |
| 371 | + elif isinstance(store_like, str) and parsed is not None: |
| 372 | + if parsed.scheme == "memory": |
| 373 | + # Create or get a ManagedMemoryStore |
| 374 | + return ManagedMemoryStore(name=parsed.name, path=parsed.path, read_only=_read_only) |
| 375 | + elif parsed.scheme == "file" or not parsed.scheme: |
| 376 | + # Local filesystem path — use parsed.path to strip the file:// scheme |
| 377 | + return await make_store(Path(parsed.path), mode=mode, storage_options=storage_options) |
| 378 | + else: |
| 379 | + # Assume fsspec can handle it (s3://, gs://, http://, etc.) |
383 | 380 | return FsspecStore.from_url( |
384 | 381 | store_like, storage_options=storage_options, read_only=_read_only |
385 | 382 | ) |
386 | | - else: |
387 | | - # Assume a filesystem path |
388 | | - return await make_store(Path(store_like), mode=mode, storage_options=storage_options) |
389 | 383 |
|
390 | 384 | elif _has_fsspec and isinstance(store_like, FSMap): |
391 | 385 | return FsspecStore.from_mapper(store_like, read_only=_read_only) |
@@ -460,25 +454,6 @@ async def make_store_path( |
460 | 454 | return await StorePath.open(store, path=path_normalized, mode=mode) |
461 | 455 |
|
462 | 456 |
|
463 | | -def _is_fsspec_uri(uri: str) -> bool: |
464 | | - """ |
465 | | - Check if a URI looks like a non-local fsspec URI. |
466 | | -
|
467 | | - Examples |
468 | | - -------- |
469 | | - ```python |
470 | | - from zarr.storage._common import _is_fsspec_uri |
471 | | - _is_fsspec_uri("s3://bucket") |
472 | | - # True |
473 | | - _is_fsspec_uri("my-directory") |
474 | | - # False |
475 | | - _is_fsspec_uri("local://my-directory") |
476 | | - # False |
477 | | - ``` |
478 | | - """ |
479 | | - return "://" in uri or ("::" in uri and "local://" not in uri) |
480 | | - |
481 | | - |
482 | 457 | async def ensure_no_existing_node( |
483 | 458 | store_path: StorePath, |
484 | 459 | zarr_format: ZarrFormat, |
|
0 commit comments