Skip to content

Commit bdcb3e9

Browse files
authored
Merge pull request #21 from maple60/codex/fix-output-directory-creation-logic
スタンドアロン/開発実行での output ディレクトリ解決を一元化
2 parents 6954789 + 7a21b6b commit bdcb3e9

7 files changed

Lines changed: 84 additions & 11 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Path utilities for output/export directories."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
import sys
7+
8+
from appdirs import user_data_dir
9+
10+
_APP_DIR_NAME = "leaf_contour_efd"
11+
12+
13+
def _user_writable_base_dir() -> Path:
14+
"""Return a user-writable directory for application outputs."""
15+
return Path(user_data_dir(_APP_DIR_NAME)).resolve()
16+
17+
18+
def _is_writable_dir(path: Path) -> bool:
19+
"""Return whether ``path`` can be used as a writable directory."""
20+
try:
21+
path.mkdir(parents=True, exist_ok=True)
22+
probe = path / ".write_test"
23+
with probe.open("w", encoding="utf-8"):
24+
pass
25+
probe.unlink(missing_ok=True)
26+
return True
27+
except OSError:
28+
return False
29+
30+
31+
def get_output_base_dir() -> Path:
32+
"""Return the base directory used for writing the ``output`` folder.
33+
34+
Rules
35+
-----
36+
- Frozen on all platforms: user-writable application data directory.
37+
- Non-frozen: repository/application root when writable, otherwise user data.
38+
"""
39+
fallback = _user_writable_base_dir()
40+
41+
if getattr(sys, "frozen", False):
42+
return fallback
43+
44+
# Development mode: src/leaf_contour_efd/utils/paths.py -> repo root
45+
repo_root = Path(__file__).resolve().parents[3]
46+
if _is_writable_dir(repo_root):
47+
return repo_root
48+
return fallback
49+
50+
51+
def get_output_dir(*parts: str) -> Path:
52+
"""Return an output subdirectory and ensure it exists.
53+
54+
If directory creation fails in the primary base location, this function
55+
transparently falls back to a user-writable directory.
56+
"""
57+
out_dir = get_output_base_dir() / "output"
58+
if parts:
59+
out_dir = out_dir.joinpath(*parts)
60+
61+
try:
62+
out_dir.mkdir(parents=True, exist_ok=True)
63+
except OSError:
64+
out_dir = _user_writable_base_dir() / "output"
65+
if parts:
66+
out_dir = out_dir.joinpath(*parts)
67+
out_dir.mkdir(parents=True, exist_ok=True)
68+
69+
return out_dir

src/leaf_contour_efd/widgets/binarize_image.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from napari.layers import Image
2727
from napari.utils.colormaps import DirectLabelColormap
2828
from pathlib import Path
29+
from leaf_contour_efd.utils.paths import get_output_dir
2930

3031

3132
def make_binarize_image_widget(viewer: "napari.Viewer"):
@@ -223,7 +224,7 @@ def binarize_image(
223224
method: str = "Otsu",
224225
threshold: int = 128,
225226
save_check: bool = True,
226-
save_dir: Path = Path("./output/binarized_images"),
227+
save_dir: Path = get_output_dir("binarized_images"),
227228
):
228229
"""
229230
Run the selected binarization method

src/leaf_contour_efd/widgets/calculate_efd.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import pandas as pd
1616
from pathlib import Path
1717
import copy
18+
from leaf_contour_efd.utils.paths import get_output_dir
1819

1920

2021
def close_contour(df_contour: pd.DataFrame) -> np.ndarray:
@@ -391,10 +392,8 @@ def calculate_efd_and_save(payload) -> None:
391392
# fallback to plain string
392393
leaf_id = str(leaf_id)
393394

394-
output_dir_ef = Path("output/coefficients_efd")
395-
output_dir_ef_normalized = Path("output/coefficients_efd_normalized")
396-
output_dir_ef.mkdir(parents=True, exist_ok=True)
397-
output_dir_ef_normalized.mkdir(parents=True, exist_ok=True)
395+
output_dir_ef = get_output_dir("coefficients_efd")
396+
output_dir_ef_normalized = get_output_dir("coefficients_efd_normalized")
398397
ef_file = output_dir_ef / f"{id}_{leaf_id}.csv"
399398
ef_normalized_file = output_dir_ef_normalized / f"{id}_{leaf_id}.csv"
400399
df_ef.to_csv(ef_file, index=False)

src/leaf_contour_efd/widgets/clear_viewer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import Sequence
2525

2626
from qtpy.QtCore import QTimer
27+
from leaf_contour_efd.utils.paths import get_output_dir
2728

2829

2930
def _clear_except_async(viewer, keep_names: set[str], on_done=None):
@@ -151,7 +152,7 @@ def _get_base_image_id() -> str:
151152

152153
# ---- create FileEdit with dynamic default path ----
153154
default_image_id = _get_base_image_id()
154-
default_dir = Path("output/rois")
155+
default_dir = get_output_dir("rois")
155156
# default_dir.mkdir(parents=True, exist_ok=True)
156157
default_path = default_dir / f"{default_image_id}.png"
157158

src/leaf_contour_efd/widgets/crop_rectangle.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from pathlib import Path
2727
import napari
2828
from qtpy.QtCore import QTimer
29+
from leaf_contour_efd.utils.paths import get_output_dir
2930

3031
COLOR_CYCLE = ["#FF4B00", "#005AFF"] # base(orange), tip(blue)
3132
LABELS = ("base", "tip")
@@ -243,7 +244,7 @@ def _apply():
243244
def add_roi(
244245
roi_index: int = 1,
245246
save_cropped: bool = True,
246-
save_dir: str = "output/cropped_images",
247+
save_dir: Path = get_output_dir("cropped_images"),
247248
):
248249
"""Add a cropped ROI image and its landmark layer."""
249250
ROIs_layer = viewer.layers["ROIs"]

src/leaf_contour_efd/widgets/extract_contour.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from datetime import datetime
3030
from napari.utils.colormaps import DirectLabelColormap
3131
from qtpy.QtCore import QTimer
32+
from leaf_contour_efd.utils.paths import get_output_dir
3233

3334

3435
def make_extract_contour_widget(viewer: "napari.Viewer"):
@@ -76,9 +77,9 @@ def make_extract_contour_widget(viewer: "napari.Viewer"):
7677
call_button="Extract Contour",
7778
)
7879
def extract_contour(
79-
folder_csv=Path("./output/contour"),
80-
folder_final_mask=Path("./output/binarized_image_final"),
81-
folder_blob_mask=Path("./output/contour_image"),
80+
folder_csv=Path(get_output_dir("contour")),
81+
folder_final_mask=Path(get_output_dir("binarized_image_final")),
82+
folder_blob_mask=Path(get_output_dir("contour_image")),
8283
save_final_mask=True,
8384
save_blob_mask=True,
8485
):

src/leaf_contour_efd/widgets/rotate_image.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from pathlib import Path
2828
from numbers import Integral
2929
import copy
30+
from leaf_contour_efd.utils.paths import get_output_dir
3031

3132

3233
# -------------------------------------------------------------------------
@@ -289,7 +290,7 @@ def make_points_metadata_widget(
289290
def points_metadata_widget(
290291
viewer: "napari.Viewer",
291292
save_rotated: bool = True,
292-
save_dir: str = "output/rotated_images",
293+
save_dir: Path = get_output_dir("rotated_images"),
293294
) -> dict[str, Any]:
294295
"""Rotate the image corresponding to the current Points layer."""
295296
try:

0 commit comments

Comments
 (0)