Skip to content

Public API for buffer objects #2876

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 13 commits into from
Jun 18, 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
8 changes: 8 additions & 0 deletions changes/2871.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Added public API for Buffer ABCs and implementations.

Use :mod:`zarr.buffer` to access buffer implementations, and
:mod:`zarr.abc.buffer` for the interface to implement new buffer types.

Users previously importing buffer from ``zarr.core.buffer`` should update their
imports to use :mod:`zarr.buffer`. As a reminder, all of ``zarr.core`` is
considered a private API that's not covered by zarr-python's versioning policy.
4 changes: 2 additions & 2 deletions docs/user-guide/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ This is the current default configuration::
'variable-length-string': {'name': 'vlen-utf8'}},
'write_empty_chunks': False},
'async': {'concurrency': 10, 'timeout': None},
'buffer': 'zarr.core.buffer.cpu.Buffer',
'buffer': 'zarr.buffer.cpu.Buffer',
'codec_pipeline': {'batch_size': 1,
'path': 'zarr.core.codec_pipeline.BatchedCodecPipeline'},
'codecs': {'blosc': 'zarr.codecs.blosc.BloscCodec',
Expand All @@ -78,5 +78,5 @@ This is the current default configuration::
'zstd': 'zarr.codecs.zstd.ZstdCodec'},
'default_zarr_format': 3,
'json_indent': 2,
'ndbuffer': 'zarr.core.buffer.cpu.NDBuffer',
'ndbuffer': 'zarr.buffer.cpu.NDBuffer',
'threading': {'max_workers': None}}
5 changes: 4 additions & 1 deletion docs/user-guide/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ Coming soon.
Custom array buffers
--------------------

Coming soon.
Zarr-python provides control over where and how arrays stored in memory through
:mod:`zarr.buffer`. Currently both CPU (the default) and GPU implementations are
provided (see :ref:`user-guide-gpu` for more). You can implement your own buffer
classes by implementing the interface defined in :mod:`zarr.abc.buffer`.

Other extensions
----------------
Expand Down
9 changes: 9 additions & 0 deletions src/zarr/abc/buffer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from zarr.core.buffer.core import ArrayLike, Buffer, BufferPrototype, NDArrayLike, NDBuffer

Check warning on line 1 in src/zarr/abc/buffer.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/abc/buffer.py#L1

Added line #L1 was not covered by tests

__all__ = [

Check warning on line 3 in src/zarr/abc/buffer.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/abc/buffer.py#L3

Added line #L3 was not covered by tests
"ArrayLike",
"Buffer",
"BufferPrototype",
"NDArrayLike",
"NDBuffer",
]
12 changes: 12 additions & 0 deletions src/zarr/buffer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
Implementations of the Zarr Buffer interface.

See Also
========
zarr.abc.buffer: Abstract base class for the Zarr Buffer interface.
"""

from zarr.buffer import cpu, gpu
from zarr.core.buffer import default_buffer_prototype

Check warning on line 10 in src/zarr/buffer/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/buffer/__init__.py#L9-L10

Added lines #L9 - L10 were not covered by tests

__all__ = ["cpu", "default_buffer_prototype", "gpu"]

Check warning on line 12 in src/zarr/buffer/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/buffer/__init__.py#L12

Added line #L12 was not covered by tests
15 changes: 15 additions & 0 deletions src/zarr/buffer/cpu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from zarr.core.buffer.cpu import (

Check warning on line 1 in src/zarr/buffer/cpu.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/buffer/cpu.py#L1

Added line #L1 was not covered by tests
Buffer,
NDBuffer,
as_numpy_array_wrapper,
buffer_prototype,
numpy_buffer_prototype,
)

__all__ = [

Check warning on line 9 in src/zarr/buffer/cpu.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/buffer/cpu.py#L9

Added line #L9 was not covered by tests
"Buffer",
"NDBuffer",
"as_numpy_array_wrapper",
"buffer_prototype",
"numpy_buffer_prototype",
]
7 changes: 7 additions & 0 deletions src/zarr/buffer/gpu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from zarr.core.buffer.gpu import Buffer, NDBuffer, buffer_prototype

Check warning on line 1 in src/zarr/buffer/gpu.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/buffer/gpu.py#L1

Added line #L1 was not covered by tests

__all__ = [

Check warning on line 3 in src/zarr/buffer/gpu.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/buffer/gpu.py#L3

Added line #L3 was not covered by tests
"Buffer",
"NDBuffer",
"buffer_prototype",
]
9 changes: 7 additions & 2 deletions src/zarr/core/buffer/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,5 +224,10 @@ def numpy_buffer_prototype() -> core.BufferPrototype:
return core.BufferPrototype(buffer=Buffer, nd_buffer=NDBuffer)


register_buffer(Buffer)
register_ndbuffer(NDBuffer)
register_buffer(Buffer, qualname="zarr.buffer.cpu.Buffer")
register_ndbuffer(NDBuffer, qualname="zarr.buffer.cpu.NDBuffer")


# backwards compatibility
register_buffer(Buffer, qualname="zarr.core.buffer.cpu.Buffer")
register_ndbuffer(NDBuffer, qualname="zarr.core.buffer.cpu.NDBuffer")
8 changes: 6 additions & 2 deletions src/zarr/core/buffer/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,9 @@ def __setitem__(self, key: Any, value: Any) -> None:

buffer_prototype = BufferPrototype(buffer=Buffer, nd_buffer=NDBuffer)

register_buffer(Buffer)
register_ndbuffer(NDBuffer)
register_buffer(Buffer, qualname="zarr.buffer.gpu.Buffer")
register_ndbuffer(NDBuffer, qualname="zarr.buffer.gpu.NDBuffer")

# backwards compatibility
register_buffer(Buffer, qualname="zarr.core.buffer.gpu.Buffer")
register_ndbuffer(NDBuffer, qualname="zarr.core.buffer.gpu.NDBuffer")
6 changes: 3 additions & 3 deletions src/zarr/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def enable_gpu(self) -> ConfigSet:
Configure Zarr to use GPUs where possible.
"""
return self.set(
{"buffer": "zarr.core.buffer.gpu.Buffer", "ndbuffer": "zarr.core.buffer.gpu.NDBuffer"}
{"buffer": "zarr.buffer.gpu.Buffer", "ndbuffer": "zarr.buffer.gpu.NDBuffer"}
)


Expand Down Expand Up @@ -128,8 +128,8 @@ def enable_gpu(self) -> ConfigSet:
"vlen-utf8": "zarr.codecs.vlen_utf8.VLenUTF8Codec",
"vlen-bytes": "zarr.codecs.vlen_utf8.VLenBytesCodec",
},
"buffer": "zarr.core.buffer.cpu.Buffer",
"ndbuffer": "zarr.core.buffer.cpu.NDBuffer",
"buffer": "zarr.buffer.cpu.Buffer",
"ndbuffer": "zarr.buffer.cpu.NDBuffer",
}
],
)
Expand Down
2 changes: 1 addition & 1 deletion src/zarr/core/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -1454,7 +1454,7 @@ async def create_hierarchy(
group already exists at path ``a``, then this function will leave the group at ``a`` as-is.

Yields
-------
------
tuple[str, AsyncArray | AsyncGroup].
"""
# check that all the nodes have the same zarr_format as Self
Expand Down
14 changes: 8 additions & 6 deletions src/zarr/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ def lazy_load(self) -> None:

self.lazy_load_list.clear()

def register(self, cls: type[T]) -> None:
self[fully_qualified_name(cls)] = cls
def register(self, cls: type[T], qualname: str | None = None) -> None:
if qualname is None:
qualname = fully_qualified_name(cls)
self[qualname] = cls


__codec_registries: dict[str, Registry[Codec]] = defaultdict(Registry)
Expand Down Expand Up @@ -131,12 +133,12 @@ def register_pipeline(pipe_cls: type[CodecPipeline]) -> None:
__pipeline_registry.register(pipe_cls)


def register_ndbuffer(cls: type[NDBuffer]) -> None:
__ndbuffer_registry.register(cls)
def register_ndbuffer(cls: type[NDBuffer], qualname: str | None = None) -> None:
__ndbuffer_registry.register(cls, qualname)


def register_buffer(cls: type[Buffer]) -> None:
__buffer_registry.register(cls)
def register_buffer(cls: type[Buffer], qualname: str | None = None) -> None:
__buffer_registry.register(cls, qualname)


def get_codec_class(key: str, reload_config: bool = False) -> type[Codec]:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import pytest

import zarr
from zarr.abc.buffer import ArrayLike, BufferPrototype, NDArrayLike
from zarr.buffer import cpu, gpu
from zarr.codecs.blosc import BloscCodec
from zarr.codecs.crc32c_ import Crc32cCodec
from zarr.codecs.gzip import GzipCodec
from zarr.codecs.transpose import TransposeCodec
from zarr.codecs.zstd import ZstdCodec
from zarr.core.buffer import ArrayLike, BufferPrototype, NDArrayLike, cpu, gpu
from zarr.storage import MemoryStore, StorePath
from zarr.testing.buffer import (
NDBufferUsingTestNDArrayLike,
Expand Down
30 changes: 24 additions & 6 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ def test_config_defaults_set() -> None:
"vlen-utf8": "zarr.codecs.vlen_utf8.VLenUTF8Codec",
"vlen-bytes": "zarr.codecs.vlen_utf8.VLenBytesCodec",
},
"buffer": "zarr.core.buffer.cpu.Buffer",
"ndbuffer": "zarr.core.buffer.cpu.NDBuffer",
"buffer": "zarr.buffer.cpu.Buffer",
"ndbuffer": "zarr.buffer.cpu.NDBuffer",
}
]
)
Expand Down Expand Up @@ -224,9 +224,6 @@ class NewBloscCodec(BloscCodec):

@pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"])
def test_config_ndbuffer_implementation(store: Store) -> None:
# has default value
assert fully_qualified_name(get_ndbuffer_class()) == config.defaults[0]["ndbuffer"]

# set custom ndbuffer with TestNDArrayLike implementation
register_ndbuffer(NDBufferUsingTestNDArrayLike)
with config.set({"ndbuffer": fully_qualified_name(NDBufferUsingTestNDArrayLike)}):
Expand All @@ -244,7 +241,7 @@ def test_config_ndbuffer_implementation(store: Store) -> None:

def test_config_buffer_implementation() -> None:
# has default value
assert fully_qualified_name(get_buffer_class()) == config.defaults[0]["buffer"]
assert config.defaults[0]["buffer"] == "zarr.buffer.cpu.Buffer"

arr = zeros(shape=(100,), store=StoreExpectingTestBuffer())

Expand Down Expand Up @@ -279,6 +276,27 @@ def test_config_buffer_implementation() -> None:
assert np.array_equal(arr_Crc32c[:], data2d)


def test_config_buffer_backwards_compatibility() -> None:
# This should warn once zarr.core is private
# https://github.com/zarr-developers/zarr-python/issues/2621
with zarr.config.set(
{"buffer": "zarr.core.buffer.cpu.Buffer", "ndbuffer": "zarr.core.buffer.cpu.NDBuffer"}
):
get_buffer_class()
get_ndbuffer_class()


@pytest.mark.gpu
def test_config_buffer_backwards_compatibility_gpu() -> None:
# This should warn once zarr.core is private
# https://github.com/zarr-developers/zarr-python/issues/2621
with zarr.config.set(
{"buffer": "zarr.core.buffer.gpu.Buffer", "ndbuffer": "zarr.core.buffer.gpu.NDBuffer"}
):
get_buffer_class()
get_ndbuffer_class()


@pytest.mark.filterwarnings("error")
def test_warning_on_missing_codec_config() -> None:
class NewCodec(BytesCodec):
Expand Down