Skip to content

Commit b1b9ad6

Browse files
takemi-ohamaclaude
andcommitted
refactor(tui): 第2弾 — 選択ヘルパの残ラダー解消とカテゴリ定義の SSoT 化
- flow.back_as_cancel 新設: 選択ヘルパに残っていた None/MENU_BACK→ARG_CANCEL 変換ラダー 4 箇所を 1 行化 (env/_select_project, project/_select_build_image, snapshot/_select_snapshot_name×2, plugin/_select_name) - app.py: TOP_CATEGORIES / _LABELS / _route の if-elif で三重持ちだった カテゴリ定義を _CATEGORIES 単一 SSoT から導出。routing は module.run の 遅延解決 dict に (monkeypatch 互換) - actions_plugin: registry 名取得 2 関数を _registry_names(lister) に統合、 空一覧の案内+中止を _select_name へ吸収し wrapper 2 関数を薄い委譲に - actions_env / actions_project: 引数なし操作 (sync/edit/up/rebuild) を dict 直書き lambda に統一 (plugin/snapshot と同形) 公開契約・プロンプト文言・順序は不変。731 passed / ruff クリーン Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d9b6340 commit b1b9ad6

6 files changed

Lines changed: 68 additions & 95 deletions

File tree

lib/devbase/tui/actions_env.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,9 @@ def _select_project(devbase_root: Path):
100100
choices = [(entry, i) for i, entry in enumerate(entries)]
101101
idx = menu.select(f"対象プロジェクトを選択 {menu.HINT_SEARCH}:",
102102
choices, back=True, search=True)
103-
if idx is None:
104-
return None # Ctrl-C → 全体中止 (ナビ規約)
105-
if idx is menu.MENU_BACK:
106-
return _ARG_CANCEL # Esc → サブメニューを再表示
107-
return rows[idx]["name"]
103+
if isinstance(idx, int):
104+
return rows[idx]["name"]
105+
return flow.back_as_cancel(idx) # None=Ctrl-C / MENU_BACK=Esc → 再表示
108106

109107

110108
def _run_in_project(devbase_root: Path, project_name: str, fn):
@@ -213,17 +211,6 @@ def _select_scoped_project(devbase_root: Path, message: str, choices):
213211
# 各操作の引数収集 + dispatch (plan 2.3 契約)
214212
# ---------------------------------------------------------------------------
215213

216-
def _op_sync(devbase_root: Path):
217-
# 引数なし。ソースファイルから認証情報を再同期する。
218-
return _dispatch(devbase_root, "sync")
219-
220-
221-
def _op_edit(devbase_root: Path):
222-
# 引数なし。$DEVBASE_ROOT/.env を $EDITOR で開く (グローバル操作。
223-
# plan 3.3 は CWD スコープとするが実装はグローバルのため chdir しない)。
224-
return _dispatch(devbase_root, "edit")
225-
226-
227214
def _op_init(devbase_root: Path):
228215
# 既存設定がある場合は --reset でやり直し (既存はバックアップされる)。
229216
reset = flow.need(menu.confirm(
@@ -331,8 +318,11 @@ def _op_import(devbase_root: Path):
331318

332319

333320
_OP_HANDLERS = {
334-
"sync": _op_sync,
335-
"edit": _op_edit,
321+
# sync は引数なしで即実行 (ソースファイルから認証情報を再同期する)。
322+
# edit も引数なし。$DEVBASE_ROOT/.env を $EDITOR で開くグローバル操作のため
323+
# chdir しない (plan 3.3 は CWD スコープとするが実装を正とする)。
324+
"sync": lambda root: _dispatch(root, "sync"),
325+
"edit": lambda root: _dispatch(root, "edit"),
336326
"init": _op_init,
337327
"list": _op_list,
338328
"set": _op_set,

lib/devbase/tui/actions_plugin.py

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -66,71 +66,59 @@ def _dispatch(devbase_root: Path, subcommand: str, **attrs) -> int:
6666
# 名前選択 (registry から一覧を取得して選ばせる)
6767
# ---------------------------------------------------------------------------
6868

69-
def _installed_plugin_names(devbase_root: Path) -> list[str]:
70-
"""導入済み plugin 名の一覧を registry (plugins.yml) から取得する。"""
71-
from devbase.plugin.registry import PluginRegistry
72-
73-
try:
74-
return [p.name for p in PluginRegistry(Path(devbase_root)).list_installed()]
75-
except DevbaseError as e:
76-
logger.error("%s", e)
77-
return []
78-
69+
def _registry_names(devbase_root: Path, lister: str) -> list[str]:
70+
"""registry (plugins.yml) から名前一覧を取得する。
7971
80-
def _repository_names(devbase_root: Path) -> list[str]:
81-
"""登録済みリポジトリ名の一覧を registry (plugins.yml) から取得する。"""
72+
``lister`` は ``PluginRegistry`` の一覧メソッド名 (``list_installed`` /
73+
``list_repositories``)。取得に失敗したら案内して空リストを返す。
74+
"""
8275
from devbase.plugin.registry import PluginRegistry
8376

8477
try:
85-
return [r.name for r in PluginRegistry(Path(devbase_root)).list_repositories()]
78+
registry = PluginRegistry(Path(devbase_root))
79+
return [item.name for item in getattr(registry, lister)()]
8680
except DevbaseError as e:
8781
logger.error("%s", e)
8882
return []
8983

9084

91-
def _select_name(message: str, names: list[str], *, all_label: str | None = None):
92-
"""名前一覧から 1 件選ばせる共通ヘルパ。
85+
def _select_name(message: str, names: list[str], *,
86+
all_label: str | None = None, empty_hint: str = "対象がありません。"):
87+
"""名前一覧から 1 件選ばせる共通ヘルパ。対象が無ければ案内して ``_ARG_CANCEL``。
9388
9489
``all_label`` 指定時は「全対象」(value="") を先頭に置く。選択メニューの ``None``
9590
(Ctrl-C → 全体中止) と衝突させないため空文字を番兵にし、``None`` への変換は
9691
呼び出し側で行う (_select_build_image と同じ流儀)。
9792
9893
戻り値: 名前 (``str``) / ``""`` (all_label 選択 = 全対象。呼び出し側で ``None``
9994
へ変換) / ``None`` (Ctrl-C → 全体中止を呼び出し元へ伝搬) / ``_ARG_CANCEL``
100-
(Esc・← → サブメニューへ戻る)。
95+
(Esc・← → サブメニューへ戻る、または対象が 1 件もない)。
10196
"""
102-
choices: list[tuple[str, str]] = []
103-
if all_label is not None:
104-
choices.append((all_label, ""))
97+
if not names:
98+
logger.info("%s", empty_hint)
99+
return _ARG_CANCEL
100+
choices = ([(all_label, "")] if all_label is not None else [])
105101
choices += [(n, n) for n in names]
106-
sel = menu.select(f"{message} {menu.HINT_BACK}:", choices, back=True, search=False)
107-
if sel is None:
108-
return None # Ctrl-C → 全体中止 (ナビ規約)
109-
if sel is menu.MENU_BACK:
110-
return _ARG_CANCEL # Esc/← → サブメニューを再表示
111-
return sel # "" = 全対象 (呼び出し側で None へ変換)
102+
return flow.back_as_cancel(menu.select(
103+
f"{message} {menu.HINT_BACK}:", choices, back=True, search=False))
112104

113105

114106
def _select_installed_plugin(devbase_root: Path, message: str, *,
115107
all_label: str | None = None):
116108
"""導入済み plugin から 1 件選ばせる。対象が無ければ案内して ``_ARG_CANCEL``。"""
117-
names = _installed_plugin_names(devbase_root)
118-
if not names:
119-
logger.info("導入済みの plugin がありません。"
120-
"`plugin install` で導入してください。")
121-
return _ARG_CANCEL
122-
return _select_name(message, names, all_label=all_label)
109+
return _select_name(
110+
message, _registry_names(devbase_root, "list_installed"),
111+
all_label=all_label,
112+
empty_hint="導入済みの plugin がありません。`plugin install` で導入してください。")
123113

124114

125115
def _select_repository(devbase_root: Path, message: str, *,
126116
all_label: str | None = None):
127117
"""登録済みリポジトリから 1 件選ばせる。対象が無ければ案内して ``_ARG_CANCEL``。"""
128-
names = _repository_names(devbase_root)
129-
if not names:
130-
logger.info("登録済みのリポジトリがありません。"
131-
"`plugin repo add` で登録してください。")
132-
return _ARG_CANCEL
133-
return _select_name(message, names, all_label=all_label)
118+
return _select_name(
119+
message, _registry_names(devbase_root, "list_repositories"),
120+
all_label=all_label,
121+
empty_hint="登録済みのリポジトリがありません。`plugin repo add` で登録してください。")
134122

135123

136124
# ---------------------------------------------------------------------------

lib/devbase/tui/actions_project.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,9 @@ def _select_build_image(devbase_root: Path):
8484
# value="" を「compose.yml 全体」に割り当て、選択メニューの None (Ctrl-C =
8585
# 全体中止) と衝突させない。呼び出し側で空文字 → None へ変換する。
8686
choices = [("compose.yml 全体をビルド", "")] + [(img, img) for img in images]
87-
sel = menu.select(f"ビルドするイメージを選択 {menu.HINT_BACK}:",
88-
choices, back=True, search=False)
89-
if sel is None:
90-
return None # Ctrl-C → 全体中止 (ナビ規約)
91-
if sel is menu.MENU_BACK:
92-
return _ARG_CANCEL # Esc/← → サブメニューを再表示
93-
return sel # "" = compose 全体 (呼び出し側で None へ変換)
87+
return flow.back_as_cancel(menu.select(
88+
f"ビルドするイメージを選択 {menu.HINT_BACK}:",
89+
choices, back=True, search=False))
9490

9591

9692
# ---------------------------------------------------------------------------
@@ -133,6 +129,10 @@ def _op_build(devbase_root: Path, name: str):
133129

134130

135131
_OP_HANDLERS = {
132+
# up/rebuild は引数なしで即実行。up は scale 属性を参照する (常に None。
133+
# 他コマンドは無視する)。
134+
"up": lambda root, name: dispatch_lifecycle("up", name, scale=None),
135+
"rebuild": lambda root, name: dispatch_lifecycle("rebuild", name, scale=None),
136136
"down": _op_down,
137137
"login": _op_login,
138138
"ps": _op_ps,
@@ -149,10 +149,6 @@ def _run_operation(devbase_root: Path, name: str, op: str):
149149
戻り値: dispatch の rc (``int``) / ``_ARG_CANCEL`` (Esc・確認拒否で引数収集を
150150
中止 = サブメニューへ戻る) / ``None`` (選択・入力中の Ctrl-C → 全体中止)。
151151
"""
152-
if op in ("up", "rebuild"):
153-
# 引数なしで即実行。up は scale 属性を参照する (常に None。他コマンドは無視)。
154-
return dispatch_lifecycle(op, name, scale=None)
155-
156152
handler = _OP_HANDLERS.get(op)
157153
if handler is None:
158154
# 到達しない (メニュー値は _RUNNING_OPS に限定される)。保守的に no-op。

lib/devbase/tui/actions_snapshot.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,7 @@ def _select_snapshot_name(devbase_root: Path, message: str):
7474

7575
if snapshots is None:
7676
# 一覧が取れない環境では名前を直接入力させる。
77-
name = menu.text(message, allow_empty=False)
78-
if name is None:
79-
return None # Ctrl-C → 全体中止 (ナビ規約)
80-
if name is menu.MENU_BACK:
81-
return _ARG_CANCEL # Esc → 操作メニューを再表示
82-
return name
77+
return flow.back_as_cancel(menu.text(message, allow_empty=False))
8378

8479
if not snapshots:
8580
logger.info("スナップショットがありません。先に作成 (create) してください。")
@@ -93,13 +88,8 @@ def _select_snapshot_name(devbase_root: Path, message: str):
9388
s.get("name"))
9489
for s in snapshots
9590
]
96-
sel = menu.select(f"{message} {menu.HINT_SEARCH}:", choices,
97-
back=True, search=True)
98-
if sel is None:
99-
return None # Ctrl-C → 全体中止 (ナビ規約)
100-
if sel is menu.MENU_BACK:
101-
return _ARG_CANCEL # Esc → 操作メニューを再表示
102-
return sel
91+
return flow.back_as_cancel(menu.select(
92+
f"{message} {menu.HINT_SEARCH}:", choices, back=True, search=True))
10393

10494

10595
def _optional_point(message: str):

lib/devbase/tui/app.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,20 @@
3535

3636
logger = get_logger(__name__)
3737

38-
# プロジェクト一覧の末尾に並べるカテゴリ (表示順)。``(key, label)`` で保持し、
38+
# プロジェクト一覧の末尾に並べるカテゴリの SSoT (表示順 / ラベル / 実装モジュール)。
3939
# 一覧メニューには ``label (key)`` 形式で表示する (key 入力での絞り込みも効く)。
40-
TOP_CATEGORIES: list[tuple[str, str]] = [
41-
("env", "環境変数"),
42-
("plugin", "プラグイン"),
43-
("snapshot", "スナップショット"),
44-
("status", "ステータス"),
40+
# モジュール参照を保持し ``run`` の解決を呼び出し時まで遅らせる
41+
# (テストが ``actions_*.run`` を monkeypatch できるようにするため)。
42+
_CATEGORIES: list[tuple[str, str, object]] = [
43+
("env", "環境変数", actions_env),
44+
("plugin", "プラグイン", actions_plugin),
45+
("snapshot", "スナップショット", actions_snapshot),
46+
("status", "ステータス", actions_status),
4547
]
4648

49+
TOP_CATEGORIES: list[tuple[str, str]] = [(k, label) for k, label, _ in _CATEGORIES]
4750
_LABELS = dict(TOP_CATEGORIES)
51+
_CATEGORY_MODULES = {k: mod for k, _, mod in _CATEGORIES}
4852

4953

5054
def _route(category: str, devbase_root: Path):
@@ -55,16 +59,11 @@ def _route(category: str, devbase_root: Path):
5559
- 操作なしで一覧へ戻るときは ``menu.MENU_BACK``
5660
- Ctrl-C 全体中止のときは ``None``
5761
"""
58-
if category == "env":
59-
return actions_env.run(devbase_root)
60-
if category == "plugin":
61-
return actions_plugin.run(devbase_root)
62-
if category == "snapshot":
63-
return actions_snapshot.run(devbase_root)
64-
if category == "status":
65-
return actions_status.run(devbase_root)
66-
logger.error("未知のカテゴリです: %s", _LABELS.get(category, category))
67-
return menu.MENU_BACK
62+
module = _CATEGORY_MODULES.get(category)
63+
if module is None:
64+
logger.error("未知のカテゴリです: %s", _LABELS.get(category, category))
65+
return menu.MENU_BACK
66+
return module.run(devbase_root)
6867

6968

7069
def _select_top(rows: list[dict]):

lib/devbase/tui/flow.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ def need(value):
5656
return value
5757

5858

59+
def back_as_cancel(value):
60+
"""``MENU_BACK`` を ``ARG_CANCEL`` へ読み替える (選択ヘルパの番兵契約用)。
61+
62+
actions_* の選択ヘルパは「Esc = 呼び出し元メニューの再表示」を ``ARG_CANCEL``
63+
で表現する契約を持つ (メニューループ自身の ``MENU_BACK`` = 1 つ上の階層へ、
64+
と区別するため)。実値と ``None`` (Ctrl-C) はそのまま通す。
65+
"""
66+
return ARG_CANCEL if value is menu.MENU_BACK else value
67+
68+
5969
def need_optional(value):
6070
"""``optional_int`` の番兵 (``ABORT`` / ``ARG_CANCEL``) を例外へ変換する。
6171

0 commit comments

Comments
 (0)