From 9c2a264918fa3d33d183cb123a40aa1f40130e7e Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 11 Dec 2025 13:23:15 +0100 Subject: [PATCH 1/4] Support zarr v3 --- pyproject.toml | 2 +- src/probeinterface/__init__.py | 10 ++++++++++ src/probeinterface/probe.py | 8 ++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0474a124..9ebdb619 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ test = [ "scipy", "pandas", "h5py", - "zarr>=2.16.0,<3.0.0" + "zarr>=3,<4" ] docs = [ diff --git a/src/probeinterface/__init__.py b/src/probeinterface/__init__.py index 3317c798..81a5c881 100644 --- a/src/probeinterface/__init__.py +++ b/src/probeinterface/__init__.py @@ -1,7 +1,17 @@ import importlib.metadata +import importlib.utils +from packaging.version import parse __version__ = importlib.metadata.version("probeinterface") +# If Zarr is installed, it must be >= 3.0.0 +ZARR_INSTALLED = importlib.utils.find_spec("zarr") is not None +if ZARR_INSTALLED: + import zarr + + if parse(zarr.__version__) < parse("3.0.0"): + raise ImportError("zarr version must be >= 3.0.0") + from .probe import Probe, select_axes from .probegroup import ProbeGroup diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index fb7ac241..707affaa 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -1152,7 +1152,7 @@ def add_probe_to_zarr_group(self, group: "zarr.Group") -> None: # add fields and contact annotations for field_name, (dtype, offset) in probe_arr.dtype.fields.items(): data = probe_arr[field_name] - group.create_dataset(name=field_name, data=data, dtype=dtype, chunks=False) + group.create_array(name=field_name, data=data, chunks=data.shape) # Annotations as a group (special attributes are stored as annotations) annotations_group = group.create_group("annotations") @@ -1161,8 +1161,8 @@ def add_probe_to_zarr_group(self, group: "zarr.Group") -> None: # Add planar contour if self.probe_planar_contour is not None: - group.create_dataset( - name="probe_planar_contour", data=self.probe_planar_contour, dtype="float64", chunks=False + group.create_array( + name="probe_planar_contour", data=self.probe_planar_contour, chunks=self.probe_planar_contour.shape ) def to_zarr(self, folder_path: str | Path) -> None: @@ -1219,7 +1219,7 @@ def from_zarr_group(group: "zarr.Group") -> "Probe": probe_arr_keys.append(key) dtype.append((key, dset.dtype)) if num_contacts is None: - num_contacts = len(dset) + num_contacts = dset.shape[0] # Create a structured array from the datasets probe_arr = np.zeros(num_contacts, dtype=dtype) From 66a0489bfacab00f4db02345e601193d58449f0a Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 11 Dec 2025 13:25:06 +0100 Subject: [PATCH 2/4] oups --- src/probeinterface/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/probeinterface/__init__.py b/src/probeinterface/__init__.py index 81a5c881..5d42fbe9 100644 --- a/src/probeinterface/__init__.py +++ b/src/probeinterface/__init__.py @@ -1,11 +1,11 @@ import importlib.metadata -import importlib.utils +import importlib.util from packaging.version import parse __version__ = importlib.metadata.version("probeinterface") # If Zarr is installed, it must be >= 3.0.0 -ZARR_INSTALLED = importlib.utils.find_spec("zarr") is not None +ZARR_INSTALLED = importlib.util.find_spec("zarr") is not None if ZARR_INSTALLED: import zarr From 77c4238e0c4f5777b22a4b99806dfe9ce92042c2 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Fri, 31 Jul 2026 12:01:47 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Alessio Buccino --- src/probeinterface/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/probeinterface/__init__.py b/src/probeinterface/__init__.py index 5c725b59..b15e7d78 100644 --- a/src/probeinterface/__init__.py +++ b/src/probeinterface/__init__.py @@ -1,17 +1,7 @@ import importlib.metadata -import importlib.util -from packaging.version import parse __version__ = importlib.metadata.version("probeinterface") -# If Zarr is installed, it must be >= 3.0.0 -ZARR_INSTALLED = importlib.util.find_spec("zarr") is not None -if ZARR_INSTALLED: - import zarr - - if parse(zarr.__version__) < parse("3.0.0"): - raise ImportError("zarr version must be >= 3.0.0") - from .probe import Probe, select_axes from .probegroup import ProbeGroup From 10e0f4f8d52ccf999f7536fa2581de8d0ed91ca7 Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Mon, 5 Oct 2026 11:19:01 -0400 Subject: [PATCH 4/4] Require Python 3.11 and zarr 3 No zarr 3 release supports Python 3.10, so the test dependencies of this branch could not be installed there. The minimum Python version is now 3.11, in the package metadata, the test matrix, and the Read the Docs build. The test workflow set up Python 3.11 for every job, whatever the matrix said, so the jobs named for Python 3.10 and 3.14 both ran on 3.11. It now uses the matrix version. Saving a probe with zarr 2 installed raises an ImportError that says zarr 3 is required, in place of an AttributeError. A new test saves a probe in a zarr format 2 group and reads it back, since that is the format existing files are in. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/full_tests.yml | 4 ++-- .readthedocs.yaml | 2 +- pyproject.toml | 2 +- src/probeinterface/probe.py | 3 +++ tests/test_probe.py | 15 +++++++++++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/full_tests.yml b/.github/workflows/full_tests.yml index 97d3bb19..3a5893e3 100644 --- a/.github/workflows/full_tests.yml +++ b/.github/workflows/full_tests.yml @@ -17,14 +17,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.14"] # Lower and higher versions we support + python-version: ["3.11", "3.14"] # Lower and higher versions we support os: [macos-latest, windows-latest, ubuntu-latest] steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version: ${{ matrix.python-version }} - name: Install package run: | python -m pip install --upgrade pip diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dd57770c..52a6640b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-24.04 tools: - python: "3.10" + python: "3.11" jobs: install: - pip install -U pip diff --git a/pyproject.toml b/pyproject.toml index af5c8fe9..26d17510 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ description = "Python package to handle probe layout, geometry and wiring to device." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index 4883b8d5..25debf23 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -1240,6 +1240,9 @@ def add_probe_to_zarr_group(self, group: "zarr.Group") -> None: group : zarr.Group The target Zarr group where the probe's data will be stored. """ + if not hasattr(group, "create_array"): + raise ImportError("Saving a probe to zarr requires zarr>=3") + probe_arr = self.to_numpy(complete=True) # add fields and contact annotations diff --git a/tests/test_probe.py b/tests/test_probe.py index 136953ea..129c62fb 100644 --- a/tests/test_probe.py +++ b/tests/test_probe.py @@ -229,6 +229,21 @@ def test_save_to_zarr(tmp_path): assert probe == reloaded_probe, "Reloaded Probe object does not match the original" +@pytest.mark.parametrize("zarr_format", [2, 3]) +def test_zarr_group_formats(tmp_path, zarr_format): + # A probe saved in a zarr format 2 group, as zarr-python 2 wrote it, is still readable + import zarr + + probe = generate_dummy_probe() + folder_path = Path(tmp_path) / "probe.zarr" + + group = zarr.open_group(folder_path, mode="w", zarr_format=zarr_format) + probe.add_probe_to_zarr_group(group) + + reloaded_probe = Probe.from_zarr(folder_path=folder_path) + assert probe == reloaded_probe + + def test_position_uniqueness(): """Test that the error message matches the full expected string for three duplicates using pytest's match regex.""" import re