Skip to content

Commit 047bd08

Browse files
committed
Make CLI execution result output opt-in
1 parent 239b097 commit 047bd08

4 files changed

Lines changed: 49 additions & 30 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ subfork execute GRAPH_ID --version v1
4747
subfork execute GRAPH_ID -o results.json
4848
```
4949

50-
`execute` waits for completion and returns JSON. Use `-o` to save results to a
51-
file; progress stays on stderr. Add `-f` / `--force` to overwrite existing output
50+
`execute` waits for completion and shows progress on stderr. Results are opt-in:
51+
use `-o results.json` to save JSON or `-o -` to print it to stdout. Add `-f` / `--force` to overwrite existing output
5252
files with `execute` or `export`. Run `subfork --help` or `subfork execute --help`
5353
for more options.
5454

‎docs/cli.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ subfork execute GRAPH_ID -o results.json
3838
subfork execute GRAPH_ID -o results.json --force
3939
```
4040

41-
Execution waits for completion and prints JSON outputs. `-o` / `--out` writes
42-
results to a file instead of stdout. Use `-f` / `--force` to overwrite an existing
41+
Execution waits for completion without printing results. `-o` / `--out` writes
42+
result JSON to a file; use `-o -` to print it to stdout. Use `-f` / `--force` to overwrite an existing
4343
file; this flag also works with `export`.
4444

4545
A yellow spinner shows active nodes. Finished nodes remain on stderr with their
46-
final status. JSON results stay on stdout, so piping works normally:
46+
final status. Opt in to JSON output for piping:
4747

4848
```bash
49-
subfork execute GRAPH_ID > results.json
49+
subfork execute GRAPH_ID -o - > results.json
5050
```
5151

5252
| Option | Behavior |
5353
| --- | --- |
54-
| `--no-wait` | Return the submission ID and status immediately |
55-
| `--raw` | Return the full execution snapshot |
54+
| `--no-wait` | Report submission ID and status on stderr immediately; use `-o` for JSON |
55+
| `--raw` | Print the full execution snapshot, or write it to the `-o` destination |
5656
| `--wait-timeout 300` | Wait up to 300 seconds; the default is 120 |
5757
| `--poll-interval 1` | Poll every second; the default is 2 |
5858

‎src/subfork/cli.py‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,11 @@ def build_parser() -> argparse.ArgumentParser:
183183
child.add_argument("--comment", default="")
184184
elif command == "execute":
185185
child.add_argument("--version", default="draft")
186-
child.add_argument("-o", "--out", help="Write result JSON to a file instead of stdout")
186+
child.add_argument(
187+
"-o",
188+
"--out",
189+
help="Write result JSON to a file, or - for stdout (default: no results)",
190+
)
187191
child.add_argument(
188192
"--no-wait", action="store_true", help="Return submission status immediately"
189193
)
@@ -314,7 +318,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
314318
if result_file is not None
315319
else (export_file if export_file != "-" else None)
316320
)
317-
if destination is not None and Path(destination).exists() and not force:
321+
if (
322+
destination is not None
323+
and destination != "-"
324+
and Path(destination).exists()
325+
and not force
326+
):
318327
raise ValueError("Output file already exists; use --force to overwrite.")
319328
with Subfork(base_url=args.base_url, timeout=args.timeout) as client:
320329
result = graph_command(client, args)
@@ -333,16 +342,25 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
333342
}
334343
else:
335344
display = result.get("outputs", {})
336-
rendered = json.dumps(display, indent=2, ensure_ascii=False) + "\n"
337-
output = getattr(args, "output", "-")
338-
if result_file is None and output == "-":
339-
sys.stdout.write(rendered)
340-
else:
341-
# Open only after the request and serialization succeed.
342-
with Path(result_file if result_file is not None else output).open(
343-
"w" if force else "x", encoding="utf-8", newline="\n"
344-
) as stream:
345-
stream.write(rendered)
345+
emit_results = args.command != "execute" or result_file is not None or args.raw
346+
if emit_results:
347+
rendered = json.dumps(display, indent=2, ensure_ascii=False) + "\n"
348+
output = result_file if result_file is not None else getattr(args, "output", "-")
349+
if output == "-":
350+
sys.stdout.write(rendered)
351+
else:
352+
# Open only after the request and serialization succeed.
353+
with Path(output).open(
354+
"w" if force else "x", encoding="utf-8", newline="\n"
355+
) as stream:
356+
stream.write(rendered)
357+
elif args.command == "execute" and args.no_wait:
358+
print(
359+
"Execution {}: {}".format(
360+
result.get("id") or result.get("execution_id"), result.get("status")
361+
),
362+
file=sys.stderr,
363+
)
346364
if failed:
347365
print(
348366
"subfork: execution did not complete successfully; use --raw for details",

‎tests/test_cli.py‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def test_removed_groups(command: str) -> None:
126126
assert result.value.code == 2
127127

128128

129-
@pytest.mark.parametrize("mode", ["default", "raw", "no-wait", "failed", "timeout"])
129+
@pytest.mark.parametrize("mode", ["default", "stdout", "raw", "no-wait", "failed", "timeout"])
130130
def test_execution_output_modes(monkeypatch: pytest.MonkeyPatch, capsys: Any, mode: str) -> None:
131131
"""Wait for results by default and preserve explicit diagnostic output modes."""
132132
seen = []
@@ -164,23 +164,24 @@ def timeout(*args: Any, **kwargs: Any) -> dict:
164164
arguments = ["execute", "g_test"]
165165
if mode in {"raw", "no-wait"}:
166166
arguments.append("--" + mode)
167+
if mode == "stdout":
168+
arguments.extend(["-o", "-"])
167169
assert cli.main(arguments) == (1 if mode in {"failed", "timeout"} else 0)
168170
captured = capsys.readouterr()
169171
assert sum(request.method == "POST" for request in seen) == 1
170172
if mode == "timeout":
171173
assert captured.out == ""
172174
assert "e_test" in captured.err and "not canceled" in captured.err
173175
return
174-
output = json.loads(captured.out)
175-
if mode == "raw":
176-
assert output == completed
177-
elif mode == "default":
178-
assert output == completed["outputs"]
176+
if mode in {"default", "no-wait", "failed"}:
177+
assert captured.out == ""
178+
if mode == "no-wait":
179+
assert "e_test" in captured.err and "running" in captured.err
180+
if mode == "failed":
181+
assert "did not complete successfully" in captured.err
179182
else:
180-
assert output == {
181-
"execution_id": "e_test",
182-
"status": "failed" if mode == "failed" else "running",
183-
}
183+
output = json.loads(captured.out)
184+
assert output == (completed if mode == "raw" else completed["outputs"])
184185
assert len(seen) == (1 if mode == "no-wait" else 2)
185186

186187

0 commit comments

Comments
 (0)