capture='fd' captures logging output from a handler holding a stale stderr reference (issue #2827). stdlib logging.StreamHandler grabs sys.stderr at configuration time. Under normal CliRunner (sys-level capture), the handler still writes to the original stream object and output is l
(tmp_path)
| 598 | |
| 599 | @needs_fd_capture |
| 600 | def test_capture_fd_logging_handler(tmp_path): |
| 601 | class="st">"""capture=&class="cm">#x27;fd' captures logging output from a handler holding a stale |
| 602 | stderr reference (issue class="cm">#2827). |
| 603 | |
| 604 | stdlib logging.StreamHandler grabs sys.stderr at configuration time. |
| 605 | Under normal CliRunner (sys-level capture), the handler still writes |
| 606 | to the original stream object and output is lost. fd-level capture |
| 607 | redirects the underlying file descriptor, so the writes are captured. |
| 608 | class="st">""" |
| 609 | import logging |
| 610 | |
| 611 | class="cm"># Create a writer backed by the real fd 2, simulating a handler |
| 612 | class="cm"># configured at import time before pytest or CliRunner replaced |
| 613 | class="cm"># sys.stderr. open(2, closefd=False) mirrors the real scenario: |
| 614 | class="cm"># the original sys.stderr is a TextIOWrapper -> BufferedWriter -> |
| 615 | class="cm"># FileIO(fd=2). |
| 616 | stale_stderr = open(2, class="st">"w", closefd=False) class="cm"># noqa: SIM115 |
| 617 | handler = logging.StreamHandler(stale_stderr) |
| 618 | handler.setFormatter(logging.Formatter(class="st">"%(message)s")) |
| 619 | |
| 620 | logger = logging.getLogger(fclass="st">"click_test_{tmp_path.name}") |
| 621 | logger.addHandler(handler) |
| 622 | logger.setLevel(logging.INFO) |
| 623 | logger.propagate = False |
| 624 | |
| 625 | @click.command() |
| 626 | def cli(): |
| 627 | logger.info(class="st">"log from stale handler") |
| 628 | click.echo(class="st">"normal echo") |
| 629 | |
| 630 | class="cm"># sys-level capture misses the log line (it bypasses sys.stderr). |
| 631 | runner_sys = CliRunner(capture=class="st">"sys") |
| 632 | result_sys = runner_sys.invoke(cli) |
| 633 | assert class="st">"normal echo" in result_sys.output |
| 634 | assert class="st">"log from stale handler" not in result_sys.output |
| 635 | |
| 636 | class="cm"># fd-level capture catches it by redirecting fd 2. |
| 637 | runner_fd = CliRunner(capture=class="st">"fd") |
| 638 | result_fd = runner_fd.invoke(cli) |
| 639 | assert class="st">"normal echo" in result_fd.output |
| 640 | assert class="st">"log from stale handler" in result_fd.output |
| 641 | |
| 642 | logger.removeHandler(handler) |
| 643 | |
| 644 | |
| 645 | @needs_fd_capture |