Skip to content

Commit 09965bb

Browse files
committed
Make python module accept io-capture args
README.md: codetracer-python-recorder/codetracer_python_recorder/cli.py: codetracer-python-recorder/tests/python/unit/test_cli.py: examples/README.md: examples/io_capture.py: examples/stdin_capture.py: Signed-off-by: Tzanko Matev <[email protected]>
1 parent 218721a commit 09965bb

File tree

6 files changed

+142
-1
lines changed

6 files changed

+142
-1
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ All subclasses carry the same attributes, so existing handlers can migrate by ca
4848

4949
Pass `--codetracer-json-errors` (or set the policy via `configure_policy(json_errors=True)`) to stream a one-line JSON trailer on stderr. The payload includes `run_id`, `trace_id`, `error_code`, `error_kind`, `message`, and the `context` map so downstream tooling can log failures without scraping text.
5050

51+
### IO capture configuration
52+
53+
Line-aware stdout/stderr capture proxies are now enabled by default. Control them through the policy layer:
54+
55+
- CLI: `python -m codetracer_python_recorder --io-capture=off script.py` disables capture, while `--io-capture=proxies+fd` also mirrors raw file-descriptor writes.
56+
- Python: `configure_policy(io_capture_line_proxies=False)` toggles proxies, and `configure_policy(io_capture_fd_fallback=True)` enables the FD fallback.
57+
- Environment: set `CODETRACER_CAPTURE_IO=off`, `proxies`, or `proxies,fd` to match the CLI and Python helpers.
58+
5159
### Migration checklist for downstream tools
5260

5361
- Catch `RecorderError` (or a subclass) instead of `RuntimeError`.

codetracer-python-recorder/codetracer_python_recorder/cli.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ def _parse_args(argv: Sequence[str]) -> RecorderCLIConfig:
9797
action="store_true",
9898
help="Emit JSON error trailers on stderr.",
9999
)
100+
parser.add_argument(
101+
"--io-capture",
102+
choices=["off", "proxies", "proxies+fd"],
103+
help=(
104+
"Control stdout/stderr capture. Without this flag, line-aware proxies stay enabled. "
105+
"'off' disables capture, 'proxies' forces proxies without FD mirroring, "
106+
"'proxies+fd' also mirrors raw file-descriptor writes."
107+
),
108+
)
100109

101110
known, remainder = parser.parse_known_args(argv)
102111
pending: list[str] = list(remainder)
@@ -145,6 +154,19 @@ def _parse_args(argv: Sequence[str]) -> RecorderCLIConfig:
145154
policy["log_file"] = Path(known.log_file).expanduser().resolve()
146155
if known.json_errors:
147156
policy["json_errors"] = True
157+
if known.io_capture:
158+
match known.io_capture:
159+
case "off":
160+
policy["io_capture_line_proxies"] = False
161+
policy["io_capture_fd_fallback"] = False
162+
case "proxies":
163+
policy["io_capture_line_proxies"] = True
164+
policy["io_capture_fd_fallback"] = False
165+
case "proxies+fd":
166+
policy["io_capture_line_proxies"] = True
167+
policy["io_capture_fd_fallback"] = True
168+
case other: # pragma: no cover - argparse choices block this
169+
parser.error(f"unsupported io-capture mode '{other}'")
148170

149171
return RecorderCLIConfig(
150172
trace_dir=trace_dir,

codetracer-python-recorder/tests/python/unit/test_cli.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,39 @@ def test_parse_args_collects_policy_overrides(tmp_path: Path) -> None:
9696
"log_file": (tmp_path / "logs" / "recorder.log").resolve(),
9797
"json_errors": True,
9898
}
99+
100+
101+
def test_parse_args_controls_io_capture(tmp_path: Path) -> None:
102+
script = tmp_path / "entry.py"
103+
_write_script(script)
104+
105+
config = _parse_args(
106+
[
107+
"--io-capture",
108+
"off",
109+
str(script),
110+
]
111+
)
112+
113+
assert config.policy_overrides == {
114+
"io_capture_line_proxies": False,
115+
"io_capture_fd_fallback": False,
116+
}
117+
118+
119+
def test_parse_args_enables_io_capture_fd_mirroring(tmp_path: Path) -> None:
120+
script = tmp_path / "entry.py"
121+
_write_script(script)
122+
123+
config = _parse_args(
124+
[
125+
"--io-capture",
126+
"proxies+fd",
127+
str(script),
128+
]
129+
)
130+
131+
assert config.policy_overrides == {
132+
"io_capture_line_proxies": True,
133+
"io_capture_fd_fallback": True,
134+
}

examples/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ Scripts
1313
- generators_async.py: A generator, async function, and async generator.
1414
- context_and_closures.py: A context manager and a nested closure.
1515
- threading.py: Two threads invoking traced functions and joining.
16-
- imports_side_effects.py: Modulelevel side effects vs main guard.
16+
- imports_side_effects.py: Module-level side effects vs main guard.
1717
- kwargs_nested.py: Nested kwargs structure to validate structured encoding.
18+
- io_capture.py: Mix of print/write/os.write to validate IO capture proxies.
19+
- stdin_capture.py: Reads piped stdin via input(), readline(), read().
1820

1921
All scripts are deterministic and print minimal output.

examples/io_capture.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Exercise IO capture proxies with mixed stdout/stderr writes."""
2+
from __future__ import annotations
3+
4+
import os
5+
import sys
6+
7+
8+
def emit_high_level() -> None:
9+
print("stdout via print()", flush=True)
10+
sys.stdout.write("stdout via sys.stdout.write()\n")
11+
sys.stdout.flush()
12+
13+
sys.stderr.write("stderr via sys.stderr.write()\n")
14+
sys.stderr.flush()
15+
16+
17+
def emit_low_level() -> None:
18+
os.write(1, b"stdout via os.write()\n")
19+
os.write(2, b"stderr via os.write()\n")
20+
21+
22+
def main() -> None:
23+
print("Demonstrating Codetracer IO capture behaviour")
24+
emit_high_level()
25+
emit_low_level()
26+
print("Done.")
27+
28+
29+
if __name__ == "__main__":
30+
main()

examples/stdin_capture.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Exercise stdin capture with sequential reads."""
2+
from __future__ import annotations
3+
4+
import sys
5+
6+
7+
def _describe(label: str, value: str | None) -> None:
8+
if value is None:
9+
print(f"{label}: <EOF>")
10+
elif value == "":
11+
print(f"{label}: <empty string>")
12+
else:
13+
print(f"{label}: {value!r}")
14+
15+
16+
def main() -> None:
17+
if sys.stdin.isatty():
18+
print("stdin is attached to a TTY; pipe data to exercise capture.")
19+
print(
20+
"Example: printf 'first\\nsecond\\nthird' | "
21+
"python -m codetracer_python_recorder examples/stdin_capture.py"
22+
)
23+
return
24+
25+
print("Inspecting stdin via input()/readline()/read()")
26+
27+
try:
28+
first = input()
29+
except EOFError:
30+
first = None
31+
_describe("input()", first)
32+
33+
second = sys.stdin.readline()
34+
_describe("sys.stdin.readline()", second if second else None)
35+
36+
remaining = sys.stdin.read()
37+
_describe("sys.stdin.read()", remaining if remaining else None)
38+
39+
print("Done reading from stdin.")
40+
41+
42+
if __name__ == "__main__":
43+
main()

0 commit comments

Comments
 (0)