Coverage for /home/crpier/Projects/snektest/snektest/output.py: 64%
130 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 00:30 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 00:30 +0300
1from __future__ import annotations
3import inspect
4import pdb # noqa: T100
5import sys
6import warnings
7from collections.abc import Callable, Generator
8from contextlib import contextmanager
9from dataclasses import dataclass
10from io import StringIO
11from typing import Any, TextIO
14class StdinProxy:
15 """Proxy for sys.stdin that disables output capture when read from.
17 This allows breakpoint()/pdb to work correctly even when output is being
18 captured. When stdin is read from (e.g., pdb waiting for user input),
19 the proxy calls a callback to restore stdout/stderr so the debugger
20 can display its prompt and receive user input.
21 """
23 def __init__(
24 self,
25 original_stdin: TextIO,
26 disable_capture_callback: Callable[[], None],
27 ) -> None:
28 self._original_stdin = original_stdin
29 self._disable_capture = disable_capture_callback
31 def read(self, size: int = -1) -> str:
32 self._disable_capture()
33 return self._original_stdin.read(size)
35 def readline(self, size: int = -1) -> str:
36 self._disable_capture()
37 return self._original_stdin.readline(size)
39 def readlines(self, hint: int = -1) -> list[str]:
40 self._disable_capture()
41 return self._original_stdin.readlines(hint)
43 def __iter__(self) -> StdinProxy:
44 self._disable_capture()
45 return self
47 def __next__(self) -> str:
48 self._disable_capture()
49 return next(self._original_stdin)
51 def fileno(self) -> int:
52 return self._original_stdin.fileno()
54 def isatty(self) -> bool:
55 return self._original_stdin.isatty()
57 def close(self) -> None:
58 pass # Don't close the original stdin
60 @property
61 def closed(self) -> bool:
62 return self._original_stdin.closed
64 @property
65 def encoding(self) -> str:
66 return self._original_stdin.encoding
68 @property
69 def mode(self) -> str:
70 return self._original_stdin.mode
72 @property
73 def name(self) -> str:
74 return self._original_stdin.name
76 def __getattr__(self, name: str) -> object:
77 return getattr(self._original_stdin, name)
80@dataclass(frozen=True)
81class _OriginalSysState:
82 stdout: Any
83 stderr: Any
84 stdin: Any
85 settrace: Any
86 breakpointhook: Any
89def _make_disable_capture(
90 *,
91 system_stdout: Any,
92 system_stderr: Any,
93) -> Callable[[], None]:
94 capture_disabled = False
96 def disable_capture() -> None:
97 nonlocal capture_disabled
98 if capture_disabled:
99 return
100 sys.stdout = system_stdout
101 sys.stderr = system_stderr
102 capture_disabled = True
104 return disable_capture
107def _make_settrace_wrapper(
108 *,
109 system_settrace: Any,
110 disable_capture: Callable[[], None],
111) -> Callable[[Any], None]:
112 def settrace_wrapper(func: Any) -> None:
113 disable_capture()
114 system_settrace(func)
116 return settrace_wrapper
119def _maybe_run_inline_pdb_breakpoint(
120 *,
121 system_breakpointhook: Any,
122 caller_frame: Any,
123 args: tuple[Any, ...],
124 kwargs: dict[str, Any],
125 pdb_factory: Callable[..., Any] = pdb.Pdb,
126) -> Any:
127 if system_breakpointhook is not sys.__breakpointhook__:
128 return system_breakpointhook(*args, **kwargs)
130 if caller_frame is None:
131 return system_breakpointhook(*args, **kwargs)
132 if args:
133 return system_breakpointhook(*args, **kwargs)
135 header = kwargs.get("header")
136 commands = kwargs.get("commands")
137 if set(kwargs) - {"header", "commands"}:
138 return system_breakpointhook(*args, **kwargs)
140 debugger: pdb.Pdb = pdb_factory(
141 mode="inline",
142 backend="monitoring",
143 colorize=True,
144 )
145 if header is not None:
146 _ = debugger.message(header)
147 return debugger.set_trace(caller_frame, commands=commands)
150def _make_breakpointhook_wrapper(
151 *,
152 system_breakpointhook: Any,
153 disable_capture: Callable[[], None],
154 frame_provider: Callable[[], Any] = inspect.currentframe,
155 pdb_factory: Callable[..., Any] = pdb.Pdb,
156) -> Callable[..., Any]:
157 def breakpointhook_wrapper(*args: Any, **kwargs: Any) -> Any:
158 disable_capture()
159 sys.breakpointhook = system_breakpointhook
161 frame = frame_provider()
162 caller_frame = frame.f_back if frame else None
164 return _maybe_run_inline_pdb_breakpoint(
165 system_breakpointhook=system_breakpointhook,
166 caller_frame=caller_frame,
167 args=args,
168 kwargs=kwargs,
169 pdb_factory=pdb_factory,
170 )
172 return breakpointhook_wrapper
175def _format_warnings(warnings_list: list[warnings.WarningMessage]) -> list[str]:
176 return [
177 f"{warning.filename}:{warning.lineno}: {warning.category.__name__}: {warning.message}"
178 for warning in warnings_list
179 ]
182def _install_capture(
183 *,
184 output_buffer: StringIO,
185 system_stdin: Any,
186 disable_capture: Callable[[], None],
187 settrace_wrapper: Any,
188 breakpointhook_wrapper: Any,
189) -> None:
190 sys.stdout = output_buffer
191 sys.stderr = output_buffer
192 sys.stdin = StdinProxy(system_stdin, disable_capture)
193 sys.settrace = settrace_wrapper
194 sys.breakpointhook = breakpointhook_wrapper
197def _restore_system_state(system: _OriginalSysState) -> None:
198 sys.stdout = system.stdout
199 sys.stderr = system.stderr
200 sys.stdin = system.stdin
201 sys.settrace = system.settrace
202 sys.breakpointhook = system.breakpointhook
205@contextmanager
206def capture_output(
207 *,
208 frame_provider: Callable[[], Any] = inspect.currentframe,
209 pdb_factory: Callable[..., Any] = pdb.Pdb,
210) -> Generator[tuple[StringIO, list[str]]]:
211 """Context manager to capture stdout, stderr, and warnings.
213 If stdin is read from (e.g., by pdb/breakpoint), output capture is
214 automatically disabled so the debugger can function properly.
215 """
216 output_buffer = StringIO()
217 captured_warnings: list[str] = []
219 original_sys = _OriginalSysState(
220 stdout=sys.stdout,
221 stderr=sys.stderr,
222 stdin=sys.stdin,
223 settrace=sys.settrace,
224 breakpointhook=sys.breakpointhook,
225 )
227 disable_capture = _make_disable_capture(
228 system_stdout=original_sys.stdout,
229 system_stderr=original_sys.stderr,
230 )
231 settrace_wrapper = _make_settrace_wrapper(
232 system_settrace=original_sys.settrace,
233 disable_capture=disable_capture,
234 )
235 breakpointhook_wrapper = _make_breakpointhook_wrapper(
236 system_breakpointhook=original_sys.breakpointhook,
237 disable_capture=disable_capture,
238 frame_provider=frame_provider,
239 pdb_factory=pdb_factory,
240 )
242 _install_capture(
243 output_buffer=output_buffer,
244 system_stdin=original_sys.stdin,
245 disable_capture=disable_capture,
246 settrace_wrapper=settrace_wrapper,
247 breakpointhook_wrapper=breakpointhook_wrapper,
248 )
250 with warnings.catch_warnings(record=True) as warning_list:
251 warnings.simplefilter("always")
252 try:
253 yield output_buffer, captured_warnings
254 finally:
255 captured_warnings.extend(_format_warnings(warning_list))
256 _restore_system_state(original_sys)
259@contextmanager
260def maybe_capture_output(
261 capture: bool,
262) -> Generator[tuple[StringIO, list[str]]]:
263 """Conditionally capture output based on a flag."""
264 if capture:
265 with capture_output() as (buffer, warnings_list):
266 yield buffer, warnings_list
267 else:
268 buffer = StringIO()
269 warnings_list: list[str] = []
270 yield buffer, warnings_list