pyfltr 2.2.0__tar.gz → 2.2.2__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.2.2}/CLAUDE.md +1 -10
- {pyfltr-2.2.0 → pyfltr-2.2.2}/PKG-INFO +2 -2
- {pyfltr-2.2.0 → pyfltr-2.2.2}/README.md +1 -1
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/usage.md +7 -5
- {pyfltr-2.2.0 → pyfltr-2.2.2}/mkdocs.yml +3 -2
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/cli.py +12 -2
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/command.py +1 -1
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/error_parser.py +67 -19
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/llm_output.py +64 -20
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/main.py +30 -11
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/error_parser_test.py +66 -10
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/output_format_test.py +109 -6
- pyfltr-2.2.0/.claude/settings.json +0 -13
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.claude/agents/error-parser-reviewer.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.claude/agents/tool-compat-checker.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.claude/skills/pyfltr-add-tool/SKILL.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.editorconfig +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.gitattributes +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.github/workflows/ci.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.github/workflows/docs.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.github/workflows/release.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.gitignore +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.gitmessage +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.markdownlint-cli2.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.npmrc +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.pre-commit-config.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.pylintrc +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.python-version +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.textlintrc.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.vscode/extensions.json +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/.vscode/settings.json +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/LICENSE +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/Makefile +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/cliff.toml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/.markdownlint-cli2.yaml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/development/development.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/development/index.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/configuration-tools.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/configuration.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/custom-commands.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/index.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/recommended-nonpython.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/guide/recommended.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/docs/index.md +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/mise.toml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/__init__.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/__main__.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/config.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/executor.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/precommit.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/ui.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyfltr/warnings_.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/pyproject.toml +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/__init__.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/cli_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/command_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/config_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/conftest.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/executor_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/llm_output_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/main_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/precommit_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/ui_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/tests/warnings_test.py +0 -0
- {pyfltr-2.2.0 → pyfltr-2.2.2}/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>` などを使用
|
|
@@ -16,12 +16,3 @@
|
|
|
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
18
|
- ドキュメント構成変更時は`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.2.
|
|
3
|
+
Version: 2.2.2
|
|
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
|
|
|
@@ -81,7 +81,7 @@ pyfltr generate-config
|
|
|
81
81
|
- markdownlint / textlint: `*.md`
|
|
82
82
|
- pytest: `*_test.py`
|
|
83
83
|
|
|
84
|
-
### `fast` / `run` / `ci`の動作の違いと自動修正(fixステージ)
|
|
84
|
+
### `fast` / `run` / `run-for-agent` / `ci`の動作の違いと自動修正(fixステージ)
|
|
85
85
|
|
|
86
86
|
各サブコマンドの主な違いを以下に示す(軽い順)。
|
|
87
87
|
|
|
@@ -191,7 +191,9 @@ CLIオプション`--output-format`が指定されている場合は環境変数
|
|
|
191
191
|
{"kind":"summary","total":2,"succeeded":0,"formatted":1,"failed":1,"skipped":0,"diagnostics":1,"exit":1}
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
stdoutモード(`--output-file`未指定)では、各ツールの完了時にdiagnostic行+tool行を随時書き出す。
|
|
195
|
+
ツール間の出力順は完了順となり、最後にwarning行+summary行が続く。
|
|
196
|
+
ファイル出力時(`--output-file`指定)では、`pyproject.toml`の定義順にツール単位でグルーピングし、先頭にwarning行、末尾にsummary行を配置する。
|
|
195
197
|
|
|
196
198
|
`warning`レコードの`source`は`config`(設定ファイル不在など)/`tool-resolve`(ツール解決失敗)/`file-resolver`(対象ファイル選定時)/`git`(`git check-ignore`失敗)のいずれか。
|
|
197
199
|
|
|
@@ -212,7 +214,7 @@ LLMエージェントがpyfltrを活用する基本的な流れ:
|
|
|
212
214
|
1. 全体実行でsummaryを確認する
|
|
213
215
|
|
|
214
216
|
```shell
|
|
215
|
-
pyfltr run-for-agent
|
|
217
|
+
pyfltr run-for-agent
|
|
216
218
|
```
|
|
217
219
|
|
|
218
220
|
末尾のsummary行(`"kind":"summary"`)で`failed`の有無と`diagnostics`数を確認し、問題がなければ完了する。
|
|
@@ -220,11 +222,11 @@ LLMエージェントがpyfltrを活用する基本的な流れ:
|
|
|
220
222
|
2. 問題があるツール/ファイルだけ個別に再実行する
|
|
221
223
|
|
|
222
224
|
```shell
|
|
223
|
-
pyfltr run-for-agent --commands=mypy path/to/file.py
|
|
225
|
+
pyfltr run-for-agent --commands=mypy path/to/file.py
|
|
224
226
|
```
|
|
225
227
|
|
|
226
228
|
`--commands`で特定ツールに絞ることで出力量を抑えつつ、`diagnostic`行から修正対象のファイル・行番号・メッセージを取得する。
|
|
227
|
-
|
|
229
|
+
詳細が必要な場合に限り`run`で再実行するなど、段階的に情報を掘り下げることも可能。
|
|
228
230
|
|
|
229
231
|
## pre-commitとの統合
|
|
230
232
|
|
|
@@ -38,9 +38,10 @@ 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 run-for-agent`: `pyfltr run --output-format=jsonl`のエイリアス。JSONL出力を既定にする(LLMエージェント向け)
|
|
41
42
|
- `pyfltr generate-config`: pyproject.toml用の設定雛形を出力
|
|
42
43
|
|
|
43
|
-
`--no-fix`で`run`/`fast`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
|
|
44
|
+
`--no-fix`で`run`/`fast`/`run-for-agent`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
|
|
44
45
|
|
|
45
46
|
## 対応ツール
|
|
46
47
|
|
|
@@ -79,7 +80,7 @@ plugins:
|
|
|
79
80
|
`--output-file`未指定時はstdoutへJSONLのみを書き、進捗ログやTUIは無音化される。指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
|
|
80
81
|
|
|
81
82
|
```shell
|
|
82
|
-
pyfltr run-for-agent
|
|
83
|
+
pyfltr run-for-agent
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
`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
|
|
|
@@ -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)
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
import json
|
|
9
9
|
import pathlib
|
|
10
10
|
import sys
|
|
11
|
+
import threading
|
|
11
12
|
import typing
|
|
12
13
|
|
|
13
14
|
import pyfltr.command
|
|
14
15
|
import pyfltr.config
|
|
15
16
|
import pyfltr.error_parser
|
|
16
17
|
|
|
18
|
+
# ストリーミング書き出し時に複数行(diagnostic行+tool行)をアトミックに出力するためのロック。
|
|
19
|
+
# 並列実行される linters/testers から同時にコールバックが呼ばれる可能性がある。
|
|
20
|
+
_write_lock = threading.Lock()
|
|
21
|
+
|
|
17
22
|
# failed かつ diagnostics=0 のときに tool.message として載せる生出力のトリム上限。
|
|
18
23
|
# 末尾 30 行を取り出し、さらに末尾 2000 文字に切り詰める。
|
|
19
24
|
_MESSAGE_MAX_LINES = 30
|
|
@@ -21,6 +26,22 @@ _MESSAGE_MAX_CHARS = 2000
|
|
|
21
26
|
_TRUNCATED_PREFIX = "... (truncated)\n"
|
|
22
27
|
|
|
23
28
|
|
|
29
|
+
def build_tool_lines(
|
|
30
|
+
result: pyfltr.command.CommandResult,
|
|
31
|
+
config: pyfltr.config.Config,
|
|
32
|
+
) -> list[str]:
|
|
33
|
+
"""1コマンド分のdiagnostic行+tool行をJSONL文字列のリストとして生成する。
|
|
34
|
+
|
|
35
|
+
diagnostic行はツール内でソートされる。
|
|
36
|
+
"""
|
|
37
|
+
sorted_errors = pyfltr.error_parser.sort_errors(result.errors, config.command_names)
|
|
38
|
+
lines: list[str] = []
|
|
39
|
+
for error in sorted_errors:
|
|
40
|
+
lines.append(_dump(_build_diagnostic_record(error)))
|
|
41
|
+
lines.append(_dump(_build_tool_record(result, diagnostics=len(result.errors))))
|
|
42
|
+
return lines
|
|
43
|
+
|
|
44
|
+
|
|
24
45
|
def build_lines(
|
|
25
46
|
results: list[pyfltr.command.CommandResult],
|
|
26
47
|
config: pyfltr.config.Config,
|
|
@@ -28,16 +49,15 @@ def build_lines(
|
|
|
28
49
|
exit_code: int,
|
|
29
50
|
warnings: list[dict[str, typing.Any]] | None = None,
|
|
30
51
|
) -> list[str]:
|
|
31
|
-
"""CommandResult
|
|
52
|
+
"""CommandResult群からJSONL各行を生成する。
|
|
32
53
|
|
|
33
54
|
出力順:
|
|
34
|
-
1.
|
|
35
|
-
2.
|
|
36
|
-
3.
|
|
37
|
-
4. summary 行 1 行
|
|
55
|
+
1. ``warnings``が非空ならkind="warning"行
|
|
56
|
+
2. ツール単位でdiagnostic行+tool行(``config.command_names``の定義順)
|
|
57
|
+
3. summary行1行
|
|
38
58
|
|
|
39
|
-
results
|
|
40
|
-
``warnings
|
|
59
|
+
resultsは順序を問わない。内部で``config.command_names``順にソートする。
|
|
60
|
+
``warnings``は``pyfltr.warnings_.collected_warnings()``の返り値を想定する。
|
|
41
61
|
"""
|
|
42
62
|
ordered = sorted(results, key=lambda r: _command_index(config, r.command))
|
|
43
63
|
|
|
@@ -46,20 +66,8 @@ def build_lines(
|
|
|
46
66
|
for warning in warnings or []:
|
|
47
67
|
lines.append(_dump(_build_warning_record(warning)))
|
|
48
68
|
|
|
49
|
-
all_errors: list[pyfltr.error_parser.ErrorLocation] = []
|
|
50
69
|
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)))
|
|
70
|
+
lines.extend(build_tool_lines(result, config))
|
|
63
71
|
|
|
64
72
|
lines.append(_dump(_build_summary_record(ordered, exit_code=exit_code)))
|
|
65
73
|
return lines
|
|
@@ -100,6 +108,42 @@ def write_jsonl(
|
|
|
100
108
|
f.write("\n")
|
|
101
109
|
|
|
102
110
|
|
|
111
|
+
def write_jsonl_streaming(
|
|
112
|
+
result: pyfltr.command.CommandResult,
|
|
113
|
+
config: pyfltr.config.Config,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""1コマンド分のdiagnostic行+tool行をstdoutに即時書き出す。
|
|
116
|
+
|
|
117
|
+
``_write_lock``取得下で書き出し+flushするため、並列実行されるlinters/testers
|
|
118
|
+
から呼ばれてもツール単位のグルーピングが崩れない。
|
|
119
|
+
"""
|
|
120
|
+
lines = build_tool_lines(result, config)
|
|
121
|
+
with _write_lock:
|
|
122
|
+
for line in lines:
|
|
123
|
+
sys.stdout.write(line)
|
|
124
|
+
sys.stdout.write("\n")
|
|
125
|
+
sys.stdout.flush()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_jsonl_footer(
|
|
129
|
+
results: list[pyfltr.command.CommandResult],
|
|
130
|
+
*,
|
|
131
|
+
exit_code: int,
|
|
132
|
+
warnings: list[dict[str, typing.Any]] | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""warning行+summary行をstdoutに書き出す。
|
|
135
|
+
|
|
136
|
+
``results``は``_build_summary_record()``の集計に使用する。
|
|
137
|
+
"""
|
|
138
|
+
with _write_lock:
|
|
139
|
+
for warning in warnings or []:
|
|
140
|
+
sys.stdout.write(_dump(_build_warning_record(warning)))
|
|
141
|
+
sys.stdout.write("\n")
|
|
142
|
+
sys.stdout.write(_dump(_build_summary_record(results, exit_code=exit_code)))
|
|
143
|
+
sys.stdout.write("\n")
|
|
144
|
+
sys.stdout.flush()
|
|
145
|
+
|
|
146
|
+
|
|
103
147
|
def _dump(record: dict[str, typing.Any]) -> str:
|
|
104
148
|
"""JSON 1 行にシリアライズする。ensure_ascii=False + 区切り最短化でトークン効率を稼ぐ。"""
|
|
105
149
|
return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
|
|
@@ -13,6 +13,7 @@ import typing
|
|
|
13
13
|
import pyfltr.cli
|
|
14
14
|
import pyfltr.command
|
|
15
15
|
import pyfltr.config
|
|
16
|
+
import pyfltr.llm_output
|
|
16
17
|
import pyfltr.ui
|
|
17
18
|
import pyfltr.warnings_
|
|
18
19
|
|
|
@@ -65,7 +66,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
65
66
|
"--no-fix",
|
|
66
67
|
default=False,
|
|
67
68
|
action="store_true",
|
|
68
|
-
help="run / fast サブコマンドで自動付与される fix ステージを抑止します。",
|
|
69
|
+
help="run / fast / run-for-agent サブコマンドで自動付与される fix ステージを抑止します。",
|
|
69
70
|
)
|
|
70
71
|
parser.add_argument(
|
|
71
72
|
"--stream",
|
|
@@ -403,6 +404,9 @@ def run_pipeline(
|
|
|
403
404
|
# UIの判定
|
|
404
405
|
use_ui = not args.no_ui and (args.ui or pyfltr.ui.can_use_ui())
|
|
405
406
|
|
|
407
|
+
# JSONL stdoutモード: ツール完了時に随時JSONL行を書き出す
|
|
408
|
+
jsonl_stdout = (args.output_format == "jsonl") and (args.output_file is None)
|
|
409
|
+
|
|
406
410
|
# run
|
|
407
411
|
include_fix_stage = bool(getattr(args, "include_fix_stage", False))
|
|
408
412
|
if use_ui:
|
|
@@ -411,8 +415,15 @@ def run_pipeline(
|
|
|
411
415
|
else:
|
|
412
416
|
# 非 TUI モード: 既定はバッファリング (最後にまとめて出力)、`--stream` で従来の即時出力。
|
|
413
417
|
per_command_log = bool(args.stream)
|
|
418
|
+
on_result = (lambda result: pyfltr.llm_output.write_jsonl_streaming(result, config)) if jsonl_stdout else None
|
|
414
419
|
results = pyfltr.cli.run_commands_with_cli(
|
|
415
|
-
commands,
|
|
420
|
+
commands,
|
|
421
|
+
args,
|
|
422
|
+
config,
|
|
423
|
+
all_files,
|
|
424
|
+
per_command_log=per_command_log,
|
|
425
|
+
include_fix_stage=include_fix_stage,
|
|
426
|
+
on_result=on_result,
|
|
416
427
|
)
|
|
417
428
|
returncode = 0
|
|
418
429
|
# `--stream` のときは詳細ログは既に出力済み。summary のみ表示する。
|
|
@@ -422,15 +433,23 @@ def run_pipeline(
|
|
|
422
433
|
if returncode == 0:
|
|
423
434
|
returncode = calculate_returncode(results, args.exit_zero_even_if_formatted)
|
|
424
435
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
436
|
+
if jsonl_stdout:
|
|
437
|
+
# ストリーミングモード: diagnostic行+tool行は出力済み。footer(warning+summary)のみ書き出す
|
|
438
|
+
pyfltr.llm_output.write_jsonl_footer(
|
|
439
|
+
results,
|
|
440
|
+
exit_code=returncode,
|
|
441
|
+
warnings=pyfltr.warnings_.collected_warnings(),
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
pyfltr.cli.render_results(
|
|
445
|
+
results,
|
|
446
|
+
config,
|
|
447
|
+
include_details=include_details,
|
|
448
|
+
output_format=args.output_format or "text",
|
|
449
|
+
output_file=args.output_file,
|
|
450
|
+
exit_code=returncode,
|
|
451
|
+
warnings=pyfltr.warnings_.collected_warnings(),
|
|
452
|
+
)
|
|
434
453
|
return returncode
|
|
435
454
|
|
|
436
455
|
|
|
@@ -437,26 +437,82 @@ def test_parse_typos_jsonl_fallback() -> None:
|
|
|
437
437
|
assert errors[0].line == 3
|
|
438
438
|
|
|
439
439
|
|
|
440
|
-
def
|
|
441
|
-
"""pytest --tb=
|
|
440
|
+
def test_parse_pytest_tb_short_project_frame() -> None:
|
|
441
|
+
"""pytest --tb=short: プロジェクト内フレームが選択される。"""
|
|
442
442
|
output = (
|
|
443
|
-
"============================= test session starts ==============================\n"
|
|
444
|
-
"collected 3 items\n"
|
|
445
|
-
"\n"
|
|
446
|
-
"tests/foo_test.py F.. [100%]\n"
|
|
447
|
-
"\n"
|
|
448
443
|
"================================= FAILURES =================================\n"
|
|
449
|
-
"
|
|
444
|
+
"_______________________________ test_bar ________________________________\n"
|
|
445
|
+
"tests/foo_test.py:42: in test_bar\n"
|
|
446
|
+
" result = do_something()\n"
|
|
447
|
+
"E AssertionError: assert 1 == 2\n"
|
|
450
448
|
"========================= short test summary info ==========================\n"
|
|
451
|
-
"FAILED tests/foo_test.py::test_bar - assert 1 == 2\n"
|
|
452
|
-
"========================= 1 failed, 2 passed in 0.5s =========================\n"
|
|
449
|
+
"FAILED tests/foo_test.py::test_bar - AssertionError: assert 1 == 2\n"
|
|
453
450
|
)
|
|
454
451
|
errors = pyfltr.error_parser.parse_errors("pytest", output)
|
|
455
452
|
assert len(errors) == 1
|
|
453
|
+
assert errors[0].file == "tests/foo_test.py"
|
|
456
454
|
assert errors[0].line == 42
|
|
457
455
|
assert "assert 1 == 2" in errors[0].message
|
|
458
456
|
|
|
459
457
|
|
|
458
|
+
def test_parse_pytest_tb_short_library_exception() -> None:
|
|
459
|
+
"""pytest --tb=short: ライブラリ内部で例外が発生した場合、テスト関数フレームが選択される。"""
|
|
460
|
+
output = (
|
|
461
|
+
"================================= FAILURES =================================\n"
|
|
462
|
+
"_______________________________ test_request ________________________________\n"
|
|
463
|
+
"tests/api_test.py:15: in test_request\n"
|
|
464
|
+
" client.get('/api')\n"
|
|
465
|
+
".venv/lib/python3.14/site-packages/httpx/_transports/default.py:118: in handle_request\n"
|
|
466
|
+
" resp = self._pool.handle_request(request)\n"
|
|
467
|
+
"E httpx.ConnectError: connection refused\n"
|
|
468
|
+
"========================= short test summary info ==========================\n"
|
|
469
|
+
)
|
|
470
|
+
errors = pyfltr.error_parser.parse_errors("pytest", output)
|
|
471
|
+
assert len(errors) == 1
|
|
472
|
+
assert errors[0].file == "tests/api_test.py"
|
|
473
|
+
assert errors[0].line == 15
|
|
474
|
+
assert "httpx.ConnectError" in errors[0].message
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_parse_pytest_tb_short_stdlib_exception() -> None:
|
|
478
|
+
"""pytest --tb=short: 標準ライブラリで例外が発生した場合、プロジェクト内フレームが選択される。
|
|
479
|
+
|
|
480
|
+
uv管理Pythonでは標準ライブラリが``..``始まりの相対パスで出力される。
|
|
481
|
+
"""
|
|
482
|
+
output = (
|
|
483
|
+
"================================= FAILURES =================================\n"
|
|
484
|
+
"_______________________________ test_path ________________________________\n"
|
|
485
|
+
"tests/path_test.py:10: in test_path\n"
|
|
486
|
+
" pathlib.Path('/nonexistent').resolve(strict=True)\n"
|
|
487
|
+
"../.local/share/uv/python/cpython-3.14.0-linux-x86_64/lib/python3.14/pathlib.py:881: in resolve\n"
|
|
488
|
+
" s = os.path.realpath(self, strict=strict)\n"
|
|
489
|
+
"E FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent'\n"
|
|
490
|
+
"========================= short test summary info ==========================\n"
|
|
491
|
+
)
|
|
492
|
+
errors = pyfltr.error_parser.parse_errors("pytest", output)
|
|
493
|
+
assert len(errors) == 1
|
|
494
|
+
assert errors[0].file == "tests/path_test.py"
|
|
495
|
+
assert errors[0].line == 10
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_parse_pytest_tb_short_all_external() -> None:
|
|
499
|
+
"""pytest --tb=short: 全フレームがプロジェクト外の場合、最後のフレームにフォールバック。"""
|
|
500
|
+
output = (
|
|
501
|
+
"================================= FAILURES =================================\n"
|
|
502
|
+
"_______________________________ test_ext ________________________________\n"
|
|
503
|
+
".venv/lib/python3.14/site-packages/somelib/core.py:50: in setup\n"
|
|
504
|
+
" do_init()\n"
|
|
505
|
+
".venv/lib/python3.14/site-packages/somelib/init.py:20: in do_init\n"
|
|
506
|
+
" raise RuntimeError('fail')\n"
|
|
507
|
+
"E RuntimeError: fail\n"
|
|
508
|
+
"========================= short test summary info ==========================\n"
|
|
509
|
+
)
|
|
510
|
+
errors = pyfltr.error_parser.parse_errors("pytest", output)
|
|
511
|
+
assert len(errors) == 1
|
|
512
|
+
assert errors[0].line == 20
|
|
513
|
+
assert "RuntimeError: fail" in errors[0].message
|
|
514
|
+
|
|
515
|
+
|
|
460
516
|
def test_parse_pytest_fallback() -> None:
|
|
461
517
|
"""pytest: --tb=line 形式がなければ FAILED 行にフォールバック(line=0)。"""
|
|
462
518
|
output = (
|
|
@@ -98,7 +98,7 @@ def test_build_lines_unsupported_tool_only(default_config):
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def test_build_lines_mixed_order(default_config):
|
|
101
|
-
"""diagnostic
|
|
101
|
+
"""ツール単位でdiagnostic+toolがグルーピングされ、config.command_names順に並ぶこと。"""
|
|
102
102
|
mypy_result = _make_result(
|
|
103
103
|
"mypy",
|
|
104
104
|
returncode=1,
|
|
@@ -118,11 +118,22 @@ def test_build_lines_mixed_order(default_config):
|
|
|
118
118
|
lines = pyfltr.llm_output.build_lines([mypy_result, pylint_result, black_result], default_config, exit_code=1)
|
|
119
119
|
parsed = [json.loads(line) for line in lines]
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
assert [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
# ツール単位のグルーピング: black(tool) → mypy(diagnostic, diagnostic, tool) → pylint(diagnostic, tool) → summary
|
|
122
|
+
assert [r["kind"] for r in parsed] == [
|
|
123
|
+
"tool", # black
|
|
124
|
+
"diagnostic",
|
|
125
|
+
"diagnostic",
|
|
126
|
+
"tool", # mypy
|
|
127
|
+
"diagnostic",
|
|
128
|
+
"tool", # pylint
|
|
129
|
+
"summary",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# mypy 内の diagnostic はファイル/行順
|
|
133
|
+
mypy_diagnostics = [r for r in parsed if r["kind"] == "diagnostic" and r["tool"] == "mypy"]
|
|
134
|
+
assert [(r["file"], r["line"]) for r in mypy_diagnostics] == [
|
|
135
|
+
("src/a.py", 30),
|
|
136
|
+
("src/b.py", 5),
|
|
126
137
|
]
|
|
127
138
|
|
|
128
139
|
tool_records = [r for r in parsed if r["kind"] == "tool"]
|
|
@@ -364,3 +375,95 @@ def test_run_cli_jsonl_restores_logger_state(mocker, caplog, capsys):
|
|
|
364
375
|
pyfltr.main.run(["ci", "--commands=mypy", str(pathlib.Path(__file__).parent.parent)])
|
|
365
376
|
|
|
366
377
|
assert "summary" in caplog.text
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# build_tool_lines のユニットテスト
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_build_tool_lines_with_diagnostics(default_config):
|
|
386
|
+
"""diagnostic行+tool行がツール単位でまとまること。"""
|
|
387
|
+
errors = [
|
|
388
|
+
_make_error("mypy", "src/b.py", 5, "later"),
|
|
389
|
+
_make_error("mypy", "src/a.py", 10, "earlier"),
|
|
390
|
+
]
|
|
391
|
+
result = _make_result("mypy", returncode=1, errors=errors)
|
|
392
|
+
lines = pyfltr.llm_output.build_tool_lines(result, default_config)
|
|
393
|
+
parsed = [json.loads(line) for line in lines]
|
|
394
|
+
|
|
395
|
+
assert len(parsed) == 3
|
|
396
|
+
# diagnostic行はツール内でファイル/行順にソートされる
|
|
397
|
+
assert parsed[0]["kind"] == "diagnostic"
|
|
398
|
+
assert parsed[0]["file"] == "src/a.py"
|
|
399
|
+
assert parsed[1]["kind"] == "diagnostic"
|
|
400
|
+
assert parsed[1]["file"] == "src/b.py"
|
|
401
|
+
# 最後にtool行
|
|
402
|
+
assert parsed[2]["kind"] == "tool"
|
|
403
|
+
assert parsed[2]["diagnostics"] == 2
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def test_build_tool_lines_no_diagnostics(default_config):
|
|
407
|
+
"""diagnosticがないツールはtool行のみ。"""
|
|
408
|
+
result = _make_result("black", returncode=0, command_type="formatter")
|
|
409
|
+
lines = pyfltr.llm_output.build_tool_lines(result, default_config)
|
|
410
|
+
parsed = [json.loads(line) for line in lines]
|
|
411
|
+
|
|
412
|
+
assert len(parsed) == 1
|
|
413
|
+
assert parsed[0]["kind"] == "tool"
|
|
414
|
+
assert parsed[0]["diagnostics"] == 0
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ---------------------------------------------------------------------------
|
|
418
|
+
# write_jsonl_streaming のユニットテスト
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_write_jsonl_streaming(default_config, capsys):
|
|
423
|
+
"""ストリーミング書き出しがstdoutに即時出力されること。"""
|
|
424
|
+
errors = [_make_error("mypy", "src/a.py", 10, "bad type")]
|
|
425
|
+
result = _make_result("mypy", returncode=1, errors=errors)
|
|
426
|
+
pyfltr.llm_output.write_jsonl_streaming(result, default_config)
|
|
427
|
+
|
|
428
|
+
captured = capsys.readouterr()
|
|
429
|
+
assert captured.err == ""
|
|
430
|
+
parsed = [json.loads(line) for line in captured.out.splitlines()]
|
|
431
|
+
assert len(parsed) == 2
|
|
432
|
+
assert parsed[0]["kind"] == "diagnostic"
|
|
433
|
+
assert parsed[1]["kind"] == "tool"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# write_jsonl_footer のユニットテスト
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_write_jsonl_footer_with_warnings(capsys):
|
|
442
|
+
"""warning行+summary行がstdoutに出力されること。"""
|
|
443
|
+
result = _make_result("mypy", returncode=1, errors=[_make_error("mypy", "a.py", 1, "bad")])
|
|
444
|
+
warnings = [{"source": "config", "message": "test warning"}]
|
|
445
|
+
pyfltr.llm_output.write_jsonl_footer(
|
|
446
|
+
[result],
|
|
447
|
+
exit_code=1,
|
|
448
|
+
warnings=warnings,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
captured = capsys.readouterr()
|
|
452
|
+
parsed = [json.loads(line) for line in captured.out.splitlines()]
|
|
453
|
+
assert len(parsed) == 2
|
|
454
|
+
assert parsed[0]["kind"] == "warning"
|
|
455
|
+
assert parsed[0]["msg"] == "test warning"
|
|
456
|
+
assert parsed[1]["kind"] == "summary"
|
|
457
|
+
assert parsed[1]["exit"] == 1
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_write_jsonl_footer_no_warnings(capsys):
|
|
461
|
+
"""warningがない場合はsummary行のみ。"""
|
|
462
|
+
result = _make_result("mypy", returncode=0)
|
|
463
|
+
pyfltr.llm_output.write_jsonl_footer([result], exit_code=0)
|
|
464
|
+
|
|
465
|
+
captured = capsys.readouterr()
|
|
466
|
+
parsed = [json.loads(line) for line in captured.out.splitlines()]
|
|
467
|
+
assert len(parsed) == 1
|
|
468
|
+
assert parsed[0]["kind"] == "summary"
|
|
469
|
+
assert parsed[0]["succeeded"] == 1
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|