You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/testing.md
+23Lines changed: 23 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -196,3 +196,26 @@ def test_prompts():
196
196
Prompts will be emulated so they write the input data to
197
197
the output stream as well. If hidden input is expected then this
198
198
does not happen.
199
+
200
+
## File Descriptors and Low-Level I/O
201
+
202
+
{class}`CliRunner` captures output by replacing `sys.stdout` and `sys.stderr` with in-memory {class}`~io.BytesIO`-backed wrappers. This is Python-level redirection: calls to {func}`~click.echo`, {func}`print`, or `sys.stdout.write()` are captured, but the wrappers have no OS-level file descriptor.
203
+
204
+
Code that calls `fileno()` on `sys.stdout` or `sys.stderr`, like {mod}`faulthandler`, {mod}`subprocess`, or C extensions, would normally crash with {exc}`io.UnsupportedOperation` inside {class}`CliRunner`.
205
+
206
+
To avoid this, {class}`CliRunner` preserves the original stream's file descriptor and exposes it via `fileno()` on the replacement wrapper.
207
+
208
+
This means:
209
+
-**Python-level writes** (`print()`, `click.echo()`, ...) are captured as usual.
210
+
-**fd-level writes** (C code writing directly to the file descriptor) go to the original terminal and are **not** captured.
211
+
212
+
This is the same trade-off that [pytest](https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html) makes with its two capture modes:
213
+
214
+
-`capsys`, which captures Python-level output, where `fileno()` raises `UnsupportedOperation` and fd-level writes are not captured.
215
+
-`capfd`, which captures fd-level output via `os.dup2()`, where `fileno()` works and fd-level writes *are* captured.
216
+
217
+
Rather than implementing a full `capfd`-style mechanism, {class}`CliRunner` takes the simpler path: expose the original `fd` so that standard library helpers keep working, while accepting that their output is not captured.
218
+
219
+
```{versionchanged} 8.3.2
220
+
`fileno()` on the redirected streams now returns the original stream's file descriptor instead of raising.
0 commit comments