diff --git a/changes/3367.bugfix.rst b/changes/3367.bugfix.rst new file mode 100644 index 0000000000..9edef00ca6 --- /dev/null +++ b/changes/3367.bugfix.rst @@ -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. \ No newline at end of file diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index d3613f7c05..fdbd3b34bd 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -39,6 +39,7 @@ ) from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata, ArrayV3Metadata from zarr.errors import ( + ArrayNotFoundError, GroupNotFoundError, NodeTypeValidationError, ZarrDeprecationWarning, @@ -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() @@ -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( diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 4fcddaa8b5..7b3f11d8cd 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -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, @@ -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), @@ -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 diff --git a/src/zarr/errors.py b/src/zarr/errors.py index 0055ea3c6c..867e801e18 100644 --- a/src/zarr/errors.py +++ b/src/zarr/errors.py @@ -1,6 +1,7 @@ from typing import Any __all__ = [ + "ArrayNotFoundError", "BaseZarrError", "ContainsArrayAndGroupError", "ContainsArrayError", @@ -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)) class ContainsGroupError(BaseZarrError): diff --git a/tests/test_api.py b/tests/test_api.py index 5447a0aa39..3668ef306a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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 @@ -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 @@ -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 @@ -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) @@ -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(): @@ -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( @@ -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+")