T-581 AI生成打开图片文件夹

This commit is contained in:
chengma
2026-07-10 10:46:52 +08:00
parent 68afe34a8d
commit 87fcda47e7
5 changed files with 366 additions and 3 deletions
+36
View File
@@ -0,0 +1,36 @@
"""Helpers for opening local folders from the GUI."""
from __future__ import annotations
import os
import subprocess
import sys
def open_in_file_manager(path: str) -> str:
"""Open an existing directory with the platform file manager.
Returns the absolute path that was opened. The helper never creates
directories; callers should present FileNotFoundError as a user-facing
warning.
"""
target = os.path.abspath(str(path or ""))
if not os.path.isdir(target):
raise FileNotFoundError(f"目录不存在:{target}")
if sys.platform.startswith("win"):
startfile = getattr(os, "startfile", None)
if startfile is None:
raise OSError("当前系统不支持打开文件夹")
startfile(target)
return target
command = ["open", target] if sys.platform == "darwin" else ["xdg-open", target]
subprocess.Popen(
command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
shell=False,
)
return target
+86
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from PySide6.QtCore import QEvent, QRectF, QSize from PySide6.QtCore import QEvent, QRectF, QSize
from PySide6.QtGui import QColor, QPainter from PySide6.QtGui import QColor, QPainter
from .. import file_manager
from ..models import GenerateTaskTableModel from ..models import GenerateTaskTableModel
from ..widgets import * from ..widgets import *
from ..workers import GenerateWorker as _RealGenerateWorker from ..workers import GenerateWorker as _RealGenerateWorker
@@ -882,6 +883,11 @@ class GenerateTab(QWidget):
for label, value in self.STATUS_FILTERS: for label, value in self.STATUS_FILTERS:
self.status_filter.addItem(label, value) self.status_filter.addItem(label, value)
self.refresh_button = QPushButton("刷新") self.refresh_button = QPushButton("刷新")
self.open_image_dir_button = QPushButton("打开图片文件夹")
self.open_image_dir_button.setObjectName("openImageDirButton")
self.open_image_dir_button.setToolTip(
"选中某行→打开该账号图片文件夹;未选中→打开当前批次文件夹;全部批次→打开图片根目录"
)
filter_layout = QHBoxLayout() filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("批次")) filter_layout.addWidget(QLabel("批次"))
@@ -893,6 +899,7 @@ class GenerateTab(QWidget):
filter_layout.addWidget(QLabel("状态")) filter_layout.addWidget(QLabel("状态"))
filter_layout.addWidget(self.status_filter, 1) filter_layout.addWidget(self.status_filter, 1)
filter_layout.addWidget(self.refresh_button) filter_layout.addWidget(self.refresh_button)
filter_layout.addWidget(self.open_image_dir_button)
self.summary_label = QLabel("任务 0 条") self.summary_label = QLabel("任务 0 条")
self.batch_progress_label = _build_batch_progress_overview("generateBatchProgressOverview") self.batch_progress_label = _build_batch_progress_overview("generateBatchProgressOverview")
@@ -973,6 +980,7 @@ class GenerateTab(QWidget):
self.item_filter.textChanged.connect(self.refresh_tasks) self.item_filter.textChanged.connect(self.refresh_tasks)
self.status_filter.currentIndexChanged.connect(self.refresh_tasks) self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
self.refresh_button.clicked.connect(self.refresh_tasks) self.refresh_button.clicked.connect(self.refresh_tasks)
self.open_image_dir_button.clicked.connect(self.open_image_directory)
self.save_title_button.clicked.connect(self.save_title_prompt) self.save_title_button.clicked.connect(self.save_title_prompt)
self.insert_old_title_button.clicked.connect(self.insert_old_title_placeholder) self.insert_old_title_button.clicked.connect(self.insert_old_title_placeholder)
self.cover_template_combo.currentIndexChanged.connect(self.load_selected_cover_template) self.cover_template_combo.currentIndexChanged.connect(self.load_selected_cover_template)
@@ -1824,6 +1832,84 @@ class GenerateTab(QWidget):
failed=payload.get("failed", 0), failed=payload.get("failed", 0),
) )
def open_image_directory(self):
target_dir, missing_message = self._resolve_image_directory_target()
if not target_dir:
message = missing_message or "无法定位图片文件夹"
QMessageBox.warning(self, "打开图片文件夹", message)
self._set_status(message, level="warning")
return
if not os.path.isdir(target_dir):
message = missing_message or "该批次/账号还没有图片"
QMessageBox.warning(self, "打开图片文件夹", message)
self._set_status(message, level="warning")
return
try:
opened_dir = file_manager.open_in_file_manager(target_dir)
except Exception as exc:
message = f"打开图片文件夹失败:{exc}"
QMessageBox.warning(self, "打开图片文件夹", message)
self._set_status(message, level="danger")
return
self._set_status(f"已打开图片文件夹:{opened_dir}", level="success")
def _resolve_image_directory_target(self):
image_root = appconfig.image_dir(self.config)
row = self._selected_row_for_image_directory()
if row is not None:
task = self.model.task_at(row)
if task is None:
return None, "没有选中可打开图片文件夹的任务"
existing_dir = self._existing_cover_parent_dir(task)
if existing_dir:
return existing_dir, "该账号还没有图片"
account = self._account_for_task(task)
if account is None:
return None, "选中任务没有匹配账号,无法定位图片文件夹"
target = os.path.dirname(
image_paths.task_image_path(
image_root,
task,
account=account,
suffix="new",
ext=".jpg",
)
)
return target, "该账号还没有图片"
selected_batch = self.batch_filter.currentData()
if selected_batch not in (None, "", "all"):
return os.path.abspath(os.path.join(image_root, str(selected_batch))), "该批次还没有图片"
return os.path.abspath(image_root), "图片根目录不存在,请先采集或生成图片"
def _selected_row_for_image_directory(self):
selection = self.task_table.selectionModel()
selected_rows = []
if selection is not None:
selected_rows = sorted({index.row() for index in selection.selectedRows()})
if not selected_rows:
return None
current = self.task_table.currentIndex()
if current.isValid() and current.row() in selected_rows:
return current.row()
return selected_rows[0]
def _existing_cover_parent_dir(self, task):
for attr in ("new_cover_path", "old_cover_path"):
value = str(getattr(task, attr, "") or "").strip()
if not value:
continue
path = os.path.abspath(value)
if os.path.isfile(path):
return os.path.dirname(path)
return None
def _account_for_task(self, task):
alias = str(getattr(task, "alias", "") or "").strip()
if not alias:
return None
return self.model.account_by_alias.get(alias)
def _selected_task(self): def _selected_task(self):
index = self.task_table.currentIndex() index = self.task_table.currentIndex()
if index.isValid(): if index.isValid():
+1
View File
@@ -98,6 +98,7 @@
- **封面提示词**:模板下拉(读 `data/prompts/cover/*.txt`)+ 图标工具栏(新建/保存/另存为/重命名/删除)+ 多行输入 + 「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。 - **封面提示词**:模板下拉(读 `data/prompts/cover/*.txt`)+ 图标工具栏(新建/保存/另存为/重命名/删除)+ 多行输入 + 「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。
- 变量:标题提示词本阶段只支持 `{旧标题}`;封面提示词支持 `{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。 - 变量:标题提示词本阶段只支持 `{旧标题}`;封面提示词支持 `{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。
- 右上:按导入批次 / 店铺 / 商品ID / 状态筛选任务;商品ID输入框按包含匹配 `item_id`,清空表示全部。 - 右上:按导入批次 / 店铺 / 商品ID / 状态筛选任务;商品ID输入框按包含匹配 `item_id`,清空表示全部。
- 筛选行提供「打开图片文件夹」按钮,用于只读打开本地图片目录:选中某行时打开该商品所在账号图片文件夹(优先打开已有新/旧封面文件的真实父目录,缺失时回退到规范账号目录);未选行且选择具体批次时打开该批次图片文件夹;未选行且为全部批次时打开图片根目录。目录不存在只中文提示,不自动创建目录,不修改任务状态。
- 右下:任务列表(店铺名、商品id、旧标题、新标题、标题状态、图片状态)+ AI生成运行日志;标题/图片状态由 `new_title`、`new_cover_path`、`stage/status` 和失败步骤推导,帮助用户区分“标题未生成 / 图片未生成 / 标题成功但图片失败”。商品ID列按原等分宽度约 50% 显示;标题状态和图片状态列在 T-554 基础上再缩到约 33%,缩出的宽度平均给旧标题和新标题。已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面、新封面和历史候选图。 - 右下:任务列表(店铺名、商品id、旧标题、新标题、标题状态、图片状态)+ AI生成运行日志;标题/图片状态由 `new_title`、`new_cover_path`、`stage/status` 和失败步骤推导,帮助用户区分“标题未生成 / 图片未生成 / 标题成功但图片失败”。商品ID列按原等分宽度约 50% 显示;标题状态和图片状态列在 T-554 基础上再缩到约 33%,缩出的宽度平均给旧标题和新标题。已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面、新封面和历史候选图。
- 底部**单个「开始生成」+「停止」**,并增加「生成内容」下拉:默认只生成标题,可选只生成封面或生成标题和封面;只生成封面要求任务已有新标题。标题/图片两条进度条右侧分别显示同宽用时标签(`生标题用时 N 秒` / `生图用时 N 秒`),运行中每秒递增,完成/停止后冻结;原图片进度条右侧的失败数和 cmhub 余额不再占用该位置。cmhub 模式会把用户设置的图片并发内部限制到最大 5,并用同样最大 5 的独立下载线程池拉取 `image_url`,不新增用户可见下载并发配置;运行日志显示用户设置并发和实际并发。下拉状态持久化到 `config.json` 的 `ai.generate_mode`,并继续写回旧兼容 `ai.generate_cover`。 - 底部**单个「开始生成」+「停止」**,并增加「生成内容」下拉:默认只生成标题,可选只生成封面或生成标题和封面;只生成封面要求任务已有新标题。标题/图片两条进度条右侧分别显示同宽用时标签(`生标题用时 N 秒` / `生图用时 N 秒`),运行中每秒递增,完成/停止后冻结;原图片进度条右侧的失败数和 cmhub 余额不再占用该位置。cmhub 模式会把用户设置的图片并发内部限制到最大 5,并用同样最大 5 的独立下载线程池拉取 `image_url`,不新增用户可见下载并发配置;运行日志显示用户设置并发和实际并发。下拉状态持久化到 `config.json` 的 `ai.generate_mode`,并继续写回旧兼容 `ai.generate_cover`。
- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **⑤ 设置**;②只暴露本轮生成标题/封面/图文的内容模式。⑤ 不新增“下载并发”控件;cmhub 图片下载并发由程序按实际生图并发自动计算,最大 5。 - 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **⑤ 设置**;②只暴露本轮生成标题/封面/图文的内容模式。⑤ 不新增“下载并发”控件;cmhub 图片下载并发由程序按实际生图并发自动计算,最大 5。
+11 -2
View File
@@ -3,7 +3,7 @@ id: T-581
title: ②AI生成增加「打开图片文件夹」按钮(级联:选中行账号图 / 批次 / 全部批次根) title: ②AI生成增加「打开图片文件夹」按钮(级联:选中行账号图 / 批次 / 全部批次根)
phase: 7 phase: 7
deps: [T-566] deps: [T-566]
status: TODO status: DONE
created: 2026-07-10 created: 2026-07-10
--- ---
@@ -79,4 +79,13 @@ created: 2026-07-10
## 执行记录 ## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) - 2026-07-10:已实现②「打开图片文件夹」按钮,位于 AI生成筛选行刷新按钮右侧;按钮按选中行、具体批次、全部批次三级规则定位本地图片目录,运行中不禁用,不修改任何任务/批次状态。
- 2026-07-10:新增 `app/gui/file_manager.py`,封装跨平台打开目录逻辑;Windows 使用 `os.startfile`,macOS/Linux 使用 `subprocess.Popen([...], shell=False)`,helper 不创建目录。
- 2026-07-10:选中行优先打开已有 `new_cover_path` / `old_cover_path` 的真实父目录;没有真实图片文件时回退到 `image_paths.task_image_path(..., "new")` 的账号目录;账号无法匹配时中文提示,不猜 slug。
- 2026-07-10:同步 `docs/routes.md` ②AI生成说明;补 `tests/test_gui.py` 覆盖选中行当前焦点优先、规范账号目录回退、具体批次目录、图片根目录、目录缺失不创建、账号未匹配提示、平台打开 helper 分支。
- 验证通过:
- `py -3.10 -m unittest tests.test_gui.GuiTests.test_generate_tab_opens_current_row_image_directory tests.test_gui.GuiTests.test_generate_tab_open_image_directory_falls_back_to_canonical_account_dir tests.test_gui.GuiTests.test_generate_tab_opens_batch_or_root_directory_without_selected_row tests.test_gui.GuiTests.test_generate_tab_open_image_directory_warns_without_creating_missing_dir tests.test_gui.GuiTests.test_generate_tab_open_image_directory_warns_when_selected_account_unmatched tests.test_gui.GuiTests.test_open_in_file_manager_uses_platform_file_manager`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `git diff --check`(仅输出当前工作区既有 CRLF 提示,无空白错误)
- 当前工作区直接运行 `py -3.10 -m unittest tests.test_gui` 失败 2 项,原因是本任务开始前已有未提交改动把默认封面提示词从 `papa1` 改成「默认」,导致两个旧断言仍期望 `papa1`;该提示词改名不属于 T-581,本次未处理、未提交。
+232 -1
View File
@@ -17,7 +17,7 @@ from app import accounts, ai, appconfig, db, image_paths, prompts, update_check
if gui.QT_IMPORT_ERROR is not None: if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装") raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtCore import QItemSelectionModel, QRect from PySide6.QtCore import QItemSelectionModel, QModelIndex, QRect
from PySide6.QtGui import QImage, QKeyEvent, QTextCursor from PySide6.QtGui import QImage, QKeyEvent, QTextCursor
from PySide6.QtWidgets import QApplication, QComboBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView from PySide6.QtWidgets import QApplication, QComboBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
@@ -39,6 +39,7 @@ from app.gui import (
TAB_TITLES, TAB_TITLES,
WriteBackWorker, WriteBackWorker,
) )
from app.gui import file_manager
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
from app.gui.main_window import _fit_and_center_window from app.gui.main_window import _fit_and_center_window
@@ -1810,6 +1811,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("开始生成", tab.generate_button.text()) self.assertEqual("开始生成", tab.generate_button.text())
self.assertEqual("停止", tab.stop_generate_button.text()) self.assertEqual("停止", tab.stop_generate_button.text())
self.assertEqual("重置生成结果", tab.reset_generate_button.text()) self.assertEqual("重置生成结果", tab.reset_generate_button.text())
self.assertEqual("打开图片文件夹", tab.open_image_dir_button.text())
self.assertEqual("openImageDirButton", tab.open_image_dir_button.objectName())
self.assertIn("当前批次文件夹", tab.open_image_dir_button.toolTip())
self.assertEqual( self.assertEqual(
["只生成标题", "只生成封面", "生成标题和封面"], ["只生成标题", "只生成封面", "生成标题和封面"],
[tab.generate_mode_combo.itemText(index) for index in range(tab.generate_mode_combo.count())], [tab.generate_mode_combo.itemText(index) for index in range(tab.generate_mode_combo.count())],
@@ -1975,6 +1979,233 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
def test_generate_tab_opens_current_row_image_directory(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
accounts.create_account("副店", "alias-b", debug_port=9223, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
},
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 3,
"account_name": "Excel副店",
"alias": "alias-b",
"item_id": "51100639511",
},
],
path=cfg["db_path"],
)
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
first_dir = os.path.join(temp_dir, "legacy-a")
second_dir = os.path.join(temp_dir, "legacy-b")
first_cover = self.write_test_image(os.path.join(first_dir, "old.jpg"))
second_cover = self.write_test_image(os.path.join(second_dir, "new.jpg"))
db.set_collected(tasks[0].id, "旧标题A", first_cover, path=cfg["db_path"])
db.set_collected(tasks[1].id, "旧标题B", "missing-old.jpg", path=cfg["db_path"])
db.set_generated(tasks[1].id, "新标题B", second_cover, path=cfg["db_path"])
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
selection = tab.task_table.selectionModel()
selection.select(tab.model.index(0, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows)
selection.select(tab.model.index(1, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows)
tab.task_table.setCurrentIndex(tab.model.index(1, 0))
with mock.patch(
"app.gui.tabs.generate.file_manager.open_in_file_manager",
return_value=second_dir,
) as open_dir:
tab.open_image_directory()
open_dir.assert_called_once_with(os.path.abspath(second_dir))
self.assert_removed(temp_dir)
def test_generate_tab_open_image_directory_falls_back_to_canonical_account_dir(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
},
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
canonical_dir = os.path.dirname(
image_paths.task_image_path(cfg["image_dir"], task, account=account, suffix="new")
)
os.makedirs(canonical_dir, exist_ok=True)
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
tab.task_table.selectRow(0)
tab.task_table.setCurrentIndex(tab.model.index(0, 0))
with mock.patch(
"app.gui.tabs.generate.file_manager.open_in_file_manager",
return_value=canonical_dir,
) as open_dir:
tab.open_image_directory()
open_dir.assert_called_once_with(os.path.abspath(canonical_dir))
self.assert_removed(temp_dir)
def test_generate_tab_opens_batch_or_root_directory_without_selected_row(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
image_root = os.path.abspath(cfg["image_dir"])
batch_dir = os.path.join(image_root, batch_id)
os.makedirs(batch_dir, exist_ok=True)
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(batch_id))
tab.task_table.clearSelection()
tab.task_table.setCurrentIndex(QModelIndex())
with mock.patch(
"app.gui.tabs.generate.file_manager.open_in_file_manager",
return_value=batch_dir,
) as open_dir:
tab.open_image_directory()
open_dir.assert_called_once_with(os.path.abspath(batch_dir))
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(None))
tab.task_table.clearSelection()
tab.task_table.setCurrentIndex(QModelIndex())
with mock.patch(
"app.gui.tabs.generate.file_manager.open_in_file_manager",
return_value=image_root,
) as open_dir:
tab.open_image_directory()
open_dir.assert_called_once_with(image_root)
self.assert_removed(temp_dir)
def test_generate_tab_open_image_directory_warns_without_creating_missing_dir(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
batch_dir = os.path.join(os.path.abspath(cfg["image_dir"]), batch_id)
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(batch_id))
tab.task_table.clearSelection()
tab.task_table.setCurrentIndex(QModelIndex())
with mock.patch("app.gui.tabs.generate.QMessageBox.warning") as warning, mock.patch(
"app.gui.tabs.generate.file_manager.open_in_file_manager"
) as open_dir:
tab.open_image_directory()
warning.assert_called_once()
self.assertIn("该批次还没有图片", warning.call_args[0][2])
open_dir.assert_not_called()
self.assertFalse(os.path.exists(batch_dir))
self.assert_removed(temp_dir)
def test_generate_tab_open_image_directory_warns_when_selected_account_unmatched(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "missing-alias",
"item_id": "51100639510",
},
],
path=cfg["db_path"],
)
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
tab.task_table.selectRow(0)
tab.task_table.setCurrentIndex(tab.model.index(0, 0))
with mock.patch("app.gui.tabs.generate.QMessageBox.warning") as warning, mock.patch(
"app.gui.tabs.generate.file_manager.open_in_file_manager"
) as open_dir:
tab.open_image_directory()
warning.assert_called_once()
self.assertIn("选中任务没有匹配账号", warning.call_args[0][2])
open_dir.assert_not_called()
self.assert_removed(temp_dir)
def test_open_in_file_manager_uses_platform_file_manager(self):
with self.make_temp_dir() as temp_dir:
target = os.path.join(temp_dir, "images")
os.makedirs(target)
with mock.patch("app.gui.file_manager.sys.platform", "win32"), mock.patch(
"app.gui.file_manager.os.startfile",
create=True,
) as startfile:
self.assertEqual(os.path.abspath(target), file_manager.open_in_file_manager(target))
startfile.assert_called_once_with(os.path.abspath(target))
with mock.patch("app.gui.file_manager.sys.platform", "darwin"), mock.patch(
"app.gui.file_manager.subprocess.Popen"
) as popen:
self.assertEqual(os.path.abspath(target), file_manager.open_in_file_manager(target))
popen.assert_called_once_with(
["open", os.path.abspath(target)],
stdout=file_manager.subprocess.DEVNULL,
stderr=file_manager.subprocess.DEVNULL,
shell=False,
)
with mock.patch("app.gui.file_manager.sys.platform", "linux"), mock.patch(
"app.gui.file_manager.subprocess.Popen"
) as popen:
self.assertEqual(os.path.abspath(target), file_manager.open_in_file_manager(target))
popen.assert_called_once_with(
["xdg-open", os.path.abspath(target)],
stdout=file_manager.subprocess.DEVNULL,
stderr=file_manager.subprocess.DEVNULL,
shell=False,
)
with self.assertRaises(FileNotFoundError):
file_manager.open_in_file_manager(os.path.join(temp_dir, "missing"))
self.assert_removed(temp_dir)
def test_generate_elapsed_timer_tracks_title_then_cover(self): def test_generate_elapsed_timer_tracks_title_then_cover(self):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir:
tab = GenerateTab(config=self.make_config(temp_dir)) tab = GenerateTab(config=self.make_config(temp_dir))