pyfltr 2.2.0__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.
- {pyfltr-2.2.0 → pyfltr-2.3.0}/CLAUDE.md +2 -10
- {pyfltr-2.2.0 → pyfltr-2.3.0}/PKG-INFO +2 -2
- {pyfltr-2.2.0 → pyfltr-2.3.0}/README.md +1 -1
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/usage.md +34 -6
- {pyfltr-2.2.0 → pyfltr-2.3.0}/mkdocs.yml +6 -7
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/cli.py +17 -3
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/command.py +1 -1
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/error_parser.py +67 -19
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/llm_output.py +101 -22
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/main.py +54 -12
- pyfltr-2.3.0/pyfltr/shell_completion.py +214 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/error_parser_test.py +66 -10
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/output_format_test.py +183 -21
- pyfltr-2.3.0/tests/shell_completion_test.py +111 -0
- pyfltr-2.2.0/.claude/settings.json +0 -13
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.claude/agents/error-parser-reviewer.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.claude/agents/tool-compat-checker.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.claude/skills/pyfltr-add-tool/SKILL.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.editorconfig +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.gitattributes +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.github/workflows/ci.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.github/workflows/docs.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.github/workflows/release.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.gitignore +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.gitmessage +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.markdownlint-cli2.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.npmrc +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.pre-commit-config.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.pylintrc +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.python-version +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.textlintrc.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.vscode/extensions.json +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/.vscode/settings.json +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/LICENSE +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/Makefile +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/cliff.toml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/.markdownlint-cli2.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/development/development.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/development/index.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/configuration-tools.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/configuration.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/custom-commands.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/index.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/recommended-nonpython.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/recommended.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/index.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/mise.toml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/__init__.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/__main__.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/config.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/executor.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/precommit.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/ui.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/warnings_.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/pyproject.toml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/__init__.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/cli_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/command_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/config_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/conftest.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/executor_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/llm_output_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/main_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/precommit_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/ui_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/warnings_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.3.0}/uv.lock +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
- `make update-actions`: GitHub Actionsのハッシュピン更新のみ(mise経由でpinact実行)
|
|
7
7
|
- テストコードは`pyfltr/xxx_.py`に対して`tests/xxx_test.py`として配置する
|
|
8
8
|
- 実行パイプラインの構造: `run_pipeline`(main.py)がTUI/非TUI分岐の最上位関数。パイプライン共通の前処理(ファイル展開など)はこの関数内でTUI起動前に実行する
|
|
9
|
-
- コミット前の検証方法: `uv run pyfltr run-for-agent
|
|
9
|
+
- コミット前の検証方法: `uv run pyfltr run-for-agent`
|
|
10
10
|
- ドキュメントなどのみの変更の場合は省略可(pre-commitで実行されるため)
|
|
11
11
|
- テストコードの単体実行なども極力 `uv run pyfltr run-for-agent <path>` を使う(pytestを直接呼び出さない)
|
|
12
12
|
- 詳細な情報などが必要な場合に限り `uv run pytest -vv <path>` などを使用
|
|
@@ -15,13 +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の役割分担」節を先に参照
|
|
19
|
-
|
|
20
|
-
## 関連ドキュメント
|
|
21
|
-
|
|
22
|
-
- @README.md
|
|
23
|
-
- @docs/index.md
|
|
24
|
-
- @docs/guide/index.md
|
|
25
|
-
- @docs/development/index.md
|
|
26
|
-
- @docs/development/development.md
|
|
27
|
-
- ドキュメント追加時は `mkdocs.yml` の `nav` を更新要
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyfltr
|
|
3
|
-
Version: 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>
|
|
@@ -54,7 +54,7 @@ Description-Content-Type: text/markdown
|
|
|
54
54
|
- 設定の集約: `pyproject.toml`に寄せた統一設定
|
|
55
55
|
- 除外指定(exclude)の書式差をツール間で吸収
|
|
56
56
|
- 自動修正系ツール(ruff format・prettierなど)を修正と失敗扱いの両立で実行
|
|
57
|
-
- LLMエージェント向けJSON Lines
|
|
57
|
+
- LLMエージェント向けJSON Lines出力(`pyfltr run-for-agent`・`PYFLTR_OUTPUT_FORMAT`環境変数・`--output-format=jsonl`)に対応
|
|
58
58
|
|
|
59
59
|
## インストール
|
|
60
60
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- 設定の集約: `pyproject.toml`に寄せた統一設定
|
|
13
13
|
- 除外指定(exclude)の書式差をツール間で吸収
|
|
14
14
|
- 自動修正系ツール(ruff format・prettierなど)を修正と失敗扱いの両立で実行
|
|
15
|
-
- LLMエージェント向けJSON Lines
|
|
15
|
+
- LLMエージェント向けJSON Lines出力(`pyfltr run-for-agent`・`PYFLTR_OUTPUT_FORMAT`環境変数・`--output-format=jsonl`)に対応
|
|
16
16
|
|
|
17
17
|
## インストール
|
|
18
18
|
|
|
@@ -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
|
対象を指定しなかった場合は、カレントディレクトリ(`.`)を指定した場合と同じ扱いとなる。
|
|
@@ -81,7 +105,7 @@ pyfltr generate-config
|
|
|
81
105
|
- markdownlint / textlint: `*.md`
|
|
82
106
|
- pytest: `*_test.py`
|
|
83
107
|
|
|
84
|
-
### `fast` / `run` / `ci`の動作の違いと自動修正(fixステージ)
|
|
108
|
+
### `fast` / `run` / `run-for-agent` / `ci`の動作の違いと自動修正(fixステージ)
|
|
85
109
|
|
|
86
110
|
各サブコマンドの主な違いを以下に示す(軽い順)。
|
|
87
111
|
|
|
@@ -176,14 +200,16 @@ CLIオプション`--output-format`が指定されている場合は環境変数
|
|
|
176
200
|
|
|
177
201
|
### jsonlスキーマ
|
|
178
202
|
|
|
179
|
-
出力は以下
|
|
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,7 +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
|
-
|
|
220
|
+
stdoutモード(`--output-file`未指定)では、先頭にheader行を出力し、各ツールの完了時にdiagnostic行+tool行を随時書き出す。
|
|
221
|
+
ツール間の出力順は完了順となり、最後にwarning行+summary行が続く。
|
|
222
|
+
ファイル出力時(`--output-file`指定)では、先頭にheader行、続いて`pyproject.toml`の定義順にツール単位でグルーピングし、先頭にwarning行、末尾にsummary行を配置する。
|
|
195
223
|
|
|
196
224
|
`warning`レコードの`source`は`config`(設定ファイル不在など)/`tool-resolve`(ツール解決失敗)/`file-resolver`(対象ファイル選定時)/`git`(`git check-ignore`失敗)のいずれか。
|
|
197
225
|
|
|
@@ -212,7 +240,7 @@ LLMエージェントがpyfltrを活用する基本的な流れ:
|
|
|
212
240
|
1. 全体実行でsummaryを確認する
|
|
213
241
|
|
|
214
242
|
```shell
|
|
215
|
-
pyfltr run-for-agent
|
|
243
|
+
pyfltr run-for-agent
|
|
216
244
|
```
|
|
217
245
|
|
|
218
246
|
末尾のsummary行(`"kind":"summary"`)で`failed`の有無と`diagnostics`数を確認し、問題がなければ完了する。
|
|
@@ -220,11 +248,11 @@ LLMエージェントがpyfltrを活用する基本的な流れ:
|
|
|
220
248
|
2. 問題があるツール/ファイルだけ個別に再実行する
|
|
221
249
|
|
|
222
250
|
```shell
|
|
223
|
-
pyfltr run-for-agent --commands=mypy path/to/file.py
|
|
251
|
+
pyfltr run-for-agent --commands=mypy path/to/file.py
|
|
224
252
|
```
|
|
225
253
|
|
|
226
254
|
`--commands`で特定ツールに絞ることで出力量を抑えつつ、`diagnostic`行から修正対象のファイル・行番号・メッセージを取得する。
|
|
227
|
-
|
|
255
|
+
詳細が必要な場合に限り`run`で再実行するなど、段階的に情報を掘り下げることも可能。
|
|
228
256
|
|
|
229
257
|
## pre-commitとの統合
|
|
230
258
|
|
|
@@ -38,9 +38,9 @@ plugins:
|
|
|
38
38
|
- `pyfltr ci`: 全チェック。formatterによる変更も失敗と判定する(CI向け)
|
|
39
39
|
- `pyfltr run`: 全チェック。fix段→formatter段→linter/tester段の順で、fix-args定義済みlinterの自動修正→formatter→残りの検査を実行する(ローカル向け)
|
|
40
40
|
- `pyfltr fast`: 軽量チェック。`run`と同じ3段構成だが、mypy/pylint/pytest等の重いツールを除外(pre-commit向け)
|
|
41
|
-
- `pyfltr
|
|
41
|
+
- `pyfltr run-for-agent`: `pyfltr run --output-format=jsonl`のエイリアス。JSONL出力を既定にする(LLMエージェント向け)
|
|
42
42
|
|
|
43
|
-
`--no-fix`で`run`/`fast`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
|
|
43
|
+
`--no-fix`で`run`/`fast`/`run-for-agent`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
|
|
44
44
|
|
|
45
45
|
## 対応ツール
|
|
46
46
|
|
|
@@ -67,19 +67,18 @@ plugins:
|
|
|
67
67
|
|
|
68
68
|
## LLMエージェント向け出力(`--output-format=jsonl`)
|
|
69
69
|
|
|
70
|
-
`--output-format=jsonl`でJSON Lines形式の構造化出力が得られる。レコードは
|
|
70
|
+
`--output-format=jsonl`でJSON Lines形式の構造化出力が得られる。レコードは5種類。
|
|
71
71
|
|
|
72
|
+
- `header`: 先頭1行。実行環境情報(`version` / `python` / `platform` / `cwd` / `commands` / `files`)
|
|
72
73
|
- `warning`: pyfltr が検出した設定・実行時の警告(`source` で発生元を識別)
|
|
73
74
|
- `diagnostic`: 1診断1行(`tool` / `file` / `line` / `col` / `rule` / `severity` / `msg`)
|
|
74
75
|
- `tool`: ツールごとの実行結果サマリ(`status` / `elapsed` / `diagnostics` / `rc`)
|
|
75
76
|
- `summary`: 最終1行の全体集計(`succeeded` / `formatted` / `failed` / `skipped` / `diagnostics` / `exit`)
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`--output-file`未指定時はstdoutへJSONLのみを書き、進捗ログやTUIは無音化される。指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
|
|
78
|
+
stdoutモード(`--output-file`未指定)の出力順は`header`→ツール完了順に`diagnostic`+`tool`→`warning`→`summary`。stdoutへJSONLのみを書き、進捗ログやTUIは無音化される。`--output-file`指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
|
|
80
79
|
|
|
81
80
|
```shell
|
|
82
|
-
pyfltr run-for-agent
|
|
81
|
+
pyfltr run-for-agent
|
|
83
82
|
```
|
|
84
83
|
|
|
85
84
|
`pyfltr run-for-agent`は`pyfltr run --output-format=jsonl`と等価のエイリアス。
|
|
@@ -29,6 +29,7 @@ def run_commands_with_cli(
|
|
|
29
29
|
*,
|
|
30
30
|
per_command_log: bool,
|
|
31
31
|
include_fix_stage: bool = False,
|
|
32
|
+
on_result: typing.Callable[[pyfltr.command.CommandResult], None] | None = None,
|
|
32
33
|
) -> list[pyfltr.command.CommandResult]:
|
|
33
34
|
"""コマンドを実行する (非 TUI)。
|
|
34
35
|
|
|
@@ -40,6 +41,9 @@ def run_commands_with_cli(
|
|
|
40
41
|
``include_fix_stage=True`` のとき、fix-args 定義済みコマンドを先に ``--fix`` 付きで
|
|
41
42
|
直列実行してから、formatter → linter/tester の順で通常実行に進む
|
|
42
43
|
(``ruff check --fix → ruff format → ruff check`` と同じ 2 段階方式の一般化)。
|
|
44
|
+
|
|
45
|
+
``on_result`` が指定されている場合、各コマンド完了時にコールバックを呼び出す。
|
|
46
|
+
JSONL stdoutモードでのストリーミング出力に使用する。
|
|
43
47
|
"""
|
|
44
48
|
results: list[pyfltr.command.CommandResult] = []
|
|
45
49
|
fixers, formatters, linters_and_testers = pyfltr.executor.split_commands_for_execution(
|
|
@@ -54,7 +58,10 @@ def run_commands_with_cli(
|
|
|
54
58
|
|
|
55
59
|
# formatters を順序実行
|
|
56
60
|
for command in formatters:
|
|
57
|
-
|
|
61
|
+
result = _run_one_command(command, args, config, all_files, per_command_log=per_command_log)
|
|
62
|
+
results.append(result)
|
|
63
|
+
if on_result is not None:
|
|
64
|
+
on_result(result)
|
|
58
65
|
|
|
59
66
|
# linters/testers を並列実行
|
|
60
67
|
if len(linters_and_testers) > 0:
|
|
@@ -64,7 +71,10 @@ def run_commands_with_cli(
|
|
|
64
71
|
for command in linters_and_testers
|
|
65
72
|
}
|
|
66
73
|
for future in concurrent.futures.as_completed(future_to_command):
|
|
67
|
-
|
|
74
|
+
result = future.result()
|
|
75
|
+
results.append(result)
|
|
76
|
+
if on_result is not None:
|
|
77
|
+
on_result(result)
|
|
68
78
|
|
|
69
79
|
return results
|
|
70
80
|
|
|
@@ -130,6 +140,8 @@ def render_results(
|
|
|
130
140
|
output_format: str = "text",
|
|
131
141
|
output_file: pathlib.Path | None = None,
|
|
132
142
|
exit_code: int = 0,
|
|
143
|
+
commands: list[str] | None = None,
|
|
144
|
+
files: int | None = None,
|
|
133
145
|
warnings: list[dict[str, typing.Any]] | None = None,
|
|
134
146
|
) -> None:
|
|
135
147
|
"""実行結果を `成功コマンド → 失敗コマンド → summary` の順でまとめて出力する。
|
|
@@ -150,7 +162,9 @@ def render_results(
|
|
|
150
162
|
warnings = warnings or []
|
|
151
163
|
|
|
152
164
|
if output_format == "jsonl":
|
|
153
|
-
pyfltr.llm_output.write_jsonl(
|
|
165
|
+
pyfltr.llm_output.write_jsonl(
|
|
166
|
+
ordered, config, exit_code=exit_code, destination=output_file, commands=commands, files=files, warnings=warnings
|
|
167
|
+
)
|
|
154
168
|
if output_file is None:
|
|
155
169
|
return
|
|
156
170
|
|
|
@@ -427,28 +427,58 @@ def _parse_typos_jsonl(output: str) -> list[ErrorLocation]:
|
|
|
427
427
|
|
|
428
428
|
|
|
429
429
|
def _parse_pytest(output: str) -> list[ErrorLocation]:
|
|
430
|
-
"""Pytest
|
|
431
|
-
# --tb=line 出力行を探す: /path/to/test.py:42: assert message
|
|
432
|
-
tb_line_re = re.compile(rf"^(?P<file>{_FILE}):(?P<line>\d+):\s+(?P<message>.+)$", re.MULTILINE)
|
|
433
|
-
# FAILURES セクション内の --tb=line 出力のみを対象にする
|
|
430
|
+
"""Pytest出力をパース。--tb=short形式のトレースバックからプロジェクト内フレームを優先的に抽出する。"""
|
|
434
431
|
failures_start = output.find("= FAILURES =")
|
|
435
432
|
summary_start = output.find("short test summary info")
|
|
436
|
-
if failures_start
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
433
|
+
if failures_start < 0:
|
|
434
|
+
return _parse_with_pattern("pytest", output, _BUILTIN_PATTERNS["pytest"])
|
|
435
|
+
|
|
436
|
+
end = summary_start if summary_start > failures_start else len(output)
|
|
437
|
+
failures_section = output[failures_start:end]
|
|
438
|
+
|
|
439
|
+
# テスト単位のブロックに分割(`_ test_name _` 区切り)
|
|
440
|
+
block_re = re.compile(r"^_+ .+ _+$", re.MULTILINE)
|
|
441
|
+
block_starts = [m.end() for m in block_re.finditer(failures_section)]
|
|
442
|
+
if not block_starts:
|
|
443
|
+
return _parse_with_pattern("pytest", output, _BUILTIN_PATTERNS["pytest"])
|
|
444
|
+
|
|
445
|
+
# フレーム行: file:line: in func_name
|
|
446
|
+
frame_re = re.compile(rf"^(?P<file>{_FILE}):(?P<line>\d+): in .+$", re.MULTILINE)
|
|
447
|
+
# エラー行: E message
|
|
448
|
+
error_re = re.compile(r"^E\s+(?P<message>.+)$", re.MULTILINE)
|
|
449
|
+
|
|
450
|
+
results: list[ErrorLocation] = []
|
|
451
|
+
for i, start in enumerate(block_starts):
|
|
452
|
+
block_end = block_starts[i + 1] if i + 1 < len(block_starts) else len(failures_section)
|
|
453
|
+
block = failures_section[start:block_end]
|
|
454
|
+
|
|
455
|
+
# フレーム群から最後のプロジェクト内フレームを選択
|
|
456
|
+
frames = list(frame_re.finditer(block))
|
|
457
|
+
if not frames:
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
chosen = frames[-1] # フォールバック: 最後のフレーム
|
|
461
|
+
for frame in reversed(frames):
|
|
462
|
+
if _is_project_path(_normalize_path(frame.group("file"))):
|
|
463
|
+
chosen = frame
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
# エラーメッセージ(先頭のE行)
|
|
467
|
+
error_match = error_re.search(block)
|
|
468
|
+
message = error_match.group("message").strip() if error_match else ""
|
|
469
|
+
|
|
470
|
+
results.append(
|
|
471
|
+
ErrorLocation(
|
|
472
|
+
file=_normalize_path(chosen.group("file")),
|
|
473
|
+
line=int(chosen.group("line")),
|
|
474
|
+
col=None,
|
|
475
|
+
command="pytest",
|
|
476
|
+
message=message,
|
|
449
477
|
)
|
|
450
|
-
|
|
451
|
-
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if results:
|
|
481
|
+
return results
|
|
452
482
|
# フォールバック: FAILED file::test_name パターン(line=0)
|
|
453
483
|
return _parse_with_pattern("pytest", output, _BUILTIN_PATTERNS["pytest"])
|
|
454
484
|
|
|
@@ -576,3 +606,21 @@ def _normalize_path(file_path: str) -> str:
|
|
|
576
606
|
return file_path
|
|
577
607
|
return result.replace("\\", "/")
|
|
578
608
|
return file_path.replace("\\", "/")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _is_project_path(normalized_path: str) -> bool:
|
|
612
|
+
"""正規化済みパスがプロジェクト内のファイルかを判定する。
|
|
613
|
+
|
|
614
|
+
以下を全て満たす場合にプロジェクト内と見なす:
|
|
615
|
+
- 相対パスである(絶対パスはcwd外 = 標準ライブラリ等)
|
|
616
|
+
- ``..``で始まらない(uv管理Pythonの標準ライブラリ等)
|
|
617
|
+
- ``.venv/``で始まらない(仮想環境内サードパーティー)
|
|
618
|
+
- ``site-packages/``・``dist-packages/``を含まない(名前の異なる仮想環境内サードパーティー)
|
|
619
|
+
"""
|
|
620
|
+
if pathlib.PurePosixPath(normalized_path).is_absolute():
|
|
621
|
+
return False
|
|
622
|
+
if normalized_path.startswith(".."):
|
|
623
|
+
return False
|
|
624
|
+
if normalized_path.startswith(".venv/"):
|
|
625
|
+
return False
|
|
626
|
+
return not ("site-packages/" in normalized_path or "dist-packages/" in normalized_path)
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
"""LLM 向け JSON Lines 出力。
|
|
2
2
|
|
|
3
3
|
`--output-format=jsonl` で呼ばれ、CommandResult 群を LLM / エージェントが
|
|
4
|
-
読みやすいフラットな JSON Lines 形式 (diagnostic / tool / summary の
|
|
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
|
|
13
|
+
import threading
|
|
11
14
|
import typing
|
|
12
15
|
|
|
13
16
|
import pyfltr.command
|
|
14
17
|
import pyfltr.config
|
|
15
18
|
import pyfltr.error_parser
|
|
16
19
|
|
|
20
|
+
# ストリーミング書き出し時に複数行(diagnostic行+tool行)をアトミックに出力するためのロック。
|
|
21
|
+
# 並列実行される linters/testers から同時にコールバックが呼ばれる可能性がある。
|
|
22
|
+
_write_lock = threading.Lock()
|
|
23
|
+
|
|
17
24
|
# failed かつ diagnostics=0 のときに tool.message として載せる生出力のトリム上限。
|
|
18
25
|
# 末尾 30 行を取り出し、さらに末尾 2000 文字に切り詰める。
|
|
19
26
|
_MESSAGE_MAX_LINES = 30
|
|
@@ -21,45 +28,54 @@ _MESSAGE_MAX_CHARS = 2000
|
|
|
21
28
|
_TRUNCATED_PREFIX = "... (truncated)\n"
|
|
22
29
|
|
|
23
30
|
|
|
31
|
+
def build_tool_lines(
|
|
32
|
+
result: pyfltr.command.CommandResult,
|
|
33
|
+
config: pyfltr.config.Config,
|
|
34
|
+
) -> list[str]:
|
|
35
|
+
"""1コマンド分のdiagnostic行+tool行をJSONL文字列のリストとして生成する。
|
|
36
|
+
|
|
37
|
+
diagnostic行はツール内でソートされる。
|
|
38
|
+
"""
|
|
39
|
+
sorted_errors = pyfltr.error_parser.sort_errors(result.errors, config.command_names)
|
|
40
|
+
lines: list[str] = []
|
|
41
|
+
for error in sorted_errors:
|
|
42
|
+
lines.append(_dump(_build_diagnostic_record(error)))
|
|
43
|
+
lines.append(_dump(_build_tool_record(result, diagnostics=len(result.errors))))
|
|
44
|
+
return lines
|
|
45
|
+
|
|
46
|
+
|
|
24
47
|
def build_lines(
|
|
25
48
|
results: list[pyfltr.command.CommandResult],
|
|
26
49
|
config: pyfltr.config.Config,
|
|
27
50
|
*,
|
|
28
51
|
exit_code: int,
|
|
52
|
+
commands: list[str] | None = None,
|
|
53
|
+
files: int | None = None,
|
|
29
54
|
warnings: list[dict[str, typing.Any]] | None = None,
|
|
30
55
|
) -> list[str]:
|
|
31
|
-
"""CommandResult
|
|
56
|
+
"""CommandResult群からJSONL各行を生成する。
|
|
32
57
|
|
|
33
58
|
出力順:
|
|
34
|
-
1.
|
|
35
|
-
2.
|
|
36
|
-
3. config.command_names
|
|
37
|
-
4. summary
|
|
59
|
+
1. ``commands``と``files``が指定されていればkind="header"行
|
|
60
|
+
2. ``warnings``が非空ならkind="warning"行
|
|
61
|
+
3. ツール単位でdiagnostic行+tool行(``config.command_names``の定義順)
|
|
62
|
+
4. summary行1行
|
|
38
63
|
|
|
39
|
-
results
|
|
40
|
-
``warnings
|
|
64
|
+
resultsは順序を問わない。内部で``config.command_names``順にソートする。
|
|
65
|
+
``warnings``は``pyfltr.warnings_.collected_warnings()``の返り値を想定する。
|
|
41
66
|
"""
|
|
42
67
|
ordered = sorted(results, key=lambda r: _command_index(config, r.command))
|
|
43
68
|
|
|
44
69
|
lines: list[str] = []
|
|
45
70
|
|
|
71
|
+
if commands is not None and files is not None:
|
|
72
|
+
lines.append(_dump(_build_header_record(commands, files)))
|
|
73
|
+
|
|
46
74
|
for warning in warnings or []:
|
|
47
75
|
lines.append(_dump(_build_warning_record(warning)))
|
|
48
76
|
|
|
49
|
-
all_errors: list[pyfltr.error_parser.ErrorLocation] = []
|
|
50
77
|
for result in ordered:
|
|
51
|
-
|
|
52
|
-
sorted_errors = pyfltr.error_parser.sort_errors(all_errors, config.command_names)
|
|
53
|
-
for error in sorted_errors:
|
|
54
|
-
lines.append(_dump(_build_diagnostic_record(error)))
|
|
55
|
-
|
|
56
|
-
diagnostic_counts: dict[str, int] = {}
|
|
57
|
-
for error in all_errors:
|
|
58
|
-
diagnostic_counts[error.command] = diagnostic_counts.get(error.command, 0) + 1
|
|
59
|
-
|
|
60
|
-
for result in ordered:
|
|
61
|
-
diagnostics = diagnostic_counts.get(result.command, 0)
|
|
62
|
-
lines.append(_dump(_build_tool_record(result, diagnostics=diagnostics)))
|
|
78
|
+
lines.extend(build_tool_lines(result, config))
|
|
63
79
|
|
|
64
80
|
lines.append(_dump(_build_summary_record(ordered, exit_code=exit_code)))
|
|
65
81
|
return lines
|
|
@@ -78,6 +94,8 @@ def write_jsonl(
|
|
|
78
94
|
*,
|
|
79
95
|
exit_code: int,
|
|
80
96
|
destination: pathlib.Path | None,
|
|
97
|
+
commands: list[str] | None = None,
|
|
98
|
+
files: int | None = None,
|
|
81
99
|
warnings: list[dict[str, typing.Any]] | None = None,
|
|
82
100
|
) -> None:
|
|
83
101
|
"""JSONL を stdout もしくは指定ファイルに書き出す。
|
|
@@ -86,7 +104,7 @@ def write_jsonl(
|
|
|
86
104
|
親ディレクトリを自動作成し、atomic write せず単純に上書きする
|
|
87
105
|
(LLM 用途の使い捨てのため)。
|
|
88
106
|
"""
|
|
89
|
-
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)
|
|
90
108
|
if destination is None:
|
|
91
109
|
for line in lines:
|
|
92
110
|
sys.stdout.write(line)
|
|
@@ -100,11 +118,72 @@ def write_jsonl(
|
|
|
100
118
|
f.write("\n")
|
|
101
119
|
|
|
102
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
|
+
|
|
132
|
+
def write_jsonl_streaming(
|
|
133
|
+
result: pyfltr.command.CommandResult,
|
|
134
|
+
config: pyfltr.config.Config,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""1コマンド分のdiagnostic行+tool行をstdoutに即時書き出す。
|
|
137
|
+
|
|
138
|
+
``_write_lock``取得下で書き出し+flushするため、並列実行されるlinters/testers
|
|
139
|
+
から呼ばれてもツール単位のグルーピングが崩れない。
|
|
140
|
+
"""
|
|
141
|
+
lines = build_tool_lines(result, config)
|
|
142
|
+
with _write_lock:
|
|
143
|
+
for line in lines:
|
|
144
|
+
sys.stdout.write(line)
|
|
145
|
+
sys.stdout.write("\n")
|
|
146
|
+
sys.stdout.flush()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def write_jsonl_footer(
|
|
150
|
+
results: list[pyfltr.command.CommandResult],
|
|
151
|
+
*,
|
|
152
|
+
exit_code: int,
|
|
153
|
+
warnings: list[dict[str, typing.Any]] | None = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""warning行+summary行をstdoutに書き出す。
|
|
156
|
+
|
|
157
|
+
``results``は``_build_summary_record()``の集計に使用する。
|
|
158
|
+
"""
|
|
159
|
+
with _write_lock:
|
|
160
|
+
for warning in warnings or []:
|
|
161
|
+
sys.stdout.write(_dump(_build_warning_record(warning)))
|
|
162
|
+
sys.stdout.write("\n")
|
|
163
|
+
sys.stdout.write(_dump(_build_summary_record(results, exit_code=exit_code)))
|
|
164
|
+
sys.stdout.write("\n")
|
|
165
|
+
sys.stdout.flush()
|
|
166
|
+
|
|
167
|
+
|
|
103
168
|
def _dump(record: dict[str, typing.Any]) -> str:
|
|
104
169
|
"""JSON 1 行にシリアライズする。ensure_ascii=False + 区切り最短化でトークン効率を稼ぐ。"""
|
|
105
170
|
return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
|
|
106
171
|
|
|
107
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
|
+
|
|
108
187
|
def _build_warning_record(entry: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
109
188
|
"""警告 dict を warning レコード dict に変換する。"""
|
|
110
189
|
return {
|