diff --git a/src/patchworks/_review.py b/src/patchworks/_review.py index 267bbe0..9a1c50c 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 7a58019..505a03d 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 fcd5c5b..66d83a5 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)