pyfltr 2.2.2__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {pyfltr-2.2.2 → pyfltr-2.3.0}/CLAUDE.md +1 -0
  2. {pyfltr-2.2.2 → pyfltr-2.3.0}/PKG-INFO +1 -1
  3. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/usage.md +29 -3
  4. {pyfltr-2.2.2 → pyfltr-2.3.0}/mkdocs.yml +3 -5
  5. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/cli.py +5 -1
  6. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/llm_output.py +40 -5
  7. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/main.py +24 -1
  8. pyfltr-2.3.0/pyfltr/shell_completion.py +214 -0
  9. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/output_format_test.py +75 -16
  10. pyfltr-2.3.0/tests/shell_completion_test.py +111 -0
  11. {pyfltr-2.2.2 → pyfltr-2.3.0}/.claude/agents/error-parser-reviewer.md +0 -0
  12. {pyfltr-2.2.2 → pyfltr-2.3.0}/.claude/agents/tool-compat-checker.md +0 -0
  13. {pyfltr-2.2.2 → pyfltr-2.3.0}/.claude/skills/pyfltr-add-tool/SKILL.md +0 -0
  14. {pyfltr-2.2.2 → pyfltr-2.3.0}/.editorconfig +0 -0
  15. {pyfltr-2.2.2 → pyfltr-2.3.0}/.gitattributes +0 -0
  16. {pyfltr-2.2.2 → pyfltr-2.3.0}/.github/workflows/ci.yaml +0 -0
  17. {pyfltr-2.2.2 → pyfltr-2.3.0}/.github/workflows/docs.yaml +0 -0
  18. {pyfltr-2.2.2 → pyfltr-2.3.0}/.github/workflows/release.yaml +0 -0
  19. {pyfltr-2.2.2 → pyfltr-2.3.0}/.gitignore +0 -0
  20. {pyfltr-2.2.2 → pyfltr-2.3.0}/.gitmessage +0 -0
  21. {pyfltr-2.2.2 → pyfltr-2.3.0}/.markdownlint-cli2.yaml +0 -0
  22. {pyfltr-2.2.2 → pyfltr-2.3.0}/.npmrc +0 -0
  23. {pyfltr-2.2.2 → pyfltr-2.3.0}/.pre-commit-config.yaml +0 -0
  24. {pyfltr-2.2.2 → pyfltr-2.3.0}/.pylintrc +0 -0
  25. {pyfltr-2.2.2 → pyfltr-2.3.0}/.python-version +0 -0
  26. {pyfltr-2.2.2 → pyfltr-2.3.0}/.textlintrc.yaml +0 -0
  27. {pyfltr-2.2.2 → pyfltr-2.3.0}/.vscode/extensions.json +0 -0
  28. {pyfltr-2.2.2 → pyfltr-2.3.0}/.vscode/settings.json +0 -0
  29. {pyfltr-2.2.2 → pyfltr-2.3.0}/LICENSE +0 -0
  30. {pyfltr-2.2.2 → pyfltr-2.3.0}/Makefile +0 -0
  31. {pyfltr-2.2.2 → pyfltr-2.3.0}/README.md +0 -0
  32. {pyfltr-2.2.2 → pyfltr-2.3.0}/cliff.toml +0 -0
  33. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/.markdownlint-cli2.yaml +0 -0
  34. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/development/development.md +0 -0
  35. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/development/index.md +0 -0
  36. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/configuration-tools.md +0 -0
  37. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/configuration.md +0 -0
  38. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/custom-commands.md +0 -0
  39. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/index.md +0 -0
  40. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/recommended-nonpython.md +0 -0
  41. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/guide/recommended.md +0 -0
  42. {pyfltr-2.2.2 → pyfltr-2.3.0}/docs/index.md +0 -0
  43. {pyfltr-2.2.2 → pyfltr-2.3.0}/mise.toml +0 -0
  44. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/__init__.py +0 -0
  45. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/__main__.py +0 -0
  46. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/command.py +0 -0
  47. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/config.py +0 -0
  48. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/error_parser.py +0 -0
  49. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/executor.py +0 -0
  50. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/precommit.py +0 -0
  51. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/ui.py +0 -0
  52. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyfltr/warnings_.py +0 -0
  53. {pyfltr-2.2.2 → pyfltr-2.3.0}/pyproject.toml +0 -0
  54. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/__init__.py +0 -0
  55. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/cli_test.py +0 -0
  56. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/command_test.py +0 -0
  57. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/config_test.py +0 -0
  58. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/conftest.py +0 -0
  59. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/error_parser_test.py +0 -0
  60. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/executor_test.py +0 -0
  61. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/llm_output_test.py +0 -0
  62. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/main_test.py +0 -0
  63. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/precommit_test.py +0 -0
  64. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/ui_test.py +0 -0
  65. {pyfltr-2.2.2 → pyfltr-2.3.0}/tests/warnings_test.py +0 -0
  66. {pyfltr-2.2.2 → pyfltr-2.3.0}/uv.lock +0 -0
@@ -15,4 +15,5 @@
15
15
 
16
16
  - `uv run mkdocs build --strict`でリンク・nav整合性を検証(ただし日本語アンカーリンク`#見出し日本語`はMkDocs TOCで解決できずINFO通知のみで`--strict`でも検知されないため手動確認要)
17
17
  - `docs/guide/index.md`の対応ツール一覧と`mkdocs.yml`内llmstxt `markdown_description`の「対応ツール」節は人手同期(SSOT化しない運用)
18
+ - `mkdocs.yml`内llmstxt `markdown_description`にはLLMが利用する際に有用な情報のみ記載する(`run-for-agent`サブコマンド、主要オプションなど)。LLMにとって不要な情報はdocs側をSSOTとし、多重管理を避ける
18
19
  - ドキュメント構成変更時は`docs/development/development.md`の「READMEとdocsの役割分担」節を先に参照
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfltr
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Python Formatters, Linters, and Testers Runner.
5
5
  Project-URL: Homepage, https://github.com/ak110/pyfltr
6
6
  Author-email: "aki." <mark@aur.ll.to>
@@ -70,6 +70,30 @@ pyfltr generate-config
70
70
  設定ファイルの雛形を標準出力に書き出す。`[tool.pyfltr]`セクションに貼り付けて利用する。
71
71
  このサブコマンドは他のオプションやターゲット指定を受け付けず、設定出力だけを行う。
72
72
 
73
+ ### サブコマンド: generate-shell-completion
74
+
75
+ ```shell
76
+ pyfltr generate-shell-completion bash
77
+ pyfltr generate-shell-completion powershell
78
+ ```
79
+
80
+ シェル補完スクリプトを標準出力に書き出す。
81
+ 引数にシェル種別(`bash`または`powershell`)を指定する。
82
+
83
+ bashでの設定例:
84
+
85
+ ```shell
86
+ eval "$(pyfltr generate-shell-completion bash)"
87
+ ```
88
+
89
+ PowerShellでの設定例:
90
+
91
+ ```powershell
92
+ pyfltr generate-shell-completion powershell | Out-String | Invoke-Expression
93
+ ```
94
+
95
+ 永続化する場合はプロファイルに上記を追記する。
96
+
73
97
  ### `[files and/or directories ...]`
74
98
 
75
99
  対象を指定しなかった場合は、カレントディレクトリ(`.`)を指定した場合と同じ扱いとなる。
@@ -176,14 +200,16 @@ CLIオプション`--output-format`が指定されている場合は環境変数
176
200
 
177
201
  ### jsonlスキーマ
178
202
 
179
- 出力は以下4種別のレコードからなる。`kind`フィールドでレコード種別を判別する。
203
+ 出力は以下5種別のレコードからなる。`kind`フィールドでレコード種別を判別する。
180
204
 
205
+ - `header`: 先頭1行。実行環境情報(`version` / `python` / `platform` / `cwd` / `commands` / `files`)
181
206
  - `warning`: pyfltrが検出した設定・実行時の警告(`source`で発生元を識別)
182
207
  - `diagnostic`: 個々の診断(`error_parser`対応ツールの抽出結果)
183
208
  - `tool`: 1ツール1レコードの実行メタ情報
184
209
  - `summary`: 最終1行、全体集計
185
210
 
186
211
  ```json
212
+ {"kind":"header","version":"1.25.0","python":"3.12.0 ...","executable":"/usr/bin/python3","platform":"linux","cwd":"/work","commands":["mypy","black"],"files":12}
187
213
  {"kind":"warning","source":"config","msg":"pre-commit が有効化されていますが、設定ファイルが見つかりません: .pre-commit-config.yaml"}
188
214
  {"kind":"diagnostic","tool":"mypy","file":"src/a.py","line":42,"col":5,"msg":"Incompatible return value type"}
189
215
  {"kind":"tool","tool":"mypy","type":"linter","status":"failed","files":12,"elapsed":0.8,"diagnostics":1,"rc":1}
@@ -191,9 +217,9 @@ CLIオプション`--output-format`が指定されている場合は環境変数
191
217
  {"kind":"summary","total":2,"succeeded":0,"formatted":1,"failed":1,"skipped":0,"diagnostics":1,"exit":1}
192
218
  ```
193
219
 
194
- stdoutモード(`--output-file`未指定)では、各ツールの完了時にdiagnostic行+tool行を随時書き出す。
220
+ stdoutモード(`--output-file`未指定)では、先頭にheader行を出力し、各ツールの完了時にdiagnostic行+tool行を随時書き出す。
195
221
  ツール間の出力順は完了順となり、最後にwarning行+summary行が続く。
196
- ファイル出力時(`--output-file`指定)では、`pyproject.toml`の定義順にツール単位でグルーピングし、先頭にwarning行、末尾にsummary行を配置する。
222
+ ファイル出力時(`--output-file`指定)では、先頭にheader行、続いて`pyproject.toml`の定義順にツール単位でグルーピングし、先頭にwarning行、末尾にsummary行を配置する。
197
223
 
198
224
  `warning`レコードの`source`は`config`(設定ファイル不在など)/`tool-resolve`(ツール解決失敗)/`file-resolver`(対象ファイル選定時)/`git`(`git check-ignore`失敗)のいずれか。
199
225
 
@@ -39,7 +39,6 @@ plugins:
39
39
  - `pyfltr run`: 全チェック。fix段→formatter段→linter/tester段の順で、fix-args定義済みlinterの自動修正→formatter→残りの検査を実行する(ローカル向け)
40
40
  - `pyfltr fast`: 軽量チェック。`run`と同じ3段構成だが、mypy/pylint/pytest等の重いツールを除外(pre-commit向け)
41
41
  - `pyfltr run-for-agent`: `pyfltr run --output-format=jsonl`のエイリアス。JSONL出力を既定にする(LLMエージェント向け)
42
- - `pyfltr generate-config`: pyproject.toml用の設定雛形を出力
43
42
 
44
43
  `--no-fix`で`run`/`fast`/`run-for-agent`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
45
44
 
@@ -68,16 +67,15 @@ plugins:
68
67
 
69
68
  ## LLMエージェント向け出力(`--output-format=jsonl`)
70
69
 
71
- `--output-format=jsonl`でJSON Lines形式の構造化出力が得られる。レコードは4種類。
70
+ `--output-format=jsonl`でJSON Lines形式の構造化出力が得られる。レコードは5種類。
72
71
 
72
+ - `header`: 先頭1行。実行環境情報(`version` / `python` / `platform` / `cwd` / `commands` / `files`)
73
73
  - `warning`: pyfltr が検出した設定・実行時の警告(`source` で発生元を識別)
74
74
  - `diagnostic`: 1診断1行(`tool` / `file` / `line` / `col` / `rule` / `severity` / `msg`)
75
75
  - `tool`: ツールごとの実行結果サマリ(`status` / `elapsed` / `diagnostics` / `rc`)
76
76
  - `summary`: 最終1行の全体集計(`succeeded` / `formatted` / `failed` / `skipped` / `diagnostics` / `exit`)
77
77
 
78
- 出力順は`warning`→`diagnostic`(file/line/col/command順)→`tool`(`pyproject.toml`定義順)→`summary`(1行)で固定。
79
-
80
- `--output-file`未指定時はstdoutへJSONLのみを書き、進捗ログやTUIは無音化される。指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
78
+ stdoutモード(`--output-file`未指定)の出力順は`header`→ツール完了順に`diagnostic`+`tool`→`warning`→`summary`。stdoutへJSONLのみを書き、進捗ログやTUIは無音化される。`--output-file`指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
81
79
 
82
80
  ```shell
83
81
  pyfltr run-for-agent
@@ -140,6 +140,8 @@ def render_results(
140
140
  output_format: str = "text",
141
141
  output_file: pathlib.Path | None = None,
142
142
  exit_code: int = 0,
143
+ commands: list[str] | None = None,
144
+ files: int | None = None,
143
145
  warnings: list[dict[str, typing.Any]] | None = None,
144
146
  ) -> None:
145
147
  """実行結果を `成功コマンド → 失敗コマンド → summary` の順でまとめて出力する。
@@ -160,7 +162,9 @@ def render_results(
160
162
  warnings = warnings or []
161
163
 
162
164
  if output_format == "jsonl":
163
- pyfltr.llm_output.write_jsonl(ordered, config, exit_code=exit_code, destination=output_file, warnings=warnings)
165
+ pyfltr.llm_output.write_jsonl(
166
+ ordered, config, exit_code=exit_code, destination=output_file, commands=commands, files=files, warnings=warnings
167
+ )
164
168
  if output_file is None:
165
169
  return
166
170
 
@@ -1,11 +1,13 @@
1
1
  """LLM 向け JSON Lines 出力。
2
2
 
3
3
  `--output-format=jsonl` で呼ばれ、CommandResult 群を LLM / エージェントが
4
- 読みやすいフラットな JSON Lines 形式 (diagnostic / tool / summary の 3 種別) に
4
+ 読みやすいフラットな JSON Lines 形式 (header / diagnostic / tool / summary の 4 種別) に
5
5
  変換して書き出す。
6
6
  """
7
7
 
8
+ import importlib.metadata
8
9
  import json
10
+ import os
9
11
  import pathlib
10
12
  import sys
11
13
  import threading
@@ -47,14 +49,17 @@ def build_lines(
47
49
  config: pyfltr.config.Config,
48
50
  *,
49
51
  exit_code: int,
52
+ commands: list[str] | None = None,
53
+ files: int | None = None,
50
54
  warnings: list[dict[str, typing.Any]] | None = None,
51
55
  ) -> list[str]:
52
56
  """CommandResult群からJSONL各行を生成する。
53
57
 
54
58
  出力順:
55
- 1. ``warnings``が非空ならkind="warning"行
56
- 2. ツール単位でdiagnostic行+tool行(``config.command_names``の定義順)
57
- 3. summary行1行
59
+ 1. ``commands``と``files``が指定されていればkind="header"行
60
+ 2. ``warnings``が非空ならkind="warning"行
61
+ 3. ツール単位でdiagnostic行+tool行(``config.command_names``の定義順)
62
+ 4. summary行1行
58
63
 
59
64
  resultsは順序を問わない。内部で``config.command_names``順にソートする。
60
65
  ``warnings``は``pyfltr.warnings_.collected_warnings()``の返り値を想定する。
@@ -63,6 +68,9 @@ def build_lines(
63
68
 
64
69
  lines: list[str] = []
65
70
 
71
+ if commands is not None and files is not None:
72
+ lines.append(_dump(_build_header_record(commands, files)))
73
+
66
74
  for warning in warnings or []:
67
75
  lines.append(_dump(_build_warning_record(warning)))
68
76
 
@@ -86,6 +94,8 @@ def write_jsonl(
86
94
  *,
87
95
  exit_code: int,
88
96
  destination: pathlib.Path | None,
97
+ commands: list[str] | None = None,
98
+ files: int | None = None,
89
99
  warnings: list[dict[str, typing.Any]] | None = None,
90
100
  ) -> None:
91
101
  """JSONL を stdout もしくは指定ファイルに書き出す。
@@ -94,7 +104,7 @@ def write_jsonl(
94
104
  親ディレクトリを自動作成し、atomic write せず単純に上書きする
95
105
  (LLM 用途の使い捨てのため)。
96
106
  """
97
- lines = build_lines(results, config, exit_code=exit_code, warnings=warnings)
107
+ lines = build_lines(results, config, exit_code=exit_code, commands=commands, files=files, warnings=warnings)
98
108
  if destination is None:
99
109
  for line in lines:
100
110
  sys.stdout.write(line)
@@ -108,6 +118,17 @@ def write_jsonl(
108
118
  f.write("\n")
109
119
 
110
120
 
121
+ def write_jsonl_header(commands: list[str], files: int) -> None:
122
+ """header行をstdoutに書き出す(ストリーミングモード用)。
123
+
124
+ パイプライン開始直後、diagnostic行より前に1回だけ呼ぶ。
125
+ """
126
+ with _write_lock:
127
+ sys.stdout.write(_dump(_build_header_record(commands, files)))
128
+ sys.stdout.write("\n")
129
+ sys.stdout.flush()
130
+
131
+
111
132
  def write_jsonl_streaming(
112
133
  result: pyfltr.command.CommandResult,
113
134
  config: pyfltr.config.Config,
@@ -149,6 +170,20 @@ def _dump(record: dict[str, typing.Any]) -> str:
149
170
  return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
150
171
 
151
172
 
173
+ def _build_header_record(commands: list[str], files: int) -> dict[str, typing.Any]:
174
+ """実行環境の基本情報を header レコード dict として返す。"""
175
+ return {
176
+ "kind": "header",
177
+ "version": importlib.metadata.version("pyfltr"),
178
+ "python": sys.version,
179
+ "executable": sys.executable,
180
+ "platform": sys.platform,
181
+ "cwd": os.getcwd(),
182
+ "commands": commands,
183
+ "files": files,
184
+ }
185
+
186
+
152
187
  def _build_warning_record(entry: dict[str, typing.Any]) -> dict[str, typing.Any]:
153
188
  """警告 dict を warning レコード dict に変換する。"""
154
189
  return {
@@ -14,6 +14,7 @@ import pyfltr.cli
14
14
  import pyfltr.command
15
15
  import pyfltr.config
16
16
  import pyfltr.llm_output
17
+ import pyfltr.shell_completion
17
18
  import pyfltr.ui
18
19
  import pyfltr.warnings_
19
20
 
@@ -41,6 +42,8 @@ def build_parser() -> argparse.ArgumentParser:
41
42
  " fast 高速ツールのみ実行 (--commands=fast 相当)。\n"
42
43
  " run-for-agent LLM エージェント向け (JSONL 出力を既定化)。\n"
43
44
  " generate-config pyproject.toml 用の設定雛形を出力する。\n"
45
+ " generate-shell-completion <shell>\n"
46
+ " シェル補完スクリプトを出力する (bash / powershell)。\n"
44
47
  "\n"
45
48
  "ドキュメント: https://ak110.github.io/pyfltr/\n"
46
49
  "llms.txt: https://ak110.github.io/pyfltr/llms.txt"
@@ -151,12 +154,16 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
151
154
  "fast",
152
155
  "run-for-agent",
153
156
  "generate-config",
157
+ "generate-shell-completion",
154
158
  # 以下は廃止済み
155
159
  "fix",
156
160
  "dirty",
157
161
  }
158
162
  )
159
163
 
164
+ # 廃止済みサブコマンド
165
+ _DEPRECATED_SUBCOMMANDS: frozenset[str] = frozenset({"fix", "dirty"})
166
+
160
167
 
161
168
  def _parse_subcommand(sys_args: typing.Sequence[str]) -> tuple[str, list[str]]:
162
169
  """第一引数からサブコマンドを判定し、(subcommand, remaining_args)を返す。
@@ -190,7 +197,7 @@ def run(sys_args: typing.Sequence[str] | None = None) -> int:
190
197
  subcommand, remaining_args = _parse_subcommand(sys_args)
191
198
 
192
199
  # 廃止済みサブコマンド
193
- if subcommand in ("fix", "dirty"):
200
+ if subcommand in _DEPRECATED_SUBCOMMANDS:
194
201
  logger.error(f"{subcommand} サブコマンドは廃止されました。")
195
202
  return 1
196
203
 
@@ -200,6 +207,17 @@ def run(sys_args: typing.Sequence[str] | None = None) -> int:
200
207
  logger.info(pyfltr.config.generate_config_text())
201
208
  return 0
202
209
 
210
+ # generate-shell-completionサブコマンド: 補完スクリプトをstdoutに出力する
211
+ if subcommand == "generate-shell-completion":
212
+ if not remaining_args or remaining_args[0] not in pyfltr.shell_completion.SUPPORTED_SHELLS:
213
+ supported = ", ".join(pyfltr.shell_completion.SUPPORTED_SHELLS)
214
+ logger.error(f"シェルを指定してください: pyfltr generate-shell-completion <{supported}>")
215
+ return 1
216
+ active_subcommands = _SUBCOMMANDS - _DEPRECATED_SUBCOMMANDS
217
+ script = pyfltr.shell_completion.generate(remaining_args[0], build_parser(), active_subcommands)
218
+ print(script, end="")
219
+ return 0
220
+
203
221
  effective_args = _build_effective_args(subcommand, remaining_args)
204
222
 
205
223
  parser = build_parser()
@@ -407,6 +425,9 @@ def run_pipeline(
407
425
  # JSONL stdoutモード: ツール完了時に随時JSONL行を書き出す
408
426
  jsonl_stdout = (args.output_format == "jsonl") and (args.output_file is None)
409
427
 
428
+ if jsonl_stdout:
429
+ pyfltr.llm_output.write_jsonl_header(commands=commands, files=len(all_files))
430
+
410
431
  # run
411
432
  include_fix_stage = bool(getattr(args, "include_fix_stage", False))
412
433
  if use_ui:
@@ -448,6 +469,8 @@ def run_pipeline(
448
469
  output_format=args.output_format or "text",
449
470
  output_file=args.output_file,
450
471
  exit_code=returncode,
472
+ commands=commands,
473
+ files=len(all_files),
451
474
  warnings=pyfltr.warnings_.collected_warnings(),
452
475
  )
453
476
  return returncode
@@ -0,0 +1,214 @@
1
+ """シェル補完スクリプトの生成。"""
2
+
3
+ import argparse
4
+
5
+ import pyfltr.config
6
+
7
+ SUPPORTED_SHELLS: tuple[str, ...] = ("bash", "powershell")
8
+ """対応シェル。"""
9
+
10
+
11
+ def generate(
12
+ shell: str,
13
+ parser: argparse.ArgumentParser,
14
+ subcommands: frozenset[str],
15
+ ) -> str:
16
+ """シェル種別に応じた補完スクリプトを返す。"""
17
+ options, output_format_choices, commands_choices = _collect_completions(parser)
18
+ subcommand_list = sorted(subcommands)
19
+ if shell == "bash":
20
+ return _generate_bash(options, output_format_choices, commands_choices, subcommand_list)
21
+ if shell == "powershell":
22
+ return _generate_powershell(options, output_format_choices, commands_choices, subcommand_list)
23
+ raise ValueError(f"未対応のシェル: {shell!r}")
24
+
25
+
26
+ def _collect_completions(
27
+ parser: argparse.ArgumentParser,
28
+ ) -> tuple[list[str], list[str], list[str]]:
29
+ """パーサーからオプション名・choices・コマンド名を収集する。
30
+
31
+ 戻り値: (options, output_format_choices, commands_choices)
32
+ """
33
+ options: list[str] = []
34
+ output_format_choices: list[str] = []
35
+ for action in parser._actions: # noqa: SLF001 # pylint: disable=protected-access
36
+ for opt in action.option_strings:
37
+ options.append(opt)
38
+ if "--output-format" in action.option_strings and action.choices:
39
+ output_format_choices = list(action.choices)
40
+
41
+ # --commands の補完候補: ビルトインコマンド名 + 静的エイリアスキー
42
+ commands_choices = list(pyfltr.config.BUILTIN_COMMAND_NAMES)
43
+ aliases = pyfltr.config.DEFAULT_CONFIG.get("aliases", {})
44
+ assert isinstance(aliases, dict)
45
+ for alias_name in aliases:
46
+ if alias_name not in commands_choices:
47
+ commands_choices.append(alias_name)
48
+ commands_choices.sort()
49
+
50
+ return sorted(options), sorted(output_format_choices), commands_choices
51
+
52
+
53
+ def _generate_bash(
54
+ options: list[str],
55
+ output_format_choices: list[str],
56
+ commands_choices: list[str],
57
+ subcommands: list[str],
58
+ ) -> str:
59
+ """bash用補完スクリプトを生成する。"""
60
+ opts = " ".join(options)
61
+ subs = " ".join(subcommands)
62
+ shells = " ".join(SUPPORTED_SHELLS)
63
+ fmt_choices = " ".join(output_format_choices)
64
+ cmd_choices = " ".join(commands_choices)
65
+
66
+ return f'''\
67
+ _pyfltr_completions() {{
68
+ local cur prev words cword
69
+ _init_completion || return
70
+
71
+ local subcommands="{subs}"
72
+ local options="{opts}"
73
+ local shells="{shells}"
74
+ local output_formats="{fmt_choices}"
75
+ local commands="{cmd_choices}"
76
+
77
+ # generate-shell-completionの第2引数: シェル名を補完
78
+ if [[ ${{cword}} -ge 2 && "${{words[1]}}" == "generate-shell-completion" ]]; then
79
+ COMPREPLY=( $(compgen -W "${{shells}}" -- "${{cur}}") )
80
+ return
81
+ fi
82
+
83
+ # --output-format / --output-format= の補完
84
+ if [[ "${{prev}}" == "--output-format" ]]; then
85
+ COMPREPLY=( $(compgen -W "${{output_formats}}" -- "${{cur}}") )
86
+ return
87
+ fi
88
+ if [[ "${{cur}}" == --output-format=* ]]; then
89
+ local prefix="${{cur%%=*}}="
90
+ local typed="${{cur#*=}}"
91
+ COMPREPLY=( $(compgen -W "${{output_formats}}" -- "${{typed}}") )
92
+ COMPREPLY=( "${{COMPREPLY[@]/#/${{prefix}}}}" )
93
+ return
94
+ fi
95
+
96
+ # --commands / --commands= の補完
97
+ if [[ "${{prev}}" == "--commands" ]]; then
98
+ COMPREPLY=( $(compgen -W "${{commands}}" -- "${{cur}}") )
99
+ return
100
+ fi
101
+ if [[ "${{cur}}" == --commands=* ]]; then
102
+ local prefix="${{cur%%=*}}="
103
+ local typed="${{cur#*=}}"
104
+ COMPREPLY=( $(compgen -W "${{commands}}" -- "${{typed}}") )
105
+ COMPREPLY=( "${{COMPREPLY[@]/#/${{prefix}}}}" )
106
+ return
107
+ fi
108
+
109
+ # 第1引数: サブコマンド + オプション
110
+ if [[ ${{cword}} -eq 1 ]]; then
111
+ COMPREPLY=( $(compgen -W "${{subcommands}} ${{options}}" -- "${{cur}}") )
112
+ return
113
+ fi
114
+
115
+ # オプション開始
116
+ if [[ "${{cur}}" == -* ]]; then
117
+ COMPREPLY=( $(compgen -W "${{options}}" -- "${{cur}}") )
118
+ return
119
+ fi
120
+
121
+ # ファイル/ディレクトリ補完
122
+ _filedir
123
+ }}
124
+
125
+ complete -o default -F _pyfltr_completions pyfltr
126
+ '''
127
+
128
+
129
+ def _generate_powershell(
130
+ options: list[str],
131
+ output_format_choices: list[str],
132
+ commands_choices: list[str],
133
+ subcommands: list[str],
134
+ ) -> str:
135
+ """PowerShell用補完スクリプトを生成する。"""
136
+ # PowerShellの配列リテラルとして出力
137
+ opts_ps = ", ".join(f"'{o}'" for o in options)
138
+ subs_ps = ", ".join(f"'{s}'" for s in subcommands)
139
+ shells_ps = ", ".join(f"'{s}'" for s in SUPPORTED_SHELLS)
140
+ fmt_ps = ", ".join(f"'{f}'" for f in output_format_choices)
141
+ cmd_ps = ", ".join(f"'{c}'" for c in commands_choices)
142
+
143
+ return f"""\
144
+ Register-ArgumentCompleter -Native -CommandName pyfltr -ScriptBlock {{
145
+ param($wordToComplete, $commandAst, $cursorPosition)
146
+
147
+ $subcommands = @({subs_ps})
148
+ $options = @({opts_ps})
149
+ $shells = @({shells_ps})
150
+ $outputFormats = @({fmt_ps})
151
+ $commands = @({cmd_ps})
152
+
153
+ $tokens = $commandAst.ToString().Substring(0, $cursorPosition).Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries)
154
+ $tokenCount = $tokens.Count
155
+
156
+ # generate-shell-completionの第2引数: シェル名を補完
157
+ if ($tokenCount -ge 2 -and $tokens[1] -eq 'generate-shell-completion') {{
158
+ $shells | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
159
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
160
+ }}
161
+ return
162
+ }}
163
+
164
+ # --output-format の値補完
165
+ if ($tokenCount -ge 2 -and $tokens[-1] -eq '--output-format') {{
166
+ $outputFormats | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
167
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
168
+ }}
169
+ return
170
+ }}
171
+ if ($wordToComplete -like '--output-format=*') {{
172
+ $typed = $wordToComplete.Substring('--output-format='.Length)
173
+ $outputFormats | Where-Object {{ $_ -like "$typed*" }} | ForEach-Object {{
174
+ $val = "--output-format=$_"
175
+ [System.Management.Automation.CompletionResult]::new($val, $val, 'ParameterValue', $_)
176
+ }}
177
+ return
178
+ }}
179
+
180
+ # --commands の値補完
181
+ if ($tokenCount -ge 2 -and $tokens[-1] -eq '--commands') {{
182
+ $commands | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
183
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
184
+ }}
185
+ return
186
+ }}
187
+ if ($wordToComplete -like '--commands=*') {{
188
+ $typed = $wordToComplete.Substring('--commands='.Length)
189
+ $commands | Where-Object {{ $_ -like "$typed*" }} | ForEach-Object {{
190
+ $val = "--commands=$_"
191
+ [System.Management.Automation.CompletionResult]::new($val, $val, 'ParameterValue', $_)
192
+ }}
193
+ return
194
+ }}
195
+
196
+ # 第1引数: サブコマンド + オプション
197
+ if ($tokenCount -le 1 -or ($tokenCount -eq 2 -and $wordToComplete -ne '')) {{
198
+ $all = $subcommands + $options
199
+ $all | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
200
+ $type = if ($_ -like '-*') {{ 'ParameterName' }} else {{ 'ParameterValue' }}
201
+ [System.Management.Automation.CompletionResult]::new($_, $_, $type, $_)
202
+ }}
203
+ return
204
+ }}
205
+
206
+ # オプション開始
207
+ if ($wordToComplete -like '-*') {{
208
+ $options | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
209
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
210
+ }}
211
+ return
212
+ }}
213
+ }}
214
+ """
@@ -33,11 +33,11 @@ def test_build_lines_supported_tool_diagnostics(default_config):
33
33
  _make_error("mypy", "src/a.py", 20, "missing return"),
34
34
  ]
35
35
  result = _make_result("mypy", returncode=1, errors=errors)
36
- lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=1)
36
+ lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=1, commands=["mypy"], files=5)
37
37
  parsed = [json.loads(line) for line in lines]
38
38
 
39
- assert [r["kind"] for r in parsed] == ["diagnostic", "diagnostic", "tool", "summary"]
40
- assert parsed[0] == {
39
+ assert [r["kind"] for r in parsed] == ["header", "diagnostic", "diagnostic", "tool", "summary"]
40
+ assert parsed[1] == {
41
41
  "kind": "diagnostic",
42
42
  "tool": "mypy",
43
43
  "file": "src/a.py",
@@ -45,17 +45,17 @@ def test_build_lines_supported_tool_diagnostics(default_config):
45
45
  "col": 4,
46
46
  "msg": "bad type",
47
47
  }
48
- assert parsed[1] == {
48
+ assert parsed[2] == {
49
49
  "kind": "diagnostic",
50
50
  "tool": "mypy",
51
51
  "file": "src/a.py",
52
52
  "line": 20,
53
53
  "msg": "missing return",
54
54
  }
55
- assert parsed[2]["diagnostics"] == 2
56
- assert parsed[2]["status"] == "failed"
57
55
  assert parsed[3]["diagnostics"] == 2
58
- assert parsed[3]["failed"] == 1
56
+ assert parsed[3]["status"] == "failed"
57
+ assert parsed[4]["diagnostics"] == 2
58
+ assert parsed[4]["failed"] == 1
59
59
 
60
60
 
61
61
  def test_build_lines_warnings_prepended(default_config):
@@ -65,14 +65,15 @@ def test_build_lines_warnings_prepended(default_config):
65
65
  {"source": "config", "message": "pre-commit 設定ファイル不在"},
66
66
  {"source": "git", "message": "git が見つからない"},
67
67
  ]
68
- lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=0, warnings=warnings)
68
+ lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=0, commands=["black"], files=1, warnings=warnings)
69
69
  parsed = [json.loads(line) for line in lines]
70
70
 
71
- assert [r["kind"] for r in parsed[:2]] == ["warning", "warning"]
72
- assert parsed[0] == {"kind": "warning", "source": "config", "msg": "pre-commit 設定ファイル不在"}
73
- assert parsed[1] == {"kind": "warning", "source": "git", "msg": "git が見つからない"}
71
+ assert parsed[0]["kind"] == "header"
72
+ assert [r["kind"] for r in parsed[1:3]] == ["warning", "warning"]
73
+ assert parsed[1] == {"kind": "warning", "source": "config", "msg": "pre-commit 設定ファイル不在"}
74
+ assert parsed[2] == {"kind": "warning", "source": "git", "msg": "git が見つからない"}
74
75
  # warnings の後に tool レコード、最後に summary が並ぶ
75
- assert [r["kind"] for r in parsed[2:]] == ["tool", "summary"]
76
+ assert [r["kind"] for r in parsed[3:]] == ["tool", "summary"]
76
77
 
77
78
 
78
79
  def test_build_lines_no_warnings_when_omitted(default_config):
@@ -84,7 +85,7 @@ def test_build_lines_no_warnings_when_omitted(default_config):
84
85
 
85
86
 
86
87
  def test_build_lines_unsupported_tool_only(default_config):
87
- """error_parser 非対応ツール (black) は tool レコードのみ。"""
88
+ """error_parser 非対応ツール (black) は tool レコードのみ(header省略時)。"""
88
89
  result = _make_result("black", returncode=1, command_type="formatter", has_error=False)
89
90
  lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=1)
90
91
  parsed = [json.loads(line) for line in lines]
@@ -115,11 +116,18 @@ def test_build_lines_mixed_order(default_config):
115
116
  black_result = _make_result("black", returncode=0, command_type="formatter")
116
117
 
117
118
  # config.command_names 順では black → mypy → pylint
118
- lines = pyfltr.llm_output.build_lines([mypy_result, pylint_result, black_result], default_config, exit_code=1)
119
+ lines = pyfltr.llm_output.build_lines(
120
+ [mypy_result, pylint_result, black_result],
121
+ default_config,
122
+ exit_code=1,
123
+ commands=["black", "mypy", "pylint"],
124
+ files=10,
125
+ )
119
126
  parsed = [json.loads(line) for line in lines]
120
127
 
121
- # ツール単位のグルーピング: black(tool) → mypy(diagnostic, diagnostic, tool) → pylint(diagnostic, tool) → summary
128
+ # header → ツール単位のグルーピング: black(tool) → mypy(diagnostic, diagnostic, tool) → pylint(diagnostic, tool) → summary
122
129
  assert [r["kind"] for r in parsed] == [
130
+ "header",
123
131
  "tool", # black
124
132
  "diagnostic",
125
133
  "diagnostic",
@@ -253,7 +261,7 @@ def test_calculate_returncode_matches_summary_exit(default_config):
253
261
  _make_result("black", returncode=0, command_type="formatter"),
254
262
  ]
255
263
  exit_code = pyfltr.main.calculate_returncode(results, exit_zero_even_if_formatted=False)
256
- lines = pyfltr.llm_output.build_lines(results, default_config, exit_code=exit_code)
264
+ lines = pyfltr.llm_output.build_lines(results, default_config, exit_code=exit_code, commands=["mypy", "black"], files=3)
257
265
  summary = json.loads(lines[-1])
258
266
  assert summary["exit"] == exit_code == 1
259
267
 
@@ -275,6 +283,9 @@ def test_run_cli_jsonl_stdout_suppresses_text(mocker, capsys):
275
283
  assert "summary" not in captured.out or '"kind":"summary"' in captured.out
276
284
  lines = [line for line in captured.out.splitlines() if line.strip()]
277
285
  assert lines, "JSONL が 1 行も出ていない"
286
+ first = json.loads(lines[0])
287
+ assert first["kind"] == "header"
288
+ assert "mypy" in first["commands"]
278
289
  last = json.loads(lines[-1])
279
290
  assert last["kind"] == "summary"
280
291
  assert last["exit"] == 0
@@ -467,3 +478,51 @@ def test_write_jsonl_footer_no_warnings(capsys):
467
478
  assert len(parsed) == 1
468
479
  assert parsed[0]["kind"] == "summary"
469
480
  assert parsed[0]["succeeded"] == 1
481
+
482
+
483
+ # ---------------------------------------------------------------------------
484
+ # header レコードのユニットテスト
485
+ # ---------------------------------------------------------------------------
486
+
487
+
488
+ def test_build_header_record_fields():
489
+ """_build_header_record が必要なフィールドをすべて含むこと。"""
490
+ record = pyfltr.llm_output._build_header_record(["ruff-format", "mypy"], 42)
491
+ assert record["kind"] == "header"
492
+ assert record["commands"] == ["ruff-format", "mypy"]
493
+ assert record["files"] == 42
494
+ assert "version" in record
495
+ assert "python" in record
496
+ assert "executable" in record
497
+ assert "platform" in record
498
+ assert "cwd" in record
499
+
500
+
501
+ def test_build_lines_header_first(default_config):
502
+ """commands/filesを指定するとheader行が先頭に出力されること。"""
503
+ result = _make_result("mypy", returncode=0)
504
+ lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=0, commands=["mypy"], files=10)
505
+ parsed = [json.loads(line) for line in lines]
506
+ assert parsed[0]["kind"] == "header"
507
+ assert parsed[0]["commands"] == ["mypy"]
508
+ assert parsed[0]["files"] == 10
509
+ assert parsed[-1]["kind"] == "summary"
510
+
511
+
512
+ def test_build_lines_no_header_when_omitted(default_config):
513
+ """commands/filesを省略するとheader行は出力されないこと。"""
514
+ result = _make_result("mypy", returncode=0)
515
+ lines = pyfltr.llm_output.build_lines([result], default_config, exit_code=0)
516
+ parsed = [json.loads(line) for line in lines]
517
+ assert all(r["kind"] != "header" for r in parsed)
518
+
519
+
520
+ def test_write_jsonl_header_stdout(capsys):
521
+ """write_jsonl_headerがstdoutにheader行を書き出すこと。"""
522
+ pyfltr.llm_output.write_jsonl_header(commands=["ruff-format", "mypy"], files=5)
523
+ captured = capsys.readouterr()
524
+ parsed = [json.loads(line) for line in captured.out.splitlines()]
525
+ assert len(parsed) == 1
526
+ assert parsed[0]["kind"] == "header"
527
+ assert parsed[0]["commands"] == ["ruff-format", "mypy"]
528
+ assert parsed[0]["files"] == 5
@@ -0,0 +1,111 @@
1
+ # pylint: disable=missing-module-docstring
2
+ # pylint: disable=missing-function-docstring
3
+ # pylint: disable=protected-access
4
+
5
+ import pyfltr.config
6
+ import pyfltr.main
7
+ import pyfltr.shell_completion
8
+
9
+
10
+ class TestCollectCompletions:
11
+ """補完データ収集のテスト。"""
12
+
13
+ def test_options_contain_verbose(self):
14
+ parser = pyfltr.main.build_parser()
15
+ options, _, _ = pyfltr.shell_completion._collect_completions(parser)
16
+ assert "--verbose" in options
17
+ assert "-v" in options
18
+
19
+ def test_options_contain_output_format(self):
20
+ parser = pyfltr.main.build_parser()
21
+ options, _, _ = pyfltr.shell_completion._collect_completions(parser)
22
+ assert "--output-format" in options
23
+
24
+ def test_output_format_choices(self):
25
+ parser = pyfltr.main.build_parser()
26
+ _, output_format_choices, _ = pyfltr.shell_completion._collect_completions(parser)
27
+ assert "text" in output_format_choices
28
+ assert "jsonl" in output_format_choices
29
+
30
+ def test_commands_choices_contain_builtin_and_aliases(self):
31
+ parser = pyfltr.main.build_parser()
32
+ _, _, commands_choices = pyfltr.shell_completion._collect_completions(parser)
33
+ # ビルトインコマンド
34
+ for name in pyfltr.config.BUILTIN_COMMAND_NAMES:
35
+ assert name in commands_choices
36
+ # 静的エイリアス
37
+ for alias in pyfltr.config.DEFAULT_CONFIG["aliases"]:
38
+ assert alias in commands_choices
39
+
40
+
41
+ class TestGenerateBash:
42
+ """bash補完スクリプト生成のテスト。"""
43
+
44
+ def test_contains_function_and_complete(self):
45
+ parser = pyfltr.main.build_parser()
46
+ subcommands = frozenset({"ci", "run", "fast", "generate-shell-completion"})
47
+ script = pyfltr.shell_completion.generate("bash", parser, subcommands)
48
+ assert "_pyfltr_completions()" in script
49
+ assert "complete -o default -F _pyfltr_completions pyfltr" in script
50
+
51
+ def test_contains_subcommands(self):
52
+ parser = pyfltr.main.build_parser()
53
+ subcommands = frozenset({"ci", "run", "generate-shell-completion"})
54
+ script = pyfltr.shell_completion.generate("bash", parser, subcommands)
55
+ assert "ci" in script
56
+ assert "run" in script
57
+ assert "generate-shell-completion" in script
58
+
59
+ def test_contains_output_format_choices(self):
60
+ parser = pyfltr.main.build_parser()
61
+ script = pyfltr.shell_completion.generate("bash", parser, frozenset({"ci"}))
62
+ assert "text" in script
63
+ assert "jsonl" in script
64
+
65
+
66
+ class TestGeneratePowershell:
67
+ """PowerShell補完スクリプト生成のテスト。"""
68
+
69
+ def test_contains_register_argument_completer(self):
70
+ parser = pyfltr.main.build_parser()
71
+ subcommands = frozenset({"ci", "run"})
72
+ script = pyfltr.shell_completion.generate("powershell", parser, subcommands)
73
+ assert "Register-ArgumentCompleter -Native -CommandName pyfltr" in script
74
+
75
+ def test_contains_subcommands(self):
76
+ parser = pyfltr.main.build_parser()
77
+ subcommands = frozenset({"ci", "run", "generate-shell-completion"})
78
+ script = pyfltr.shell_completion.generate("powershell", parser, subcommands)
79
+ assert "'ci'" in script
80
+ assert "'run'" in script
81
+ assert "'generate-shell-completion'" in script
82
+
83
+
84
+ class TestMainIntegration:
85
+ """main.py経由の統合テスト。"""
86
+
87
+ def test_bash_success(self, capsys):
88
+ rc = pyfltr.main.run(["generate-shell-completion", "bash"])
89
+ assert rc == 0
90
+ captured = capsys.readouterr()
91
+ assert "_pyfltr_completions()" in captured.out
92
+
93
+ def test_powershell_success(self, capsys):
94
+ rc = pyfltr.main.run(["generate-shell-completion", "powershell"])
95
+ assert rc == 0
96
+ captured = capsys.readouterr()
97
+ assert "Register-ArgumentCompleter" in captured.out
98
+
99
+ def test_no_shell_argument(self):
100
+ rc = pyfltr.main.run(["generate-shell-completion"])
101
+ assert rc == 1
102
+
103
+ def test_invalid_shell_argument(self):
104
+ rc = pyfltr.main.run(["generate-shell-completion", "zsh"])
105
+ assert rc == 1
106
+
107
+ def test_subcommand_recognized(self):
108
+ """generate-shell-completionがサブコマンドとして認識される。"""
109
+ sub, remaining = pyfltr.main._parse_subcommand(["generate-shell-completion", "bash"])
110
+ assert sub == "generate-shell-completion"
111
+ assert remaining == ["bash"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes