Skip to content

Commit d98b67e

Browse files
takemi-ohamaclaude
andcommitted
refactor: 第3弾 — 100行超関数の全解消と重複ロジックの SSoT 化
可読性・Pythonic 化・関数行数適正化・メンテナンス性向上を目的とした 横断リファクタリング (動作変更なし、731 tests green / ruff clean)。 - 関数分割: migrate / _install_from_repo / install_plugin / add_repository / refresh_repository / _add_env_parser / generate_scaled_compose / _ensure_images / cmd_up / sync_projects → 100 行超の関数 8 件をゼロに - SSoT 化: RegistryInfo.available_plugins() 新設 (AvailablePlugin 変換 4 箇所を一元化)、env パース共通化 (_parse_env_assignment)、 @ref 拒否エラー・Available plugins 表示・git 実行・[name] positional 定義の共通ヘルパ化、レコード更新を dataclasses.replace に統一 - Pythonic 化: 自前 _deep_copy → copy.deepcopy、線形探索 → next()、 内包表記 / setdefault / startswith タプル化、_dispatch の if ラダー → 宣言的テーブル + importlib - 死蔵コード除去: 到達不能 except、未使用 import / 変数 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b1b9ad6 commit d98b67e

10 files changed

Lines changed: 798 additions & 752 deletions

File tree

lib/devbase/cli.py

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import argparse
55
import os
66
import sys
7+
from importlib import import_module
78
from pathlib import Path
89

910
from devbase.errors import DevbaseError
@@ -86,6 +87,14 @@ def _require_devbase_root() -> Path:
8687
return Path(root)
8788

8889

90+
def _add_name_arg(parser):
91+
"""省略可能な `[name]` positional (プロジェクト名) を登録する。
92+
93+
`project <sub> [name]` とトップレベルショートカットで同一定義を共有する。
94+
"""
95+
parser.add_argument('name', nargs='?', default=None, help='Project name')
96+
97+
8998
def _add_login_subparser(sub):
9099
"""`login` サブコマンドを登録する (project / container 共通)。
91100
@@ -156,27 +165,24 @@ def _add_project_parser(subparsers):
156165
pj_parser = subparsers.add_parser('project', help='Manage projects (CWD-independent)')
157166
pj_sub = pj_parser.add_subparsers(dest='subcommand')
158167

159-
pj_up = pj_sub.add_parser('up', help='Start containers')
160-
pj_up.add_argument('name', nargs='?', default=None, help='Project name')
161-
162-
pj_down = pj_sub.add_parser('down', help='Stop and remove containers')
163-
pj_down.add_argument('name', nargs='?', default=None, help='Project name')
168+
_add_name_arg(pj_sub.add_parser('up', help='Start containers'))
169+
_add_name_arg(pj_sub.add_parser('down', help='Stop and remove containers'))
164170

165171
_add_login_subparser(pj_sub)
166172

167173
pj_ps = pj_sub.add_parser('ps', help='Show container status')
168-
pj_ps.add_argument('name', nargs='?', default=None, help='Project name')
174+
_add_name_arg(pj_ps)
169175
pj_ps.add_argument('--all', '-a', action='store_true', help='Show all containers')
170176

171177
pj_logs = pj_sub.add_parser('logs', help='Show container logs')
172-
pj_logs.add_argument('name', nargs='?', default=None, help='Project name')
178+
_add_name_arg(pj_logs)
173179
pj_logs.add_argument('--follow', '-f', action='store_true', help='Follow log output')
174180
pj_logs.add_argument('--tail', type=int, default=None, help='Number of lines')
175181

176182
# NOTE: `[name]` optional + `new_scale` 必須 int の順。値が 1 個なら new_scale に、
177183
# 2 個なら (name, new_scale) に割り当てられ曖昧にならない (tests/cli 参照)。
178184
pj_scale = pj_sub.add_parser('scale', help='Scale containers online')
179-
pj_scale.add_argument('name', nargs='?', default=None, help='Project name')
185+
_add_name_arg(pj_scale)
180186
pj_scale.add_argument('new_scale', type=int, help='New number of containers')
181187

182188
_add_build_subparser(pj_sub)
@@ -185,9 +191,8 @@ def _add_project_parser(subparsers):
185191
# 省略可能な `[name]` を取り、name 指定時は _dispatch_lifecycle が chdir してから
186192
# 実行する。wrapper の _PROJECT_NAME_SUBCOMMANDS / _NAME_RESOLVABLE_SHORTCUTS にも
187193
# 追加すること。
188-
pj_rebuild = pj_sub.add_parser(
189-
'rebuild', help='Rebuild images without cache (docker compose build --no-cache)')
190-
pj_rebuild.add_argument('name', nargs='?', default=None, help='Project name')
194+
_add_name_arg(pj_sub.add_parser(
195+
'rebuild', help='Rebuild images without cache (docker compose build --no-cache)'))
191196

192197
# `list` は lifecycle ではなく一覧表示 (commands/project.py)。name positional は
193198
# 取らない (wrapper の _PROJECT_NAME_SUBCOMMANDS にも含めない)。
@@ -243,6 +248,12 @@ def _add_env_parser(subparsers):
243248
env_sub.add_parser('edit', help='Open .env in editor')
244249
env_sub.add_parser('project', help='Setup project-specific variables')
245250

251+
_add_env_export_parser(env_sub)
252+
_add_env_import_parser(env_sub)
253+
254+
255+
def _add_env_export_parser(env_sub):
256+
"""`env export` サブコマンドを登録する。"""
246257
env_export = env_sub.add_parser(
247258
'export',
248259
help='Export .env files as an encrypted bundle (age)',
@@ -278,6 +289,9 @@ def _add_env_parser(subparsers):
278289
'(per-object SSE is always applied regardless of this flag). '
279290
'Has no effect for non-s3:// destinations.')
280291

292+
293+
def _add_env_import_parser(env_sub):
294+
"""`env import` サブコマンドを登録する。"""
281295
env_import = env_sub.add_parser(
282296
'import',
283297
help='Import .env files from a bundle (age-encrypted or plaintext tar.gz)',
@@ -421,29 +435,24 @@ def _add_shortcuts(subparsers):
421435
注記参照): bin/devbase が build を shell 実装 (cmd_build) に委譲するため、
422436
Python 側でトップレベル build を広告すると実経路と乖離する。
423437
"""
424-
login_sc = subparsers.add_parser('login', help='Login to container')
425-
login_sc.add_argument('index', nargs='?', default='1', help='Container index')
438+
_add_login_subparser(subparsers)
426439

427440
ps_sc = subparsers.add_parser('ps', help='Show container status')
428-
ps_sc.add_argument('name', nargs='?', default=None, help='Project name')
441+
_add_name_arg(ps_sc)
429442
ps_sc.add_argument('--all', '-a', action='store_true', help='Show all containers')
430443

431-
up_sc = subparsers.add_parser('up', help='Start containers')
432-
up_sc.add_argument('name', nargs='?', default=None, help='Project name')
433-
434-
down_sc = subparsers.add_parser('down', help='Stop and remove containers')
435-
down_sc.add_argument('name', nargs='?', default=None, help='Project name')
444+
_add_name_arg(subparsers.add_parser('up', help='Start containers'))
445+
_add_name_arg(subparsers.add_parser('down', help='Stop and remove containers'))
436446

437447
# `[name]` optional + `new_scale` 必須 int の順 (project scale と同じ規則)。
438448
scale_sc = subparsers.add_parser('scale', help='Scale containers online')
439-
scale_sc.add_argument('name', nargs='?', default=None, help='Project name')
449+
_add_name_arg(scale_sc)
440450
scale_sc.add_argument('new_scale', type=int, help='New number of containers')
441451

442452
# `rebuild` は project rebuild のトップレベルシノニム (Python 実装のため build と
443453
# 異なりショートカット可)。up/down と同じく `[name]` を受け付ける。
444-
rebuild_sc = subparsers.add_parser(
445-
'rebuild', help='Rebuild images without cache (docker compose build --no-cache)')
446-
rebuild_sc.add_argument('name', nargs='?', default=None, help='Project name')
454+
_add_name_arg(subparsers.add_parser(
455+
'rebuild', help='Rebuild images without cache (docker compose build --no-cache)'))
447456

448457
# `list` は `project list` のトップレベルシノニム。lifecycle ではなく一覧表示
449458
# のため SHORTCUTS (project lifecycle へ写像) ではなく _dispatch で個別に
@@ -569,6 +578,17 @@ def main():
569578
return 1
570579

571580

581+
# DEVBASE_ROOT 必須コマンドの定義: cmd -> (module, function, args を渡すか)。
582+
# 起動コストを抑えるため import は dispatch 時に遅延させる (従来の関数内 import と同等)。
583+
_ROOT_COMMANDS = {
584+
'init': ('devbase.commands.init', 'cmd_init', False),
585+
'status': ('devbase.commands.status', 'cmd_status', False),
586+
'env': ('devbase.commands.env', 'cmd_env', True),
587+
'plugin': ('devbase.commands.plugin', 'cmd_plugin', True),
588+
'snapshot': ('devbase.commands.snapshot', 'cmd_snapshot', True),
589+
}
590+
591+
572592
def _dispatch(cmd, args):
573593
"""Dispatch command to handler."""
574594
# Resolve group aliases
@@ -604,30 +624,14 @@ def _dispatch(cmd, args):
604624
return cmd_container(args)
605625

606626
# --- Commands requiring DEVBASE_ROOT ---
627+
spec = _ROOT_COMMANDS.get(cmd)
628+
if spec is None:
629+
logger.error("Unknown command: '%s'", cmd)
630+
return 1
631+
module_name, func_name, takes_args = spec
632+
func = getattr(import_module(module_name), func_name)
607633
devbase_root = _require_devbase_root()
608-
609-
if cmd == 'init':
610-
from devbase.commands.init import cmd_init
611-
return cmd_init(devbase_root)
612-
613-
if cmd == 'status':
614-
from devbase.commands.status import cmd_status
615-
return cmd_status(devbase_root)
616-
617-
if cmd == 'env':
618-
from devbase.commands.env import cmd_env
619-
return cmd_env(devbase_root, args)
620-
621-
if cmd == 'plugin':
622-
from devbase.commands.plugin import cmd_plugin
623-
return cmd_plugin(devbase_root, args)
624-
625-
if cmd == 'snapshot':
626-
from devbase.commands.snapshot import cmd_snapshot
627-
return cmd_snapshot(devbase_root, args)
628-
629-
logger.error("Unknown command: '%s'", cmd)
630-
return 1
634+
return func(devbase_root, args) if takes_args else func(devbase_root)
631635

632636

633637
if __name__ == '__main__':

0 commit comments

Comments
 (0)