• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kimata / py-project / 21103641783

18 Jan 2026 01:03AM UTC coverage: 83.917% (+2.1%) from 81.864%
21103641783

push

github

kimata
refactor: Enum/dataclass の活用と型整備

- ApplyStatus enum を導入(status 文字列の置き換え)
- GitCommitFile, ValidationResult, MyPyLibDependencyMatch を dataclass 化
- ProgressType 型エイリアスを追加
- NullProgressManager を活用した Null Object パターンの適用
- TimeoutError を dep_updater の例外処理に追加
- テストを新しいデータ構造に対応

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

87 of 94 new or added lines in 9 files covered. (92.55%)

80 existing lines in 4 files now uncovered.

1127 of 1343 relevant lines covered (83.92%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.07
/src/py_project/cli.py
1
#!/usr/bin/env python3
2
"""
3
複数の Python プロジェクトに標準的な設定を一括適用します。
4

5
Usage:
6
  py-project [-c CONFIG] [-a] [-p PROJECT]... [-t TYPE]... [-d] [-b] [-v] [options]
7
  py-project [-c CONFIG] --validate
8
  py-project [-c CONFIG] --list-projects
9
  py-project --list-configs
10
  py-project [-c CONFIG] --update-deps [-a] [-d] [--include-projects] [--include-config] [-p PROJECT]...
11

12
Options:
13
  -c CONFIG, --config CONFIG    CONFIG を設定ファイルとして読み込みます。[default: config.yaml]
14
  -a, --apply                   実際に変更を適用します。(指定しないとドライラン)
15
  -p PROJECT, --project PROJECT 対象プロジェクトを限定します。(複数指定可)
16
  -t TYPE, --type TYPE          対象設定タイプを限定します。(複数指定可)
17
  -d, --diff                    差分を詳細表示します。
18
  -b, --backup                  適用前にバックアップを作成します。
19
  -v, --verbose                 詳細ログを出力します。
20
  --no-sync                     pyproject.toml 更新後に uv sync を実行しません。
21
  --git-commit                  更新したファイルを git add & commit します。
22
  --git-push                    更新したファイルを git add & commit & push します。
23
  --validate                    設定ファイルの検証のみ行います。
24
  --list-projects               プロジェクト一覧を表示します。
25
  --list-configs                設定タイプ一覧を表示します。
26
  --update-deps                 依存関係を最新バージョンに更新します。
27
  --include-projects            プロジェクトの dependencies も更新対象にします。
28
  --include-config              config.yaml の extra_dev_deps も更新対象にします。
29
"""
30

31
import logging
1✔
32
import pathlib
1✔
33
import sys
1✔
34

35
import docopt
1✔
36
import my_lib.config
1✔
37
import my_lib.cui_progress
1✔
38
import my_lib.logger
1✔
39
import rich.console
1✔
40
import rich.table
1✔
41

42
import py_project.applier
1✔
43
import py_project.config
1✔
44
import py_project.dep_updater
1✔
45
import py_project.handlers
1✔
46

47
_SCHEMA_CONFIG = "schema/config.schema"
1✔
48

49

50
def execute(
1✔
51
    config: py_project.config.Config,
52
    options: py_project.config.ApplyOptions,
53
    projects: py_project.applier.TargetList | None = None,
54
    config_types: py_project.applier.TargetList | None = None,
55
) -> int:
56
    """設定を適用する
57

58
    Args:
59
        config: アプリケーション設定
60
        options: 適用オプション
61
        projects: 対象プロジェクト名のリスト
62
        config_types: 対象設定タイプのリスト
63

64
    Returns:
65
        エラー数(0 なら成功)
66

67
    """
68
    console = rich.console.Console()
1✔
69
    progress = my_lib.cui_progress.ProgressManager(
1✔
70
        console=console,
71
        title=" 🐍 py-project ",
72
    )
73

74
    try:
1✔
75
        progress.start()
1✔
76

77
        summary = py_project.applier.apply_configs(
1✔
78
            config=config,
79
            options=options,
80
            projects=projects,
81
            config_types=config_types,
82
            console=console,
83
            progress=progress,
84
        )
85

86
        return summary.errors
1✔
87
    finally:
88
        progress.stop()
1✔
89

90

91
def show_config_types() -> None:
1✔
92
    """設定タイプ一覧を表示"""
93
    console = rich.console.Console()
1✔
94
    table = rich.table.Table(title="設定タイプ一覧")
1✔
95
    table.add_column("名前", style="cyan")
1✔
96
    table.add_column("説明")
1✔
97

98
    descriptions = {
1✔
99
        "pre-commit": "pre-commit 設定ファイル",
100
        "ruff": "ruff 設定ファイル",
101
        "yamllint": "yamllint 設定ファイル",
102
        "prettier": "prettier 設定ファイル",
103
        "python-version": ".python-version ファイル",
104
        "dockerignore": ".dockerignore ファイル",
105
        "gitignore": ".gitignore ファイル",
106
        "renovate": "renovate 設定ファイル",
107
        "pyproject": "pyproject.toml 共通セクション",
108
        "my-py-lib": "my-py-lib 依存関係の更新",
109
    }
110

111
    for name in py_project.handlers.HANDLERS:
1✔
112
        table.add_row(name, descriptions.get(name, ""))
1✔
113

114
    console.print(table)
1✔
115

116

117
def show_projects(config: py_project.config.Config) -> None:
1✔
118
    """プロジェクト一覧を表示"""
119
    console = rich.console.Console()
1✔
120
    table = rich.table.Table(title="プロジェクト一覧")
1✔
121
    table.add_column("名前", style="cyan")
1✔
122
    table.add_column("パス")
1✔
123
    table.add_column("設定タイプ")
1✔
124

125
    default_configs = config.defaults.configs
1✔
126

127
    for proj in config.projects:
1✔
128
        configs = proj.configs if proj.configs is not None else default_configs
1✔
129
        configs_str = ", ".join(configs) if configs else "(デフォルト)"
1✔
130

131
        table.add_row(proj.name, proj.path, configs_str)
1✔
132

133
    console.print(table)
1✔
134

135

136
def main() -> None:
1✔
137
    """CLI エントリポイント"""
138
    if __doc__ is None:
×
UNCOV
139
        raise RuntimeError("__doc__ is not set")
×
140

141
    args = docopt.docopt(__doc__)
×
142

143
    config_file: str = args["--config"]
×
144
    apply_mode: bool = args["--apply"]
×
145
    projects: list[str] | None = args["--project"] if args["--project"] else None
×
146
    config_types: list[str] | None = args["--type"] if args["--type"] else None
×
147
    show_diff: bool = args["--diff"]
×
148
    backup: bool = args["--backup"]
×
149
    verbose: bool = args["--verbose"]
×
150
    no_sync: bool = args["--no-sync"]
×
151
    git_commit_flag: bool = args["--git-commit"]
×
152
    git_push_flag: bool = args["--git-push"]
×
153
    validate_only: bool = args["--validate"]
×
154
    list_projects_flag: bool = args["--list-projects"]
×
155
    list_configs_flag: bool = args["--list-configs"]
×
156
    update_deps_flag: bool = args["--update-deps"]
×
157
    include_projects_flag: bool = args["--include-projects"]
×
158
    include_config_flag: bool = args["--include-config"]
×
159

160
    log_level = logging.DEBUG if verbose else logging.INFO
×
161
    my_lib.logger.init("py-project", level=log_level)
×
162

163
    console = rich.console.Console()
×
164

165
    # 設定タイプ一覧表示
166
    if list_configs_flag:
×
167
        show_config_types()
×
168
        sys.exit(0)
×
169

170
    # 設定ファイル読み込み
171
    try:
×
172
        config_dict = my_lib.config.load(config_file, pathlib.Path(_SCHEMA_CONFIG))
×
173
    except my_lib.config.ConfigFileNotFoundError as e:
×
174
        console.print(f"[red]設定ファイルが見つかりません: {e}[/red]")
×
175
        sys.exit(1)
×
176
    except my_lib.config.ConfigParseError as e:
×
177
        console.print(f"[red]設定ファイルの形式が不正です:[/red]\n{e.details}")
×
178
        sys.exit(1)
×
179
    except my_lib.config.ConfigValidationError as e:
×
180
        console.print(f"[red]設定ファイルの検証に失敗しました:[/red]\n{e.details}")
×
181
        sys.exit(1)
×
182

183
    # 検証のみ
184
    if validate_only:
×
185
        console.print("[green]設定ファイルは正常です[/green]")
×
186
        sys.exit(0)
×
187

188
    # dict を Config オブジェクトに変換
189
    config = py_project.config.Config.from_dict(config_dict)
×
190

191
    # プロジェクト一覧表示
192
    if list_projects_flag:
×
193
        show_projects(config)
×
194
        sys.exit(0)
×
195

196
    # 依存関係更新
197
    if update_deps_flag:
×
198
        results: list[py_project.dep_updater.FileUpdateResult] = []
×
199

200
        # -p が指定された場合はプロジェクトのみ(テンプレートは更新しない)
201
        update_template = projects is None
×
202

203
        # テンプレートの dependency-groups.dev を更新
204
        if update_template:
×
205
            template_dir = config.get_template_dir()
×
206
            template_path = template_dir / "pyproject" / "sections.toml"
×
207
            console.print("[bold]━━━ テンプレート (sections.toml) ━━━[/bold]")
×
208
            py_project.dep_updater.update_template_deps(
×
209
                template_path=template_path,
210
                dry_run=not apply_mode,
211
                console=console,
212
            )
213

214
        # プロジェクトの dependencies を更新
215
        if include_projects_flag or projects is not None:
×
216
            target_projects = [p for p in config.projects if projects is None or p.name in projects]
×
217

218
            for proj in target_projects:
×
219
                result = py_project.dep_updater.update_project_deps(
×
220
                    project=proj,
221
                    dry_run=not apply_mode,
222
                    console=console,
223
                )
224
                if result is not None:
×
225
                    results.append(result)
×
226

227
        # config.yaml の extra_dev_deps を更新
228
        if include_config_flag:
×
229
            console.print("\n[bold]━━━ 設定ファイル (config.yaml) ━━━[/bold]")
×
230
            result = py_project.dep_updater.update_config_deps(
×
231
                config_path=pathlib.Path(config_file),
232
                projects=projects,
233
                dry_run=not apply_mode,
234
                console=console,
235
            )
236
            if result is not None:
×
237
                results.append(result)
×
238

239
        # 差分表示
240
        if show_diff and results:
×
241
            console.print("\n[bold]━━━ 差分 ━━━[/bold]\n")
×
242
            for result in results:
×
243
                diff_text = py_project.dep_updater.format_diff(result)
×
244
                console.print(diff_text)
×
245
                console.print()
×
246

247
        # サマリー
248
        console.print()
×
249
        if not apply_mode:
×
250
            total_updates = sum(sum(1 for u in r.updates if u.updated) for r in results)
×
251
            if total_updates > 0:
×
252
                console.print(f"[yellow]🔍 合計 {total_updates} 個の依存関係が更新可能です[/yellow]")
×
253
                console.print("[dim]--apply を指定すると実際に更新されます[/dim]")
×
254

255
        sys.exit(0)
×
256

257
    # 設定適用
258
    options = py_project.config.ApplyOptions(
×
259
        dry_run=not apply_mode,
260
        backup=backup,
261
        show_diff=show_diff,
262
        run_sync=not no_sync,
263
        git_commit=git_commit_flag,
264
        git_push=git_push_flag,
265
    )
266
    ret_code = execute(
×
267
        config=config,
268
        options=options,
269
        projects=projects,
270
        config_types=config_types,
271
    )
272

273
    sys.exit(ret_code)
×
274

275

276
if __name__ == "__main__":
277
    main()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc