diff --git a/changes/3098.misc.rst b/changes/3098.misc.rst new file mode 100644 index 0000000000..5ec1057365 --- /dev/null +++ b/changes/3098.misc.rst @@ -0,0 +1,3 @@ +Define Zarr-specific warning classes that subclass the Python built-in warnings. +These classes makes it easier to control the visibility of warnings emitted by Zarr Python. +See `zarr.errors` for these warning classes. \ No newline at end of file diff --git a/docs/user-guide/arrays.rst b/docs/user-guide/arrays.rst index 67b134d442..257fac450c 100644 --- a/docs/user-guide/arrays.rst +++ b/docs/user-guide/arrays.rst @@ -238,6 +238,8 @@ built-in delta filter:: >>> import lzma >>> from numcodecs.zarr3 import LZMA + >>> import warnings + >>> warnings.filterwarnings("ignore", category=UserWarning) >>> >>> lzma_filters = [dict(id=lzma.FILTER_DELTA, dist=4), dict(id=lzma.FILTER_LZMA2, preset=1)] >>> compressors = LZMA(filters=lzma_filters) diff --git a/docs/user-guide/consolidated_metadata.rst b/docs/user-guide/consolidated_metadata.rst index 05a3aa7fb4..9d05231f4a 100644 --- a/docs/user-guide/consolidated_metadata.rst +++ b/docs/user-guide/consolidated_metadata.rst @@ -27,6 +27,8 @@ In Python, the consolidated metadata is available on the ``.consolidated_metadat attribute of the ``GroupMetadata`` object. >>> import zarr + >>> import warnings + >>> warnings.filterwarnings("ignore", category=UserWarning) >>> >>> store = zarr.storage.MemoryStore() >>> group = zarr.create_group(store=store) diff --git a/pyproject.toml b/pyproject.toml index 7bbf6cc2a0..95528c4558 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -389,16 +389,6 @@ addopts = [ ] filterwarnings = [ "error", - # TODO: explicitly filter or catch the warnings below where we expect them to be emitted in the tests - "ignore:Consolidated metadata is currently not part in the Zarr format 3 specification.*:UserWarning", - "ignore:Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__.*:UserWarning", - "ignore:Automatic shard shape inference is experimental and may change without notice.*:UserWarning", - "ignore:The codec .* is currently not part in the Zarr format 3 specification.*:UserWarning", - "ignore:The dtype .* is currently not part in the Zarr format 3 specification.*:UserWarning", - "ignore:Use zarr.create_array instead.:DeprecationWarning", - "ignore:Duplicate name.*:UserWarning", - "ignore:The `compressor` argument is deprecated. Use `compressors` instead.:UserWarning", - "ignore:Numcodecs codecs are not in the Zarr version 3 specification and may not be supported by other zarr implementations.:UserWarning", "ignore:Unclosed client session T: f"{version} passing these as positional arguments " "will result in an error" ), - FutureWarning, + ZarrFutureWarning, stacklevel=2, ) kwargs.update(zip(sig.parameters, args, strict=False)) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index dee96ffdee..78b68caf73 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -39,7 +39,13 @@ create_hierarchy, ) from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata, ArrayV3Metadata -from zarr.errors import GroupNotFoundError, NodeTypeValidationError +from zarr.errors import ( + GroupNotFoundError, + NodeTypeValidationError, + ZarrDeprecationWarning, + ZarrRuntimeWarning, + ZarrUserWarning, +) from zarr.storage import StorePath from zarr.storage._common import make_store_path @@ -162,7 +168,7 @@ def _handle_zarr_version_or_format( ) if zarr_version is not None: warnings.warn( - "zarr_version is deprecated, use zarr_format", DeprecationWarning, stacklevel=2 + "zarr_version is deprecated, use zarr_format", ZarrDeprecationWarning, stacklevel=2 ) return zarr_version return zarr_format @@ -228,7 +234,7 @@ async def consolidate_metadata( warnings.warn( "Consolidated metadata is currently not part in the Zarr format 3 specification. It " "may not be supported by other zarr implementations and may change in the future.", - category=UserWarning, + category=ZarrUserWarning, stacklevel=1, ) @@ -536,7 +542,7 @@ async def save_group( await asyncio.gather(*aws) -@deprecated("Use AsyncGroup.tree instead.") +@deprecated("Use AsyncGroup.tree instead.", category=ZarrDeprecationWarning) async def tree(grp: AsyncGroup, expand: bool | None = None, level: int | None = None) -> Any: """Provide a rich display of the hierarchy. @@ -674,13 +680,13 @@ async def group( store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) if chunk_store is not None: - warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if cache_attrs is not None: - warnings.warn("cache_attrs is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if synchronizer is not None: - warnings.warn("synchronizer is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if meta_array is not None: - warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if attributes is None: attributes = {} @@ -827,13 +833,13 @@ async def open_group( zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format) if cache_attrs is not None: - warnings.warn("cache_attrs is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if synchronizer is not None: - warnings.warn("synchronizer is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if meta_array is not None: - warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if chunk_store is not None: - warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path) if attributes is None: @@ -1011,19 +1017,19 @@ async def create( ) if synchronizer is not None: - warnings.warn("synchronizer is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if chunk_store is not None: - warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if cache_metadata is not None: - warnings.warn("cache_metadata is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("cache_metadata is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if cache_attrs is not None: - warnings.warn("cache_attrs is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if object_codec is not None: - warnings.warn("object_codec is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("object_codec is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if read_only is not None: - warnings.warn("read_only is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("read_only is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if meta_array is not None: - warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2) + warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if write_empty_chunks is not None: _warn_write_empty_chunks_kwarg() @@ -1042,7 +1048,7 @@ async def create( "This is redundant. When both are set, write_empty_chunks will be used instead " "of the value in config." ) - warnings.warn(UserWarning(msg), stacklevel=1) + warnings.warn(ZarrUserWarning(msg), stacklevel=1) config_parsed = dataclasses.replace(config_parsed, write_empty_chunks=write_empty_chunks) return await AsyncArray._create( diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 4ce02e7b6d..ed1ae2cf2a 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -10,6 +10,7 @@ from zarr.core.group import Group from zarr.core.sync import sync from zarr.core.sync_group import create_hierarchy +from zarr.errors import ZarrDeprecationWarning if TYPE_CHECKING: from collections.abc import Iterable @@ -339,7 +340,7 @@ def save_group( ) -@deprecated("Use Group.tree instead.") +@deprecated("Use Group.tree instead.", category=ZarrDeprecationWarning) def tree(grp: Group, expand: bool | None = None, level: int | None = None) -> Any: """Provide a rich display of the hierarchy. diff --git a/src/zarr/codecs/transpose.py b/src/zarr/codecs/transpose.py index c87804685c..d6310d38a4 100644 --- a/src/zarr/codecs/transpose.py +++ b/src/zarr/codecs/transpose.py @@ -56,7 +56,7 @@ def validate( ) -> None: if len(self.order) != len(shape): raise ValueError( - f"The `order` tuple needs have as many entries as there are dimensions in the array. Got {self.order}." + f"The `order` tuple must have as many entries as there are dimensions in the array. Got {self.order}." ) if len(self.order) != len(set(self.order)): raise ValueError( @@ -71,7 +71,7 @@ def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: ndim = array_spec.ndim if len(self.order) != ndim: raise ValueError( - f"The `order` tuple needs have as many entries as there are dimensions in the array. Got {self.order}." + f"The `order` tuple must have as many entries as there are dimensions in the array. Got {self.order}." ) if len(self.order) != len(set(self.order)): raise ValueError( diff --git a/src/zarr/convenience.py b/src/zarr/convenience.py index 88f10663b7..3ca4ffcb4b 100644 --- a/src/zarr/convenience.py +++ b/src/zarr/convenience.py @@ -22,6 +22,7 @@ save_group, tree, ) +from zarr.errors import ZarrDeprecationWarning __all__ = [ "consolidate_metadata", @@ -40,6 +41,6 @@ warnings.warn( "zarr.convenience is deprecated. " "Import these functions from the top level zarr. namespace instead.", - DeprecationWarning, + ZarrDeprecationWarning, stacklevel=2, ) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 260e94bc88..ca8bc414cc 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -119,7 +119,7 @@ ) from zarr.core.metadata.v3 import parse_node_type_array from zarr.core.sync import sync -from zarr.errors import MetadataValidationError +from zarr.errors import MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning from zarr.registry import ( _parse_array_array_codec, _parse_array_bytes_codec, @@ -232,7 +232,7 @@ async def get_array_metadata( if zarr_json_bytes is not None and zarray_bytes is not None: # warn and favor v3 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, stacklevel=1) + warnings.warn(msg, category=ZarrUserWarning, stacklevel=1) if zarr_json_bytes is None and zarray_bytes is None: raise FileNotFoundError(store_path) # set zarr_format based on which keys were found @@ -441,7 +441,7 @@ async def create( ) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]: ... @classmethod - @deprecated("Use zarr.api.asynchronous.create_array instead.") + @deprecated("Use zarr.api.asynchronous.create_array instead.", category=ZarrDeprecationWarning) async def create( cls, store: StoreLike, @@ -1061,7 +1061,7 @@ def serializer(self) -> ArrayBytesCodec | None: ) @property - @deprecated("Use AsyncArray.compressors instead.") + @deprecated("Use AsyncArray.compressors instead.", category=ZarrDeprecationWarning) def compressor(self) -> numcodecs.abc.Codec | None: """ Compressor that is applied to each chunk of the array. @@ -1855,7 +1855,7 @@ class Array: _async_array: AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] @classmethod - @deprecated("Use zarr.create_array instead.") + @deprecated("Use zarr.create_array instead.", category=ZarrDeprecationWarning) def create( cls, store: StoreLike, @@ -2242,7 +2242,7 @@ def serializer(self) -> None | ArrayBytesCodec: return self._async_array.serializer @property - @deprecated("Use Array.compressors instead.") + @deprecated("Use Array.compressors instead.", category=ZarrDeprecationWarning) def compressor(self) -> numcodecs.abc.Codec | None: """ Compressor that is applied to each chunk of the array. @@ -4648,7 +4648,7 @@ def _parse_keep_array_attr( warnings.warn( "The 'order' attribute of a Zarr format 2 array does not have a direct analogue in Zarr format 3. " "The existing order='F' of the source Zarr format 2 array will be ignored.", - UserWarning, + ZarrUserWarning, stacklevel=2, ) elif order is None and zarr_format == 2: @@ -4937,7 +4937,7 @@ def _parse_deprecated_compressor( if zarr_format == 3: warn( "The `compressor` argument is deprecated. Use `compressors` instead.", - category=UserWarning, + category=ZarrUserWarning, stacklevel=2, ) if compressor is None: diff --git a/src/zarr/core/buffer/gpu.py b/src/zarr/core/buffer/gpu.py index d46ee6c8e5..4eca197222 100644 --- a/src/zarr/core/buffer/gpu.py +++ b/src/zarr/core/buffer/gpu.py @@ -13,6 +13,7 @@ from zarr.core.buffer import core from zarr.core.buffer.core import ArrayLike, BufferPrototype, NDArrayLike +from zarr.errors import ZarrUserWarning from zarr.registry import ( register_buffer, register_ndbuffer, @@ -72,6 +73,7 @@ def __init__(self, array_like: ArrayLike) -> None: ) warnings.warn( msg, + category=ZarrUserWarning, stacklevel=2, ) self._data = cp.asarray(array_like) diff --git a/src/zarr/core/chunk_grids.py b/src/zarr/core/chunk_grids.py index 6a3d6816a6..7fa1fc7e38 100644 --- a/src/zarr/core/chunk_grids.py +++ b/src/zarr/core/chunk_grids.py @@ -22,6 +22,7 @@ parse_named_configuration, parse_shapelike, ) +from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from collections.abc import Iterator @@ -233,7 +234,7 @@ def _auto_partition( if shard_shape == "auto": warnings.warn( "Automatic shard shape inference is experimental and may change without notice.", - UserWarning, + ZarrUserWarning, stacklevel=2, ) _shards_out = () diff --git a/src/zarr/core/codec_pipeline.py b/src/zarr/core/codec_pipeline.py index 23c27e40c6..3bc3c1cfc7 100644 --- a/src/zarr/core/codec_pipeline.py +++ b/src/zarr/core/codec_pipeline.py @@ -17,6 +17,7 @@ from zarr.core.common import ChunkCoords, concurrent_map from zarr.core.config import config from zarr.core.indexing import SelectorTuple, is_scalar +from zarr.errors import ZarrUserWarning from zarr.registry import register_pipeline if TYPE_CHECKING: @@ -501,6 +502,7 @@ def codecs_from_list( warn( "Combining a `sharding_indexed` codec disables partial reads and " "writes, which may lead to inefficient performance.", + category=ZarrUserWarning, stacklevel=3, ) diff --git a/src/zarr/core/common.py b/src/zarr/core/common.py index 33590c83a5..4c0247426e 100644 --- a/src/zarr/core/common.py +++ b/src/zarr/core/common.py @@ -23,6 +23,7 @@ from typing_extensions import ReadOnly from zarr.core.config import config as zarr_config +from zarr.errors import ZarrRuntimeWarning if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Iterator @@ -205,7 +206,7 @@ def _warn_write_empty_chunks_kwarg() -> None: "argument, as in `config={'write_empty_chunks': True}`," "or change the global 'array.write_empty_chunks' configuration variable." ) - warnings.warn(msg, RuntimeWarning, stacklevel=2) + warnings.warn(msg, ZarrRuntimeWarning, stacklevel=2) def _warn_order_kwarg() -> None: @@ -216,7 +217,7 @@ def _warn_order_kwarg() -> None: "argument, as in `config={'order': 'C'}`," "or change the global 'array.order' configuration variable." ) - warnings.warn(msg, RuntimeWarning, stacklevel=2) + warnings.warn(msg, ZarrRuntimeWarning, stacklevel=2) def _default_zarr_format() -> ZarrFormat: diff --git a/src/zarr/core/dtype/common.py b/src/zarr/core/dtype/common.py index 3cc31df9e3..652b5fdbe3 100644 --- a/src/zarr/core/dtype/common.py +++ b/src/zarr/core/dtype/common.py @@ -16,6 +16,7 @@ from typing_extensions import ReadOnly from zarr.core.common import NamedConfig +from zarr.errors import UnstableSpecificationWarning EndiannessStr = Literal["little", "big"] ENDIANNESS_STR: Final = "little", "big" @@ -216,9 +217,6 @@ class HasObjectCodec: object_codec_id: ClassVar[str] -class UnstableSpecificationWarning(FutureWarning): ... - - def v3_unstable_dtype_warning(dtype: object) -> None: """ Emit this warning when a data type does not have a stable zarr v3 spec diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index f18c723a76..4bdc7b549f 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -51,7 +51,13 @@ from zarr.core.config import config from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.core.sync import SyncMixin, sync -from zarr.errors import ContainsArrayError, ContainsGroupError, MetadataValidationError +from zarr.errors import ( + ContainsArrayError, + ContainsGroupError, + MetadataValidationError, + ZarrDeprecationWarning, + ZarrUserWarning, +) from zarr.storage import StoreLike, StorePath from zarr.storage._common import ensure_no_existing_node, make_store_path from zarr.storage._utils import _join_paths, _normalize_path_keys, normalize_path @@ -558,7 +564,7 @@ async def open( if zarr_json_bytes is not None and zgroup_bytes is not None: # warn and favor v3 msg = f"Both zarr.json (Zarr format 3) and .zgroup (Zarr format 2) metadata objects exist at {store_path}. Zarr format 3 will be used." - warnings.warn(msg, stacklevel=1) + warnings.warn(msg, category=ZarrUserWarning, stacklevel=1) if zarr_json_bytes is None and zgroup_bytes is None: raise FileNotFoundError( f"could not find zarr.json or .zgroup objects in {store_path}" @@ -1152,7 +1158,7 @@ async def create_array( write_data=write_data, ) - @deprecated("Use AsyncGroup.create_array instead.") + @deprecated("Use AsyncGroup.create_array instead.", category=ZarrDeprecationWarning) async def create_dataset( self, name: str, *, shape: ShapeLike, **kwargs: Any ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: @@ -1186,7 +1192,7 @@ async def create_dataset( await array.setitem(slice(None), data) return array - @deprecated("Use AsyncGroup.require_array instead.") + @deprecated("Use AsyncGroup.require_array instead.", category=ZarrDeprecationWarning) async def require_dataset( self, name: str, @@ -2587,7 +2593,7 @@ def create_array( ) ) - @deprecated("Use Group.create_array instead.") + @deprecated("Use Group.create_array instead.", category=ZarrDeprecationWarning) def create_dataset(self, name: str, **kwargs: Any) -> Array: """Create an array. @@ -2611,7 +2617,7 @@ def create_dataset(self, name: str, **kwargs: Any) -> Array: """ return Array(self._sync(self._async_group.create_dataset(name, **kwargs))) - @deprecated("Use Group.require_array instead.") + @deprecated("Use Group.require_array instead.", category=ZarrDeprecationWarning) def require_dataset(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> Array: """Obtain an array, creating if it doesn't exist. @@ -2833,7 +2839,7 @@ def move(self, source: str, dest: str) -> None: """ return self._sync(self._async_group.move(source, dest)) - @deprecated("Use Group.create_array instead.") + @deprecated("Use Group.create_array instead.", category=ZarrDeprecationWarning) def array( self, name: str, @@ -3385,7 +3391,7 @@ async def _iter_members( # in which case `key` cannot be the name of a sub-array or sub-group. warnings.warn( f"Object at {e.args[0]} is not recognized as a component of a Zarr hierarchy.", - UserWarning, + ZarrUserWarning, stacklevel=1, ) continue diff --git a/src/zarr/core/metadata/v2.py b/src/zarr/core/metadata/v2.py index 17af3538a9..9ad6b3bc42 100644 --- a/src/zarr/core/metadata/v2.py +++ b/src/zarr/core/metadata/v2.py @@ -11,6 +11,7 @@ from zarr.core.chunk_grids import RegularChunkGrid from zarr.core.dtype import get_data_type_from_json from zarr.core.dtype.common import OBJECT_CODEC_IDS, DTypeSpec_V2 +from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from typing import Literal, Self @@ -188,7 +189,7 @@ def from_dict(cls, data: dict[str, Any]) -> ArrayV2Metadata: "This is contrary to the Zarr V2 specification, and will cause an error in the future. " "Use None (or Null in a JSON document) instead of an empty list of filters." ) - warnings.warn(msg, UserWarning, stacklevel=1) + warnings.warn(msg, ZarrUserWarning, stacklevel=1) _data["filters"] = None _data = {k: v for k, v in _data.items() if k in expected} diff --git a/src/zarr/creation.py b/src/zarr/creation.py index 8197c4950c..622406ed75 100644 --- a/src/zarr/creation.py +++ b/src/zarr/creation.py @@ -23,6 +23,7 @@ zeros, zeros_like, ) +from zarr.errors import ZarrDeprecationWarning __all__ = [ "array", @@ -42,6 +43,6 @@ warnings.warn( "zarr.creation is deprecated. " "Import these functions from the top level zarr. namespace instead.", - DeprecationWarning, + ZarrDeprecationWarning, stacklevel=2, ) diff --git a/src/zarr/errors.py b/src/zarr/errors.py index 4f972a6703..0055ea3c6c 100644 --- a/src/zarr/errors.py +++ b/src/zarr/errors.py @@ -8,6 +8,10 @@ "GroupNotFoundError", "MetadataValidationError", "NodeTypeValidationError", + "UnstableSpecificationWarning", + "ZarrDeprecationWarning", + "ZarrFutureWarning", + "ZarrRuntimeWarning", ] @@ -61,8 +65,38 @@ class MetadataValidationError(BaseZarrError): class NodeTypeValidationError(MetadataValidationError): """ - Specialized exception when the node_type of the metadata document is incorrect.. + Specialized exception when the node_type of the metadata document is incorrect. This can be raised when the value is invalid or unexpected given the context, for example an 'array' node when we expected a 'group'. """ + + +class ZarrFutureWarning(FutureWarning): + """ + A warning intended for end users raised to indicate deprecated features. + """ + + +class UnstableSpecificationWarning(ZarrFutureWarning): + """ + A warning raised to indicate that a feature is outside the Zarr specification. + """ + + +class ZarrDeprecationWarning(DeprecationWarning): + """ + A warning raised to indicate that a feature will be removed in a future release. + """ + + +class ZarrUserWarning(UserWarning): + """ + A warning raised to report problems with user code. + """ + + +class ZarrRuntimeWarning(RuntimeWarning): + """ + A warning for dubious runtime behavior. + """ diff --git a/src/zarr/registry.py b/src/zarr/registry.py index 189d42abed..fc3ffd7f7c 100644 --- a/src/zarr/registry.py +++ b/src/zarr/registry.py @@ -7,6 +7,7 @@ from zarr.core.config import BadConfigError, config from zarr.core.dtype import data_type_registry +from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from importlib.metadata import EntryPoint @@ -160,6 +161,7 @@ def get_codec_class(key: str, reload_config: bool = False) -> type[Codec]: warnings.warn( f"Codec '{key}' not configured in config. Selecting any implementation.", stacklevel=2, + category=ZarrUserWarning, ) return list(codec_classes.values())[-1] selected_codec_cls = codec_classes[config_entry] diff --git a/src/zarr/storage/__init__.py b/src/zarr/storage/__init__.py index 6721139375..00df50214f 100644 --- a/src/zarr/storage/__init__.py +++ b/src/zarr/storage/__init__.py @@ -3,6 +3,7 @@ from types import ModuleType from typing import Any +from zarr.errors import ZarrDeprecationWarning from zarr.storage._common import StoreLike, StorePath from zarr.storage._fsspec import FsspecStore from zarr.storage._local import LocalStore @@ -33,7 +34,7 @@ def __setattr__(self, attr: str, value: Any) -> None: "setting zarr.storage.default_compressor is deprecated, use " "zarr.config to configure array.v2_default_compressor " "e.g. config.set({'codecs.zstd':'numcodecs.Zstd', 'array.v2_default_compressor.numeric': 'zstd'})", - DeprecationWarning, + ZarrDeprecationWarning, stacklevel=1, ) else: diff --git a/src/zarr/storage/_fsspec.py b/src/zarr/storage/_fsspec.py index e169eededc..bbb934cc7d 100644 --- a/src/zarr/storage/_fsspec.py +++ b/src/zarr/storage/_fsspec.py @@ -15,6 +15,7 @@ SuffixByteRequest, ) from zarr.core.buffer import Buffer +from zarr.errors import ZarrUserWarning from zarr.storage._common import _dereference_path if TYPE_CHECKING: @@ -101,7 +102,7 @@ class FsspecStore(Store): Warns ----- - UserWarning + ZarrUserWarning If the file system (fs) was not created with `asynchronous=True`. See Also @@ -137,6 +138,7 @@ def __init__( if not self.fs.asynchronous: warnings.warn( f"fs ({fs}) was not created with `asynchronous=True`, this may lead to surprising behavior", + category=ZarrUserWarning, stacklevel=2, ) if "://" in path and not path.startswith("http"): diff --git a/src/zarr/testing/__init__.py b/src/zarr/testing/__init__.py index 0b4d8cf417..21a3572846 100644 --- a/src/zarr/testing/__init__.py +++ b/src/zarr/testing/__init__.py @@ -1,10 +1,14 @@ import importlib.util import warnings +from zarr.errors import ZarrUserWarning + if importlib.util.find_spec("pytest") is not None: from zarr.testing.store import StoreTests else: - warnings.warn("pytest not installed, skipping test suite", stacklevel=2) + warnings.warn( + "pytest not installed, skipping test suite", category=ZarrUserWarning, stacklevel=2 + ) from zarr.testing.utils import assert_bytes_equal diff --git a/tests/test_api.py b/tests/test_api.py index 01fb40f050..12acf80589 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,6 +7,7 @@ import zarr.codecs import zarr.storage from zarr.core.array import init_array +from zarr.storage import LocalStore, ZipStore from zarr.storage._common import StorePath if TYPE_CHECKING: @@ -41,8 +42,8 @@ save_group, ) from zarr.core.buffer import NDArrayLike -from zarr.errors import MetadataValidationError -from zarr.storage import LocalStore, MemoryStore, ZipStore +from zarr.errors import MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning +from zarr.storage import MemoryStore from zarr.storage._utils import normalize_path from zarr.testing.utils import gpu_test @@ -169,7 +170,7 @@ def test_v2_and_v3_exist_at_same_path(store: Store) -> None: zarr.create_array(store, shape=(10,), dtype="uint8", zarr_format=3) zarr.create_array(store, shape=(10,), dtype="uint8", zarr_format=2) msg = f"Both zarr.json (Zarr format 3) and .zarray (Zarr format 2) metadata objects exist at {store}. Zarr v3 will be used." - with pytest.warns(UserWarning, match=re.escape(msg)): + with pytest.warns(ZarrUserWarning, match=re.escape(msg)): zarr.open(store=store) @@ -470,7 +471,7 @@ def test_tree() -> None: g3.create_group("baz") g5 = g3.create_group("qux") g5.create_array("baz", shape=(100,), chunks=(10,), dtype="float64") - with pytest.warns(DeprecationWarning, match=r"Group\.tree instead\."): # noqa: PT031 + with pytest.warns(ZarrDeprecationWarning, match=r"Group\.tree instead\."): # noqa: PT031 assert repr(zarr.tree(g1)) == repr(g1.tree()) assert str(zarr.tree(g1)) == str(g1.tree()) @@ -1350,7 +1351,7 @@ def test_no_overwrite_open(tmp_path: Path, open_func: Callable, mode: str) -> No existing_fpath = add_empty_file(tmp_path) assert existing_fpath.exists() - with contextlib.suppress(FileExistsError, FileNotFoundError, UserWarning): + with contextlib.suppress(FileExistsError, FileNotFoundError, ZarrUserWarning): open_func(store=store, mode=mode) if mode == "w": assert not existing_fpath.exists() diff --git a/tests/test_array.py b/tests/test_array.py index f672006f9a..46b78de7bf 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -63,7 +63,11 @@ from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.core.sync import sync -from zarr.errors import ContainsArrayError, ContainsGroupError +from zarr.errors import ( + ContainsArrayError, + ContainsGroupError, + ZarrUserWarning, +) from zarr.storage import LocalStore, MemoryStore, StorePath from .test_dtype.conftest import zdtype_examples @@ -922,13 +926,16 @@ def test_auto_partition_auto_shards( expected_shards += (2 * cs,) else: expected_shards += (cs,) - - auto_shards, _ = _auto_partition( - array_shape=array_shape, - chunk_shape=chunk_shape, - shard_shape="auto", - item_size=dtype.itemsize, - ) + with pytest.warns( + ZarrUserWarning, + match="Automatic shard shape inference is experimental and may change without notice.", + ): + auto_shards, _ = _auto_partition( + array_shape=array_shape, + chunk_shape=chunk_shape, + shard_shape="auto", + item_size=dtype.itemsize, + ) assert auto_shards == expected_shards @@ -1631,7 +1638,7 @@ async def test_from_array_arraylike( def test_from_array_F_order() -> None: arr = zarr.create_array(store={}, data=np.array([1]), order="F", zarr_format=2) with pytest.warns( - UserWarning, + ZarrUserWarning, match="The existing order='F' of the source Zarr format 2 array will be ignored.", ): zarr.from_array(store={}, data=arr, zarr_format=3) @@ -1679,21 +1686,24 @@ def test_roundtrip_numcodecs() -> None: # Create the array with the correct codecs root = zarr.group(store) - root.create_array( - "test", - shape=(720, 1440), - chunks=(720, 1440), - dtype="float64", - compressors=compressors, - filters=filters, - fill_value=-9.99, - dimension_names=["lat", "lon"], - ) + warn_msg = "Numcodecs codecs are not in the Zarr version 3 specification and may not be supported by other zarr implementations." + with pytest.warns(UserWarning, match=warn_msg): + root.create_array( + "test", + shape=(720, 1440), + chunks=(720, 1440), + dtype="float64", + compressors=compressors, + filters=filters, + fill_value=-9.99, + dimension_names=["lat", "lon"], + ) BYTES_CODEC = {"name": "bytes", "configuration": {"endian": "little"}} # Read in the array again and check compressor config root = zarr.open_group(store) - metadata = root["test"].metadata.to_dict() + with pytest.warns(UserWarning, match=warn_msg): + metadata = root["test"].metadata.to_dict() expected = (*filters, BYTES_CODEC, *compressors) assert metadata["codecs"] == expected diff --git a/tests/test_buffer.py b/tests/test_buffer.py index bbfa25d138..b50e5abb67 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -13,6 +13,7 @@ from zarr.codecs.gzip import GzipCodec from zarr.codecs.transpose import TransposeCodec from zarr.codecs.zstd import ZstdCodec +from zarr.errors import ZarrUserWarning from zarr.storage import MemoryStore, StorePath from zarr.testing.buffer import ( NDBufferUsingTestNDArrayLike, @@ -138,13 +139,17 @@ async def test_codecs_use_of_gpu_prototype() -> None: filters=[TransposeCodec(order=(1, 0))], ) expect[:] = cp.arange(100).reshape(10, 10) - - await a.setitem( - selection=(slice(0, 10), slice(0, 10)), - value=expect[:], - prototype=gpu.buffer_prototype, - ) - got = await a.getitem(selection=(slice(0, 10), slice(0, 10)), prototype=gpu.buffer_prototype) + msg = "Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__ for zero-copy transfers, falling back to slow copy based path" + with pytest.warns(ZarrUserWarning, match=msg): + await a.setitem( + selection=(slice(0, 10), slice(0, 10)), + value=expect[:], + prototype=gpu.buffer_prototype, + ) + with pytest.warns(ZarrUserWarning, match=msg): + got = await a.getitem( + selection=(slice(0, 10), slice(0, 10)), prototype=gpu.buffer_prototype + ) assert isinstance(got, cp.ndarray) assert cp.array_equal(expect, got) @@ -164,15 +169,17 @@ async def test_sharding_use_of_gpu_prototype() -> None: fill_value=0, ) expect[:] = cp.arange(100).reshape(10, 10) - - await a.setitem( - selection=(slice(0, 10), slice(0, 10)), - value=expect[:], - prototype=gpu.buffer_prototype, - ) - got = await a.getitem( - selection=(slice(0, 10), slice(0, 10)), prototype=gpu.buffer_prototype - ) + msg = "Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__ for zero-copy transfers, falling back to slow copy based path" + with pytest.warns(ZarrUserWarning, match=msg): + await a.setitem( + selection=(slice(0, 10), slice(0, 10)), + value=expect[:], + prototype=gpu.buffer_prototype, + ) + with pytest.warns(ZarrUserWarning, match=msg): + got = await a.getitem( + selection=(slice(0, 10), slice(0, 10)), prototype=gpu.buffer_prototype + ) assert isinstance(got, cp.ndarray) assert cp.array_equal(expect, got) diff --git a/tests/test_codecs/test_codecs.py b/tests/test_codecs/test_codecs.py index d52a9e1c44..a2dad41a1b 100644 --- a/tests/test_codecs/test_codecs.py +++ b/tests/test_codecs/test_codecs.py @@ -20,9 +20,12 @@ from zarr.core.buffer import default_buffer_prototype from zarr.core.indexing import BasicSelection, morton_order_iter from zarr.core.metadata.v3 import ArrayV3Metadata +from zarr.dtype import UInt8 +from zarr.errors import ZarrUserWarning from zarr.storage import StorePath if TYPE_CHECKING: + from zarr.abc.codec import Codec from zarr.abc.store import Store from zarr.core.buffer.core import NDArrayLikeOrScalar from zarr.core.common import ChunkCoords, MemoryOrder @@ -290,97 +293,37 @@ async def test_dimension_names(store: Store) -> None: assert "dimension_names" not in json.loads(zarr_json_buffer.to_bytes()) -@pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) -def test_invalid_metadata(store: Store) -> None: - spath2 = StorePath(store, "invalid_codec_order") - with pytest.raises(TypeError): - Array.create( - spath2, - shape=(16, 16), - chunk_shape=(16, 16), - dtype=np.dtype("uint8"), - fill_value=0, - codecs=[ - BytesCodec(), - TransposeCodec(order=order_from_dim("F", 2)), - ], - ) - spath3 = StorePath(store, "invalid_order") - with pytest.raises(TypeError): - Array.create( - spath3, - shape=(16, 16), - chunk_shape=(16, 16), - dtype=np.dtype("uint8"), - fill_value=0, - codecs=[ - TransposeCodec(order="F"), # type: ignore[arg-type] - BytesCodec(), - ], - ) - spath4 = StorePath(store, "invalid_missing_bytes_codec") - with pytest.raises(ValueError): - Array.create( - spath4, - shape=(16, 16), - chunk_shape=(16, 16), - dtype=np.dtype("uint8"), - fill_value=0, - codecs=[ - TransposeCodec(order=order_from_dim("F", 2)), - ], - ) - spath5 = StorePath(store, "invalid_inner_chunk_shape") - with pytest.raises(ValueError): - Array.create( - spath5, - shape=(16, 16), - chunk_shape=(16, 16), - dtype=np.dtype("uint8"), - fill_value=0, - codecs=[ - ShardingCodec(chunk_shape=(8,)), - ], - ) - spath6 = StorePath(store, "invalid_inner_chunk_shape") - with pytest.raises(ValueError): - Array.create( - spath6, - shape=(16, 16), - chunk_shape=(16, 16), - dtype=np.dtype("uint8"), - fill_value=0, - codecs=[ - ShardingCodec(chunk_shape=(8, 7)), - ], - ) - spath7 = StorePath(store, "warning_inefficient_codecs") - with pytest.warns( - UserWarning, - match="Combining a `sharding_indexed` codec disables partial reads and writes, which may lead to inefficient performance", - ): - Array.create( - spath7, - shape=(16, 16), - chunk_shape=(16, 16), - dtype=np.dtype("uint8"), +@pytest.mark.parametrize( + "codecs", + [ + (BytesCodec(), TransposeCodec(order=order_from_dim("F", 2))), + (TransposeCodec(order=order_from_dim("F", 2)),), + ], +) +def test_invalid_metadata(codecs: tuple[Codec, ...]) -> None: + shape = (16,) + chunks = (16,) + data_type = UInt8() + with pytest.raises(ValueError, match="The `order` tuple must have as many entries"): + ArrayV3Metadata( + shape=shape, + chunk_grid={"name": "regular", "configuration": {"chunk_shape": chunks}}, + chunk_key_encoding={"name": "default", "configuration": {"separator": "/"}}, # type: ignore[arg-type] fill_value=0, - codecs=[ - ShardingCodec(chunk_shape=(8, 8)), - GzipCodec(), - ], + data_type=data_type, + codecs=codecs, + attributes={}, + dimension_names=None, ) -@pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) -def test_invalid_metadata_create_array(store: Store) -> None: - spath = StorePath(store, "warning_inefficient_codecs") +def test_invalid_metadata_create_array() -> None: with pytest.warns( - UserWarning, + ZarrUserWarning, match="codec disables partial reads and writes, which may lead to inefficient performance", ): zarr.create_array( - spath, + {}, shape=(16, 16), chunks=(16, 16), dtype=np.dtype("uint8"), diff --git a/tests/test_codecs/test_sharding.py b/tests/test_codecs/test_sharding.py index 403fd80e81..eb80545ff3 100644 --- a/tests/test_codecs/test_sharding.py +++ b/tests/test_codecs/test_sharding.py @@ -17,7 +17,8 @@ TransposeCodec, ) from zarr.core.buffer import NDArrayLike, default_buffer_prototype -from zarr.storage import StorePath +from zarr.errors import ZarrUserWarning +from zarr.storage import StorePath, ZipStore from ..conftest import ArrayRequest from .test_codecs import _AsyncArrayProxy, order_from_dim @@ -228,7 +229,11 @@ def test_sharding_partial_overwrite( assert np.array_equal(data, read_data) data += 10 - a[:10, :10, :10] = data + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + a[:10, :10, :10] = data + else: + a[:10, :10, :10] = data read_data = a[0:10, 0:10, 0:10] assert np.array_equal(data, read_data) @@ -257,22 +262,22 @@ def test_nested_sharding( ) -> None: data = array_fixture spath = StorePath(store) - a = Array.create( - spath, - shape=data.shape, - chunk_shape=(64, 64, 64), - dtype=data.dtype, - fill_value=0, - codecs=[ - ShardingCodec( + msg = "Combining a `sharding_indexed` codec disables partial reads and writes, which may lead to inefficient performance." + with pytest.warns(ZarrUserWarning, match=msg): + a = zarr.create_array( + spath, + shape=data.shape, + chunks=(64, 64, 64), + dtype=data.dtype, + fill_value=0, + serializer=ShardingCodec( chunk_shape=(32, 32, 32), codecs=[ ShardingCodec(chunk_shape=(16, 16, 16), index_location=inner_index_location) ], index_location=outer_index_location, - ) - ], - ) + ), + ) a[:, :, :] = data diff --git a/tests/test_config.py b/tests/test_config.py index da5b2cc488..0c029dda3a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -24,6 +24,7 @@ from zarr.core.codec_pipeline import BatchedCodecPipeline from zarr.core.config import BadConfigError, config from zarr.core.indexing import SelectorTuple +from zarr.errors import ZarrUserWarning from zarr.registry import ( fully_qualified_name, get_buffer_class, @@ -287,7 +288,9 @@ class NewCodec2(BytesCodec): # warning because multiple implementations are available but none is selected in the config register_codec("new_codec", NewCodec2) - with pytest.warns(UserWarning, match="not configured in config. Selecting any implementation"): + with pytest.warns( + ZarrUserWarning, match="not configured in config. Selecting any implementation" + ): get_codec_class("new_codec") # no warning if multiple implementations are available and one is selected in the config diff --git a/tests/test_dtype/test_npy/test_bytes.py b/tests/test_dtype/test_npy/test_bytes.py index 78980f7809..39b5416635 100644 --- a/tests/test_dtype/test_npy/test_bytes.py +++ b/tests/test_dtype/test_npy/test_bytes.py @@ -2,8 +2,8 @@ import pytest from tests.test_dtype.test_wrapper import BaseTestZDType -from zarr.core.dtype.common import UnstableSpecificationWarning from zarr.core.dtype.npy.bytes import NullTerminatedBytes, RawBytes, VariableLengthBytes +from zarr.errors import UnstableSpecificationWarning class TestNullTerminatedBytes(BaseTestZDType): @@ -150,7 +150,7 @@ def test_unstable_dtype_warning( Test that we get a warning when serializing a dtype without a zarr v3 spec to json when zarr_format is 3 """ - with pytest.raises(UnstableSpecificationWarning): + with pytest.warns(UnstableSpecificationWarning): zdtype.to_json(zarr_format=3) diff --git a/tests/test_dtype/test_npy/test_string.py b/tests/test_dtype/test_npy/test_string.py index 2cde6a1ac1..19d202d164 100644 --- a/tests/test_dtype/test_npy/test_string.py +++ b/tests/test_dtype/test_npy/test_string.py @@ -5,8 +5,8 @@ from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype import FixedLengthUTF32 -from zarr.core.dtype.common import UnstableSpecificationWarning from zarr.core.dtype.npy.string import _NUMPY_SUPPORTS_VLEN_STRING, VariableLengthUTF8 +from zarr.errors import UnstableSpecificationWarning if _NUMPY_SUPPORTS_VLEN_STRING: @@ -136,7 +136,7 @@ def test_unstable_dtype_warning(zdtype: FixedLengthUTF32 | VariableLengthUTF8) - Test that we get a warning when serializing a dtype without a zarr v3 spec to json when zarr_format is 3 """ - with pytest.raises(UnstableSpecificationWarning): + with pytest.warns(UnstableSpecificationWarning): zdtype.to_json(zarr_format=3) diff --git a/tests/test_group.py b/tests/test_group.py index 7705fa205a..e5cfe82daa 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -2,6 +2,7 @@ import contextlib import inspect +import json import operator import pickle import re @@ -39,7 +40,13 @@ ) from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.core.sync import _collect_aiterator, sync -from zarr.errors import ContainsArrayError, ContainsGroupError, MetadataValidationError +from zarr.errors import ( + ContainsArrayError, + ContainsGroupError, + MetadataValidationError, + ZarrDeprecationWarning, + ZarrUserWarning, +) from zarr.storage import LocalStore, MemoryStore, StorePath, ZipStore from zarr.storage._common import make_store_path from zarr.storage._utils import _join_paths, normalize_path @@ -52,6 +59,7 @@ from _pytest.compat import LEGACY_PATH + from zarr.core.buffer.core import Buffer from zarr.core.common import JSON, ZarrFormat @@ -203,11 +211,17 @@ def test_group_members(store: Store, zarr_format: ZarrFormat, consolidated_metad # this warning shows up when extra objects show up in the hierarchy warn_context = pytest.warns( - UserWarning, match=r"Object at .* is not recognized as a component of a Zarr hierarchy." + ZarrUserWarning, + match=r"(?:Object at .* is not recognized as a component of a Zarr hierarchy.)|(?:Consolidated metadata is currently not part in the Zarr format 3 specification.)", ) if consolidated_metadata: - with warn_context: - zarr.consolidate_metadata(store=store, zarr_format=zarr_format) + if isinstance(store, ZipStore): + with warn_context: + with pytest.warns(UserWarning, match="Duplicate name: "): + zarr.consolidate_metadata(store=store, zarr_format=zarr_format) + else: + with warn_context: + zarr.consolidate_metadata(store=store, zarr_format=zarr_format) # now that we've consolidated the store, we shouldn't get the warnings from the unrecognized objects anymore # we use a nullcontext to handle these cases warn_context = contextlib.nullcontext() @@ -267,7 +281,11 @@ def test_group(store: Store, zarr_format: ZarrFormat) -> None: assert dict(bar2.attrs) == {"baz": "qux"} # update a group's attributes - bar2.attrs.update({"name": "bar"}) + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + bar2.attrs.update({"name": "bar"}) + else: + bar2.attrs.update({"name": "bar"}) # bar.attrs was modified in-place assert dict(bar2.attrs) == {"baz": "qux", "name": "bar"} @@ -340,7 +358,30 @@ def test_group_getitem(store: Store, zarr_format: ZarrFormat, consolidated: bool subsubarray = subgroup.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="uint8") if consolidated: - group = zarr.api.synchronous.consolidate_metadata(store=store, zarr_format=zarr_format) + if zarr_format == 3: + with pytest.warns( # noqa: PT031 + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + group = zarr.api.synchronous.consolidate_metadata( + store=store, zarr_format=zarr_format + ) + else: + group = zarr.api.synchronous.consolidate_metadata( + store=store, zarr_format=zarr_format + ) + else: + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + group = zarr.api.synchronous.consolidate_metadata( + store=store, zarr_format=zarr_format + ) + else: + group = zarr.api.synchronous.consolidate_metadata( + store=store, zarr_format=zarr_format + ) # we're going to assume that `group.metadata` is correct, and reuse that to focus # on indexing in this test. Other tests verify the correctness of group.metadata object.__setattr__( @@ -398,8 +439,11 @@ def test_group_get_with_default(store: Store, zarr_format: ZarrFormat) -> None: # now with a group subgroup = group.require_group("subgroup") - subgroup.attrs["foo"] = "bar" - + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + subgroup.attrs["foo"] = "bar" + else: + subgroup.attrs["foo"] = "bar" result = group.get("subgroup", 8) assert result.attrs["foo"] == "bar" @@ -417,7 +461,22 @@ def test_group_delitem(store: Store, zarr_format: ZarrFormat, consolidated: bool subarray = group.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="uint8") if consolidated: - group = zarr.api.synchronous.consolidate_metadata(store=store, zarr_format=zarr_format) + if zarr_format == 3: + with pytest.warns( # noqa: PT031 + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + group = zarr.api.synchronous.consolidate_metadata( + store=store, zarr_format=zarr_format + ) + else: + group = zarr.api.synchronous.consolidate_metadata( + store=store, zarr_format=zarr_format + ) + else: + group = zarr.api.synchronous.consolidate_metadata(store=store, zarr_format=zarr_format) object.__setattr__( subgroup.metadata, "consolidated_metadata", ConsolidatedMetadata(metadata={}) ) @@ -512,7 +571,22 @@ def test_group_child_iterators(store: Store, zarr_format: ZarrFormat, consolidat expected_arrays = list(zip(expected_array_keys, expected_array_values, strict=False)) if consolidate: - group = zarr.consolidate_metadata(store) + if zarr_format == 3: + with pytest.warns( # noqa: PT031 + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + group = zarr.consolidate_metadata(store) + else: + group = zarr.consolidate_metadata(store) + else: + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + group = zarr.consolidate_metadata(store) + else: + group = zarr.consolidate_metadata(store) if zarr_format == 2: metadata = { "subarray": { @@ -608,7 +682,11 @@ def test_group_update_attributes(store: Store, zarr_format: ZarrFormat) -> None: group = Group.from_store(store, zarr_format=zarr_format, attributes=attrs) assert group.attrs == attrs new_attrs = {"bar": 100} - new_group = group.update_attributes(new_attrs) + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + new_group = group.update_attributes(new_attrs) + else: + new_group = group.update_attributes(new_attrs) updated_attrs = attrs.copy() updated_attrs.update(new_attrs) @@ -623,7 +701,11 @@ async def test_group_update_attributes_async(store: Store, zarr_format: ZarrForm group = Group.from_store(store, zarr_format=zarr_format, attributes=attrs) assert group.attrs == attrs new_attrs = {"bar": 100} - new_group = await group.update_attributes_async(new_attrs) + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name: "): + new_group = await group.update_attributes_async(new_attrs) + else: + new_group = await group.update_attributes_async(new_attrs) assert new_group.attrs == new_attrs @@ -648,8 +730,12 @@ def test_group_create_array( array = group.create_array(name=name, shape=shape, dtype=dtype) array[:] = data elif method == "array": - with pytest.warns(DeprecationWarning, match=r"Group\.create_array instead\."): - array = group.array(name=name, data=data, shape=shape, dtype=dtype) + with pytest.warns(ZarrDeprecationWarning, match=r"Group\.create_array instead\."): + with pytest.warns( + ZarrUserWarning, + match="The `compressor` argument is deprecated. Use `compressors` instead.", + ): + array = group.array(name=name, data=data, shape=shape, dtype=dtype) else: raise AssertionError @@ -660,8 +746,12 @@ def test_group_create_array( a[:] = data elif method == "array": with pytest.raises(ContainsArrayError): # noqa: PT012 - with pytest.warns(DeprecationWarning, match=r"Group\.create_array instead\."): - a = group.array(name=name, shape=shape, dtype=dtype) + with pytest.warns(ZarrDeprecationWarning, match=r"Group\.create_array instead\."): + with pytest.warns( + ZarrUserWarning, + match="The `compressor` argument is deprecated. Use `compressors` instead.", + ): + a = group.array(name=name, shape=shape, dtype=dtype) a[:] = data assert array.path == normalize_path(name) @@ -1024,7 +1114,11 @@ async def test_asyncgroup_update_attributes(store: Store, zarr_format: ZarrForma store=store, zarr_format=zarr_format, attributes=attributes_old ) - agroup_new_attributes = await agroup.update_attributes(attributes_new) + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name"): + agroup_new_attributes = await agroup.update_attributes(attributes_new) + else: + agroup_new_attributes = await agroup.update_attributes(attributes_new) attributes_updated = attributes_old.copy() attributes_updated.update(attributes_new) assert agroup_new_attributes.attrs == attributes_updated @@ -1099,8 +1193,16 @@ async def test_group_members_async(store: Store, consolidated_metadata: bool) -> assert all_children == expected if consolidated_metadata: - await zarr.api.asynchronous.consolidate_metadata(store=store) - group = await zarr.api.asynchronous.open_group(store=store) + with pytest.warns( # noqa: PT031 + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name"): + await zarr.api.asynchronous.consolidate_metadata(store=store) + else: + await zarr.api.asynchronous.consolidate_metadata(store=store) + group = await zarr.api.asynchronous.open_group(store=store) nmembers = await group.nmembers(max_depth=None) assert nmembers == 6 @@ -1194,27 +1296,27 @@ def test_create_dataset_with_data(store: Store, zarr_format: ZarrFormat) -> None """ root = Group.from_store(store=store, zarr_format=zarr_format) arr = np.random.random((5, 5)) - with pytest.warns(DeprecationWarning, match=r"Group\.create_array instead\."): + with pytest.warns(ZarrDeprecationWarning, match=r"Group\.create_array instead\."): data = root.create_dataset("random", data=arr, shape=arr.shape) np.testing.assert_array_equal(np.asarray(data), arr) async def test_create_dataset(store: Store, zarr_format: ZarrFormat) -> None: root = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) - with pytest.warns(DeprecationWarning, match=r"Group\.create_array instead\."): + with pytest.warns(ZarrDeprecationWarning, match=r"Group\.create_array instead\."): foo = await root.create_dataset("foo", shape=(10,), dtype="uint8") assert foo.shape == (10,) with ( pytest.raises(ContainsArrayError), - pytest.warns(DeprecationWarning, match=r"Group\.create_array instead\."), + pytest.warns(ZarrDeprecationWarning, match=r"Group\.create_array instead\."), ): await root.create_dataset("foo", shape=(100,), dtype="int8") _ = await root.create_group("bar") with ( pytest.raises(ContainsGroupError), - pytest.warns(DeprecationWarning, match=r"Group\.create_array instead\."), + pytest.warns(ZarrDeprecationWarning, match=r"Group\.create_array instead\."), ): await root.create_dataset("bar", shape=(100,), dtype="int8") @@ -1253,8 +1355,25 @@ async def test_members_name(store: Store, consolidate: bool, zarr_format: ZarrFo b.create_array("array", shape=(1,), dtype="uint8") if consolidate: - group = zarr.api.synchronous.consolidate_metadata(store) - + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name"): # noqa: PT031 + if zarr_format == 3: + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + group = zarr.api.synchronous.consolidate_metadata(store) + else: + group = zarr.api.synchronous.consolidate_metadata(store) + else: + if zarr_format == 3: + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + group = zarr.api.synchronous.consolidate_metadata(store) + else: + group = zarr.api.synchronous.consolidate_metadata(store) result = group["a"]["b"] assert result.name == "/a/b" @@ -1283,6 +1402,21 @@ def test_open_mutable_mapping_sync(): assert isinstance(group.store_path.store, MemoryStore) +async def test_open_ambiguous_node(): + zarr_json_bytes = default_buffer_prototype().buffer.from_bytes( + json.dumps({"zarr_format": 3, "node_type": "group"}).encode("utf-8") + ) + zgroup_bytes = default_buffer_prototype().buffer.from_bytes( + json.dumps({"zarr_format": 2}).encode("utf-8") + ) + store: dict[str, Buffer] = {"zarr.json": zarr_json_bytes, ".zgroup": zgroup_bytes} + with pytest.warns( + ZarrUserWarning, + match=r"Both zarr\.json \(Zarr format 3\) and \.zgroup \(Zarr format 2\) metadata objects exist at", + ): + await AsyncGroup.open(store, zarr_format=None) + + class TestConsolidated: async def test_group_getitem_consolidated(self, store: Store) -> None: root = await AsyncGroup.from_store(store=store) @@ -1303,7 +1437,15 @@ async def test_group_getitem_consolidated(self, store: Store) -> None: x1 = await x0.create_group("x1") await x1.create_group("x2") - await zarr.api.asynchronous.consolidate_metadata(store) + with pytest.warns( # noqa: PT031 + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name"): + await zarr.api.asynchronous.consolidate_metadata(store) + else: + await zarr.api.asynchronous.consolidate_metadata(store) # On disk, we've consolidated all the metadata in the root zarr.json group = await zarr.api.asynchronous.open(store=store) @@ -1360,7 +1502,15 @@ async def test_group_delitem_consolidated(self, store: Store) -> None: x2 = await x1.create_group("x2") await x2.create_array("data", shape=(1,), dtype="uint8") - await zarr.api.asynchronous.consolidate_metadata(store) + with pytest.warns( # noqa: PT031 + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + if isinstance(store, ZipStore): + with pytest.warns(UserWarning, match="Duplicate name"): + await zarr.api.asynchronous.consolidate_metadata(store) + else: + await zarr.api.asynchronous.consolidate_metadata(store) group = await zarr.api.asynchronous.open_consolidated(store=store) assert len(group.metadata.consolidated_metadata.metadata) == 2 @@ -1384,7 +1534,11 @@ def test_open_consolidated_raises(self, store: Store) -> None: # Now create consolidated metadata... root.create_group("g0") - zarr.consolidate_metadata(store) + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + zarr.consolidate_metadata(store) # and explicitly ignore it. group = zarr.open_group(store=store, use_consolidated=False) @@ -1404,7 +1558,11 @@ async def test_open_consolidated_raises_async(self, store: Store) -> None: # Now create consolidated metadata... await root.create_group("g0") - await zarr.api.asynchronous.consolidate_metadata(store) + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await zarr.api.asynchronous.consolidate_metadata(store) # and explicitly ignore it. group = await zarr.api.asynchronous.open_group(store=store, use_consolidated=False) diff --git a/tests/test_metadata/test_consolidated.py b/tests/test_metadata/test_consolidated.py index 19eba4fb86..e23444cf93 100644 --- a/tests/test_metadata/test_consolidated.py +++ b/tests/test_metadata/test_consolidated.py @@ -22,6 +22,7 @@ from zarr.core.group import ConsolidatedMetadata, GroupMetadata from zarr.core.metadata import ArrayV3Metadata from zarr.core.metadata.v2 import ArrayV2Metadata +from zarr.errors import ZarrUserWarning from zarr.storage import StorePath if TYPE_CHECKING: @@ -67,7 +68,11 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: # arrays under arrays # single array # etc. - await consolidate_metadata(memory_store_with_hierarchy) + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await consolidate_metadata(memory_store_with_hierarchy) group2 = await AsyncGroup.open(memory_store_with_hierarchy) array_metadata = { @@ -215,7 +220,11 @@ def test_consolidated_sync(self, memory_store): g.create_array(name="lon", shape=(2,), dtype=dtype) g.create_array(name="time", shape=(3,), dtype=dtype) - zarr.api.synchronous.consolidate_metadata(memory_store) + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + zarr.api.synchronous.consolidate_metadata(memory_store) group2 = zarr.api.synchronous.Group.open(memory_store) array_metadata = { @@ -298,7 +307,11 @@ async def test_not_writable_raises(self, memory_store: zarr.storage.MemoryStore) await consolidate_metadata(read_store) async def test_non_root_node(self, memory_store_with_hierarchy: Store) -> None: - await consolidate_metadata(memory_store_with_hierarchy, path="child") + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await consolidate_metadata(memory_store_with_hierarchy, path="child") root = await AsyncGroup.open(memory_store_with_hierarchy) child = await AsyncGroup.open(StorePath(memory_store_with_hierarchy) / "child") @@ -484,7 +497,14 @@ async def test_to_dict_order( await child.create_array("d", shape=(1,), dtype=dtype) # Consolidate metadata and re-open store - await zarr.api.asynchronous.consolidate_metadata(memory_store) + if zarr_format == 3: + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await zarr.api.asynchronous.consolidate_metadata(memory_store) + else: + await zarr.api.asynchronous.consolidate_metadata(memory_store) g2 = await zarr.api.asynchronous.open_group(store=memory_store) assert list(g2.metadata.consolidated_metadata.metadata) == ["a", "b", "c"] @@ -582,7 +602,14 @@ async def test_use_consolidated_false( await g.create_group(name="a") # test a stale read - await zarr.api.asynchronous.consolidate_metadata(memory_store) + if zarr_format == 3: + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await zarr.api.asynchronous.consolidate_metadata(memory_store) + else: + await zarr.api.asynchronous.consolidate_metadata(memory_store) await g.create_group(name="b") stale = await zarr.api.asynchronous.open_group(store=memory_store) @@ -597,7 +624,14 @@ async def test_use_consolidated_false( assert len([x async for x in good.members()]) == 2 # reconsolidate - await zarr.api.asynchronous.consolidate_metadata(memory_store) + if zarr_format == 3: + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await zarr.api.asynchronous.consolidate_metadata(memory_store) + else: + await zarr.api.asynchronous.consolidate_metadata(memory_store) good = await zarr.api.asynchronous.open_group(store=memory_store) assert len([x async for x in good.members()]) == 2 @@ -613,7 +647,11 @@ async def test_stale_child_metadata_ignored(self, memory_store: zarr.storage.Mem await zarr.api.asynchronous.consolidate_metadata(memory_store, path="foo") await root.create_group("foo/bar/spam") - await zarr.api.asynchronous.consolidate_metadata(memory_store) + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await zarr.api.asynchronous.consolidate_metadata(memory_store) reopened = await zarr.api.asynchronous.open_consolidated(store=memory_store, zarr_format=3) result = [x[0] async for x in reopened.members(max_depth=None)] @@ -637,7 +675,7 @@ async def test_use_consolidated_for_children_members( # Now according to the consolidated metadata, "a" has children ["b"] # but according to the unconsolidated metadata, "a" has children ["b", "c"] group = await zarr.api.asynchronous.open_group(store=memory_store, path="a") - with pytest.warns(UserWarning, match="Object at 'c' not found"): + with pytest.warns(ZarrUserWarning, match="Object at 'c' not found"): result = sorted([x[0] async for x in group.members(max_depth=None)]) expected = ["b"] assert result == expected @@ -655,7 +693,14 @@ async def test_consolidated_metadata_encodes_special_chars( ): root = await group(store=memory_store, zarr_format=zarr_format) _time = await root.create_array("time", shape=(12,), dtype=np.float64, fill_value=fill_value) - await zarr.api.asynchronous.consolidate_metadata(memory_store) + if zarr_format == 3: + with pytest.warns( + ZarrUserWarning, + match="Consolidated metadata is currently not part in the Zarr format 3 specification.", + ): + await zarr.api.asynchronous.consolidate_metadata(memory_store) + else: + await zarr.api.asynchronous.consolidate_metadata(memory_store) root = await group(store=memory_store, zarr_format=zarr_format) root_buffer = root.metadata.to_buffer_dict(default_buffer_prototype()) diff --git a/tests/test_metadata/test_v2.py b/tests/test_metadata/test_v2.py index a2894529aa..e18841f1f3 100644 --- a/tests/test_metadata/test_v2.py +++ b/tests/test_metadata/test_v2.py @@ -15,6 +15,7 @@ from zarr.core.group import ConsolidatedMetadata, GroupMetadata from zarr.core.metadata import ArrayV2Metadata from zarr.core.metadata.v2 import parse_zarr_format +from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from typing import Any @@ -93,7 +94,7 @@ def test_filters_empty_tuple_warns() -> None: "fill_value": 0, } with pytest.warns( - UserWarning, match="Found an empty list of filters in the array metadata document." + ZarrUserWarning, match="Found an empty list of filters in the array metadata document." ): meta = ArrayV2Metadata.from_dict(metadata_dict) assert meta.filters is None diff --git a/tests/test_store/test_fsspec.py b/tests/test_store/test_fsspec.py index abee298a8c..82a96b5d1e 100644 --- a/tests/test_store/test_fsspec.py +++ b/tests/test_store/test_fsspec.py @@ -14,6 +14,7 @@ from zarr.abc.store import OffsetByteRequest from zarr.core.buffer import Buffer, cpu, default_buffer_prototype from zarr.core.sync import _collect_aiterator, sync +from zarr.errors import ZarrUserWarning from zarr.storage import FsspecStore from zarr.storage._fsspec import _make_async from zarr.testing.store import StoreTests @@ -258,7 +259,7 @@ def test_init_warns_if_fs_asynchronous_is_false(self) -> None: f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=False ) store_kwargs = {"fs": fs, "path": path} - with pytest.warns(UserWarning, match=r".* was not created with `asynchronous=True`.*"): + with pytest.warns(ZarrUserWarning, match=r".* was not created with `asynchronous=True`.*"): self.store_cls(**store_kwargs) async def test_empty_nonexistent_path(self, store_kwargs: dict[str, Any]) -> None: diff --git a/tests/test_store/test_memory.py b/tests/test_store/test_memory.py index 4fc3f6e698..0b6bae757d 100644 --- a/tests/test_store/test_memory.py +++ b/tests/test_store/test_memory.py @@ -9,6 +9,7 @@ import zarr from zarr.core.buffer import Buffer, cpu, gpu +from zarr.errors import ZarrUserWarning from zarr.storage import GpuMemoryStore, MemoryStore from zarr.testing.store import StoreTests from zarr.testing.utils import gpu_test @@ -130,6 +131,8 @@ def test_from_dict(self) -> None: "a": gpu.Buffer.from_bytes(b"aaaa"), "b": cpu.Buffer.from_bytes(b"bbbb"), } - result = GpuMemoryStore.from_dict(d) + msg = "Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__ for zero-copy transfers, falling back to slow copy based path" + with pytest.warns(ZarrUserWarning, match=msg): + result = GpuMemoryStore.from_dict(d) for v in result._store_dict.values(): assert type(v) is gpu.Buffer diff --git a/tests/test_v2.py b/tests/test_v2.py index 4d17305995..70e8f2923f 100644 --- a/tests/test_v2.py +++ b/tests/test_v2.py @@ -21,6 +21,7 @@ from zarr.core.dtype.wrapper import ZDType from zarr.core.group import Group from zarr.core.sync import sync +from zarr.errors import ZarrDeprecationWarning from zarr.storage import MemoryStore, StorePath @@ -226,7 +227,7 @@ def test_v2_non_contiguous(numpy_order: Literal["C", "F"], zarr_order: Literal[" def test_default_compressor_deprecation_warning() -> None: - with pytest.warns(DeprecationWarning, match="default_compressor is deprecated"): + with pytest.warns(ZarrDeprecationWarning, match="default_compressor is deprecated"): zarr.storage.default_compressor = "zarr.codecs.zstd.ZstdCodec()" # type: ignore[attr-defined]