From 24d243f9e5abb41f6de9cd3b2365d00532f006c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 11:37:16 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Review:=20read=20a=20zipped=20st?= =?UTF-8?q?ore's=20label=20groups=20in=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review._load_decisions and nearest_parents opened groups with a plain zarr.open_group, which cannot follow bundle.zip/labels/. So the corrected view of a .zip bundle failed, and napari-chunked-regionprops fell back to the raw table. Use open_group_any, like the rest of the read path. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PfZ9brRCJ2iCMswgjuSC1x --- src/patchworks/_review.py | 5 ++++- src/patchworks/_tables.py | 2 +- tests/test_review.py | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/patchworks/_review.py b/src/patchworks/_review.py index 267bbe07..9a1c50ce 100644 --- a/src/patchworks/_review.py +++ b/src/patchworks/_review.py @@ -291,9 +291,12 @@ def _table_group(self, name: str, mode: str = "r"): ) def _load_decisions(self, name: str) -> None: + from ._io import open_group_any + log = dict(self._table_group(name).attrs.get(REVIEW_KEY) or {}) + # open_group_any: a .zip bundle is read in place (never saved to). fingerprint = label_fingerprint( - zarr.open_group(_label_group(self.store, name), mode="r") + open_group_any(_label_group(self.store, name)) ) decisions = log.get("decisions") or {} if decisions and log.get("labels") != fingerprint: diff --git a/src/patchworks/_tables.py b/src/patchworks/_tables.py index 7a580193..505a03d5 100644 --- a/src/patchworks/_tables.py +++ b/src/patchworks/_tables.py @@ -822,7 +822,7 @@ def nearest_parents( size = read_pixel_size(cpath) spacing = np.array([float(size.get(ax, 1.0)) for ax in axes]) margin = np.ceil(max_distance_um / spacing).astype(int) - table = zarr.open_group(f"{cpath}/{TABLE_GROUP}", mode="r") + table = open_group_any(f"{cpath}/{TABLE_GROUP}") all_labels = table["label"][...] lo = np.stack([np.asarray(table[f"bbox_min_{ax}"][...]) for ax in axes], 1) hi = np.stack([np.asarray(table[f"bbox_max_{ax}"][...]) for ax in axes], 1) diff --git a/tests/test_review.py b/tests/test_review.py index fcd5c5b5..66d83a54 100644 --- a/tests/test_review.py +++ b/tests/test_review.py @@ -69,6 +69,31 @@ def test_decisions_apply_everywhere_and_persist(scene): assert 4 in Review(scene).effective("cilia_labels").index +def test_a_zip_bundle_is_reviewed_read_only(scene): + """A zipped store opens (napari-chunked-regionprops shows its corrected + tables through this), keeps its decisions, and refuses new ones.""" + import os + import zipfile + + rv = Review(scene, expect=EXPECT) + rv.decide("cilia_labels", 4, "wrong") + bundle = f"{scene}.zip" + root = os.path.dirname(str(scene)) + with zipfile.ZipFile(bundle, "w") as archive: + for here, _, files in os.walk(scene): + for f in files: + path = os.path.join(here, f) + archive.write(path, os.path.relpath(path, root)) + + zipped = Review(bundle, expect=EXPECT) + assert 4 not in zipped.effective("cilia_labels").index + pd.testing.assert_frame_equal( + zipped.effective("cyto_labels"), rv.effective("cyto_labels") + ) + with pytest.raises(PermissionError): + zipped.decide("cilia_labels", 5, "wrong") + + def test_merging_a_parent_moves_its_children(scene): rv = Review(scene, expect=EXPECT) rv.decide("cyto_labels", 2, "merge", into=1)