feat: default export dir to <install root>\合并后的图片 when packaged
get_output_dir() now returns <install root>\合并后的图片 (parent of app\) for frozen builds — next to Launcher.exe, easy to find, not replaced on update. Falls back to <data_dir>/output if the install root is read-only; dev mode unchanged. The export panel already uses get_output_dir(), so its default follows automatically. - tests/test_file_service.py: +3 (dev, frozen install-root, fallback) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,8 +79,36 @@ def get_log_dir():
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dir_writable(path):
|
||||||
|
"""Create *path* and probe it; True if a file can be written there."""
|
||||||
|
try:
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
probe = path / ".write_test"
|
||||||
|
probe.write_text("x", encoding="ascii")
|
||||||
|
probe.unlink()
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Default export folder name, placed next to Launcher.exe in the packaged layout.
|
||||||
|
_OUTPUT_DIR_NAME = "合并后的图片"
|
||||||
|
|
||||||
|
|
||||||
def get_output_dir():
|
def get_output_dir():
|
||||||
"""Return <data_dir>/output/ as a Path, creating the directory if absent."""
|
"""Return the default export directory as a Path, creating it if absent.
|
||||||
|
|
||||||
|
Packaged: <install root>\\合并后的图片 (next to Launcher.exe — easy to find
|
||||||
|
and NOT replaced on update, unlike app\\). Install root is the parent of the
|
||||||
|
app\\ folder (get_app_dir()). Falls back to <data_dir>/output if the install
|
||||||
|
root is not writable. Development: <data_dir>/output (the project dir).
|
||||||
|
|
||||||
|
This is only the default; the user's chosen output_dir takes precedence.
|
||||||
|
"""
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
candidate = get_app_dir().parent / _OUTPUT_DIR_NAME
|
||||||
|
if _is_dir_writable(candidate):
|
||||||
|
return candidate
|
||||||
d = get_data_dir() / "output"
|
d = get_data_dir() / "output"
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
return d
|
return d
|
||||||
|
|||||||
@@ -1002,8 +1002,8 @@
|
|||||||
任务:
|
任务:
|
||||||
|
|
||||||
- [x] 文档:`docs/10` §4/§5、`docs/02` 配置说明、`docs/07` §7.5 对齐
|
- [x] 文档:`docs/10` §4/§5、`docs/02` 配置说明、`docs/07` §7.5 对齐
|
||||||
- [ ] `file_service.get_output_dir()`:打包态优先 `<安装根>\合并后的图片`(安装根 = `get_app_dir().parent`),不可写回退 `get_data_dir()/output`;开发态用项目目录;补单测
|
- [x] `file_service.get_output_dir()`:打包态优先 `<安装根>\合并后的图片`(安装根 = `get_app_dir().parent`),不可写回退 `get_data_dir()/output`;开发态用项目目录;`tests/test_file_service.py` 3 个单测
|
||||||
- [ ] 导出面板默认值随之变化(沿用 `get_output_dir()`,无需单独改)
|
- [x] 导出面板默认值随之变化(沿用 `get_output_dir()`,无需单独改)
|
||||||
- [ ] GUI 实测:打包运行后默认导出到 `<安装根>\合并后的图片`,且更新后仍在
|
- [ ] GUI 实测:打包运行后默认导出到 `<安装根>\合并后的图片`,且更新后仍在
|
||||||
|
|
||||||
## 18. 后续暂缓任务
|
## 18. 后续暂缓任务
|
||||||
|
|||||||
@@ -52,5 +52,56 @@ class TestGetDataDir(unittest.TestCase):
|
|||||||
self.assertEqual(fs.get_data_dir(), fs.get_app_dir())
|
self.assertEqual(fs.get_data_dir(), fs.get_app_dir())
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetOutputDir(unittest.TestCase):
|
||||||
|
"""get_output_dir() default location (docs/10-lan-update.md §5)."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
import tempfile
|
||||||
|
self.tmp = Path(tempfile.mkdtemp())
|
||||||
|
self._env = os.environ.get("CMBOT_DATA_DIR")
|
||||||
|
self._frozen = getattr(sys, "frozen", None)
|
||||||
|
os.environ["CMBOT_DATA_DIR"] = str(self.tmp / "data")
|
||||||
|
if hasattr(sys, "frozen"):
|
||||||
|
del sys.frozen
|
||||||
|
self._orig_app_dir = fs.get_app_dir
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
import shutil
|
||||||
|
fs.get_app_dir = self._orig_app_dir
|
||||||
|
if self._env is None:
|
||||||
|
os.environ.pop("CMBOT_DATA_DIR", None)
|
||||||
|
else:
|
||||||
|
os.environ["CMBOT_DATA_DIR"] = self._env
|
||||||
|
if self._frozen is None:
|
||||||
|
if hasattr(sys, "frozen"):
|
||||||
|
del sys.frozen
|
||||||
|
else:
|
||||||
|
sys.frozen = self._frozen
|
||||||
|
shutil.rmtree(str(self.tmp), ignore_errors=True)
|
||||||
|
|
||||||
|
def test_dev_uses_data_output(self):
|
||||||
|
# not frozen -> <data_dir>/output
|
||||||
|
self.assertEqual(fs.get_output_dir(), Path(os.environ["CMBOT_DATA_DIR"]) / "output")
|
||||||
|
|
||||||
|
def test_frozen_uses_install_root(self):
|
||||||
|
app = self.tmp / "install" / "app"
|
||||||
|
app.mkdir(parents=True)
|
||||||
|
fs.get_app_dir = lambda: app
|
||||||
|
sys.frozen = True
|
||||||
|
self.assertEqual(fs.get_output_dir(), self.tmp / "install" / "合并后的图片")
|
||||||
|
|
||||||
|
def test_frozen_falls_back_when_not_writable(self):
|
||||||
|
app = self.tmp / "install" / "app"
|
||||||
|
app.mkdir(parents=True)
|
||||||
|
fs.get_app_dir = lambda: app
|
||||||
|
sys.frozen = True
|
||||||
|
orig = fs._is_dir_writable
|
||||||
|
fs._is_dir_writable = lambda p: False
|
||||||
|
try:
|
||||||
|
self.assertEqual(fs.get_output_dir(), Path(os.environ["CMBOT_DATA_DIR"]) / "output")
|
||||||
|
finally:
|
||||||
|
fs._is_dir_writable = orig
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user