Skip to content
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
34 changes: 34 additions & 0 deletions PyReconstruct/modules/backend/func/zarr_naming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Naming/location helpers for the "convert images to scaled zarr" action.

Kept Qt-free so the default-path and suffix logic is unit-testable without a GUI.
The store name must end in ``zarr`` -- ``image_layer.ImageLayer.is_zarr_file`` is
``series.src_dir.endswith("zarr")`` -- so ``ensure_zarr_suffix`` enforces that
invariant regardless of what the user types or how the save dialog behaves.
"""
import os


def default_scaled_zarr_fp(src_dir: str, series_name: str) -> str:
"""Default save path for a newly converted scaled zarr.

Places ``<series>.zarr`` as a **sibling** of the source image directory (one
level up), so the zarr sits next to the images rather than inside them. When
the source directory is empty/unknown, returns just the filename and lets the
save dialog fall back to its last-used folder.
"""
name = f"{series_name}.zarr"
if src_dir:
return os.path.join(os.path.dirname(os.path.normpath(src_dir)), name)
return name


def ensure_zarr_suffix(fp: str) -> str:
"""Guarantee the store path ends in ``zarr`` (what ``is_zarr_file`` checks).

A name already ending in ``zarr`` is left untouched -- so an override like
``proj.zarr`` or ``proj-zarr`` is respected -- and ``.zarr`` is appended
otherwise (e.g. the user typed a bare ``proj``).
"""
if not fp:
return fp
return fp if os.path.normpath(fp).endswith("zarr") else fp + ".zarr"
10 changes: 8 additions & 2 deletions PyReconstruct/modules/gui/main/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,14 +413,20 @@ def srcToZarr(self, create_new=True):
return

if create_new:
from PyReconstruct.modules.backend.func.zarr_naming import (
default_scaled_zarr_fp, ensure_zarr_suffix,
)
zarr_fp = FileDialog.get(
"save",
self,
"Convert Images to scaled zarr",
file_name=f"{self.series.name}_images.zarr",
filter="Zarr Directory (*.zarr)"
file_name=default_scaled_zarr_fp(self.series.src_dir, self.series.name),
filter="Zarr Directory (*zarr)"
)
if not zarr_fp: return
# is_zarr_file needs the store name to end in "zarr"; enforce it here
# rather than rely on the save dialog's platform-dependent suffixing.
zarr_fp = ensure_zarr_suffix(zarr_fp)

zarr_converter = Path(assets_dir) / "scripts/start_process.py"
launch_prefix = script_launch_prefix()
Expand Down
62 changes: 62 additions & 0 deletions tests/test_zarr_convert_naming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Default naming/location for the "convert images to scaled zarr" action.

Two invariants:
* the default store sits NEXT TO the source image directory (one level up),
not inside it, and is named ``<series>.zarr`` (not the old ``_images.zarr``);
* the final store name always ends in ``zarr`` -- ``ImageLayer.is_zarr_file`` is
``series.src_dir.endswith("zarr")``, so anything else would break detection.
"""
import os

from PyReconstruct.modules.backend.func.zarr_naming import (
default_scaled_zarr_fp,
ensure_zarr_suffix,
)


def test_default_is_sibling_of_src_dir_named_after_series():
assert default_scaled_zarr_fp("/data/exp/images", "myseries") == \
os.path.join("/data/exp", "myseries.zarr")


def test_default_is_next_to_not_within_src_dir():
src = "/data/exp/images"
out = default_scaled_zarr_fp(src, "s")
assert os.path.dirname(out) == os.path.dirname(os.path.normpath(src))
assert not out.startswith(os.path.normpath(src) + os.sep) # NOT inside src


def test_default_normalizes_trailing_separator():
assert default_scaled_zarr_fp("/data/exp/images/", "s") == \
os.path.join("/data/exp", "s.zarr")


def test_default_falls_back_to_bare_name_without_src_dir():
assert default_scaled_zarr_fp("", "s") == "s.zarr"
assert default_scaled_zarr_fp(None, "s") == "s.zarr"


def test_default_uses_dot_zarr_not_underscore_images():
out = default_scaled_zarr_fp("/a/b", "proj")
assert out.endswith("proj.zarr")
assert "_images" not in out


def test_ensure_suffix_leaves_zarr_names_untouched():
assert ensure_zarr_suffix("/x/proj.zarr") == "/x/proj.zarr"
assert ensure_zarr_suffix("/x/proj-zarr") == "/x/proj-zarr" # already ends in "zarr"


def test_ensure_suffix_appends_when_missing():
assert ensure_zarr_suffix("/x/proj") == "/x/proj.zarr"
assert ensure_zarr_suffix("/x/output") == "/x/output.zarr"


def test_ensure_suffix_always_satisfies_is_zarr_detection():
# ImageLayer.is_zarr_file == src_dir.endswith("zarr")
for name in ("proj", "proj.zarr", "proj-zarr", "weird_name"):
assert ensure_zarr_suffix(f"/x/{name}").endswith("zarr")


def test_ensure_suffix_empty_is_noop():
assert ensure_zarr_suffix("") == ""
Loading