Skip to content

Added ArrayNotFoundError #3367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3367.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `zarr.errors.ArrayNotFoundError`, which is raised when attempting to open a zarr array that does not exist, and `zarr.errors.NodeNotFoundError`, which is raised when failing to open an array or a group in a context where either an array or a group was expected.
5 changes: 3 additions & 2 deletions src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata, ArrayV3Metadata
from zarr.errors import (
ArrayNotFoundError,
GroupNotFoundError,
NodeTypeValidationError,
ZarrDeprecationWarning,
Expand Down Expand Up @@ -1257,7 +1258,7 @@ async def open_array(

try:
return await AsyncArray.open(store_path, zarr_format=zarr_format)
except FileNotFoundError:
except FileNotFoundError as err:
if not store_path.read_only and mode in _CREATE_MODES:
overwrite = _infer_overwrite(mode)
_zarr_format = zarr_format or _default_zarr_format()
Expand All @@ -1267,7 +1268,7 @@ async def open_array(
overwrite=overwrite,
**kwargs,
)
raise
raise ArrayNotFoundError(store_path.store, store_path.path) from err


async def open_like(
Expand Down
25 changes: 21 additions & 4 deletions src/zarr/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@
)
from zarr.core.metadata.v3 import parse_node_type_array
from zarr.core.sync import sync
from zarr.errors import MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning
from zarr.errors import (
ArrayNotFoundError,
MetadataValidationError,
ZarrDeprecationWarning,
ZarrUserWarning,
)
from zarr.registry import (
_parse_array_array_codec,
_parse_array_bytes_codec,
Expand Down Expand Up @@ -216,11 +221,19 @@ async def get_array_metadata(
(store_path / ZATTRS_JSON).get(prototype=cpu_buffer_prototype),
)
if zarray_bytes is None:
raise FileNotFoundError(store_path)
msg = (
"A Zarr V2 array metadata document was not found in store "
f"{store_path.store!r} at path {store_path.path!r}."
)
raise ArrayNotFoundError(msg)
elif zarr_format == 3:
zarr_json_bytes = await (store_path / ZARR_JSON).get(prototype=cpu_buffer_prototype)
if zarr_json_bytes is None:
raise FileNotFoundError(store_path)
msg = (
"A Zarr V3 array metadata document was not found in store "
f"{store_path.store!r} at path {store_path.path!r}."
)
raise ArrayNotFoundError(msg)
elif zarr_format is None:
zarr_json_bytes, zarray_bytes, zattrs_bytes = await gather(
(store_path / ZARR_JSON).get(prototype=cpu_buffer_prototype),
Expand All @@ -232,7 +245,11 @@ async def get_array_metadata(
msg = f"Both zarr.json (Zarr format 3) and .zarray (Zarr format 2) metadata objects exist at {store_path}. Zarr v3 will be used."
warnings.warn(msg, category=ZarrUserWarning, stacklevel=1)
if zarr_json_bytes is None and zarray_bytes is None:
raise FileNotFoundError(store_path)
msg = (
f"Neither Zarr V3 nor Zarr V2 array metadata documents "
f"were found in store {store_path.store!r} at path {store_path.path!r}."
)
raise ArrayNotFoundError(msg)
# set zarr_format based on which keys were found
if zarr_json_bytes is not None:
zarr_format = 3
Expand Down
42 changes: 40 additions & 2 deletions src/zarr/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

__all__ = [
"ArrayNotFoundError",
"BaseZarrError",
"ContainsArrayAndGroupError",
"ContainsArrayError",
Expand All @@ -26,12 +27,49 @@ def __init__(self, *args: Any) -> None:
super().__init__(self._msg.format(*args))


class GroupNotFoundError(BaseZarrError, FileNotFoundError):
class NodeNotFoundError(BaseZarrError, FileNotFoundError):
"""
Raised when a node (array or group) is not found at a certain path.
"""

def __init__(self, *args: Any) -> None:
if len(args) == 1:
# Pre-formatted message
super(BaseZarrError, self).__init__(args[0])
else:
# Store and path arguments - format them
_msg = "No node found in store {!r} at path {!r}"
super(BaseZarrError, self).__init__(_msg.format(*args))


class ArrayNotFoundError(NodeNotFoundError):
"""
Raised when an array isn't found at a certain path.
"""

def __init__(self, *args: Any) -> None:
if len(args) == 1:
# Pre-formatted message
super(BaseZarrError, self).__init__(args[0])
else:
# Store and path arguments - format them
_msg = "No array found in store {!r} at path {!r}"
super(BaseZarrError, self).__init__(_msg.format(*args))


class GroupNotFoundError(NodeNotFoundError):
"""
Raised when a group isn't found at a certain path.
"""

_msg = "No group found in store {!r} at path {!r}"
def __init__(self, *args: Any) -> None:
if len(args) == 1:
# Pre-formatted message
super(BaseZarrError, self).__init__(args[0])
else:
# Store and path arguments - format them
_msg = "No group found in store {!r} at path {!r}"
super(BaseZarrError, self).__init__(_msg.format(*args))
Comment on lines +65 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a separate PR we should remove the pre-formatting of exception messages, and make every exception just take a single string



class ContainsGroupError(BaseZarrError):
Expand Down
41 changes: 32 additions & 9 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import zarr.codecs
import zarr.storage
from zarr.core.array import init_array
from zarr.core.array import AsyncArray, init_array
from zarr.storage import LocalStore, ZipStore
from zarr.storage._common import StorePath

Expand Down Expand Up @@ -42,7 +42,13 @@
save_group,
)
from zarr.core.buffer import NDArrayLike
from zarr.errors import MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning
from zarr.errors import (
ArrayNotFoundError,
MetadataValidationError,
NodeNotFoundError,
ZarrDeprecationWarning,
ZarrUserWarning,
)
from zarr.storage import MemoryStore
from zarr.storage._utils import normalize_path
from zarr.testing.utils import gpu_test
Expand Down Expand Up @@ -70,11 +76,11 @@ def test_create(memory_store: Store) -> None:

# create array with float shape
with pytest.raises(TypeError):
z = create(shape=(400.5, 100), store=store, overwrite=True) # type: ignore [arg-type]
z = create(shape=(400.5, 100), store=store, overwrite=True) # type: ignore[arg-type]

# create array with float chunk shape
with pytest.raises(TypeError):
z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore [arg-type]
z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type]


# TODO: parametrize over everything this function takes
Expand Down Expand Up @@ -185,10 +191,27 @@ async def test_open_array(memory_store: MemoryStore, zarr_format: ZarrFormat) ->
assert z.read_only

# path not found
with pytest.raises(FileNotFoundError):
with pytest.raises(NodeNotFoundError):
zarr.api.synchronous.open(store="doesnotexist", mode="r", zarr_format=zarr_format)


@pytest.mark.asyncio
async def test_async_array_open_array_not_found() -> None:
"""Test that AsyncArray.open raises ArrayNotFoundError when array doesn't exist"""
store = MemoryStore()
# Try to open an array that does not exist
with pytest.raises(ArrayNotFoundError):
await AsyncArray.open(store, zarr_format=2)


def test_array_open_array_not_found_sync() -> None:
"""Test that Array.open raises ArrayNotFoundError when array doesn't exist"""
store = MemoryStore()
# Try to open an array that does not exist
with pytest.raises(ArrayNotFoundError):
Array.open(store)


@pytest.mark.parametrize("store", ["memory", "local", "zip"], indirect=True)
def test_v2_and_v3_exist_at_same_path(store: Store) -> None:
zarr.create_array(store, shape=(10,), dtype="uint8", zarr_format=3)
Expand Down Expand Up @@ -266,7 +289,7 @@ def test_save(store: Store, n_args: int, n_kwargs: int, path: None | str) -> Non
assert isinstance(array, Array)
assert_array_equal(array[:], data)
else:
save(store, *args, path=path, **kwargs) # type: ignore [arg-type]
save(store, *args, path=path, **kwargs) # type: ignore[arg-type]
group = zarr.api.synchronous.open(store, path=path)
assert isinstance(group, Group)
for array in group.array_values():
Expand Down Expand Up @@ -1208,13 +1231,13 @@ async def test_metadata_validation_error() -> None:
MetadataValidationError,
match="Invalid value for 'zarr_format'. Expected '2, 3, or None'. Got '3.0'.",
):
await zarr.api.asynchronous.open_group(zarr_format="3.0") # type: ignore [arg-type]
await zarr.api.asynchronous.open_group(zarr_format="3.0") # type: ignore[arg-type]

with pytest.raises(
MetadataValidationError,
match="Invalid value for 'zarr_format'. Expected '2, 3, or None'. Got '3.0'.",
):
await zarr.api.asynchronous.open_array(shape=(1,), zarr_format="3.0") # type: ignore [arg-type]
await zarr.api.asynchronous.open_array(shape=(1,), zarr_format="3.0") # type: ignore[arg-type]


@pytest.mark.parametrize(
Expand All @@ -1224,7 +1247,7 @@ async def test_metadata_validation_error() -> None:
)
def test_open_array_with_mode_r_plus(store: Store, zarr_format: ZarrFormat) -> None:
# 'r+' means read/write (must exist)
with pytest.raises(FileNotFoundError):
with pytest.raises(ArrayNotFoundError):
zarr.open_array(store=store, mode="r+", zarr_format=zarr_format)
zarr.ones(store=store, shape=(3, 3), zarr_format=zarr_format)
z2 = zarr.open_array(store=store, mode="r+")
Expand Down
Loading