Skip to content

fix(capture): let the capture fix decide for itself whether pytest needs it - #14

Merged
livingstaccato merged 1 commit into
mainfrom
fix/capture-fix-self-retires
Sep 9, 2026
Merged

livingstaccato merged 1 commit into
mainfrom
fix/capture-fix-self-retires

Conversation

@livingstaccato

Copy link
Copy Markdown
Member

The capture patch is a stopgap for a pytest defect, and it carried no way to stop.

Why

install_capture_swap_fix guarded only on the two methods still coming from _pytest.capture. That catches another patcher and nothing else. A pytest that fixes this upstream keeps the methods exactly where they are — so the guard passes, the patch lands on the corrected implementation, and the sys.stdout reassignment goes back into a capture that had stopped doing it. Silently.

Upstream has now said which way it is going: pytest-dev/pytest#14996 drew We should use a distinct stream instead of this ai confusion. Work on distinct streams is in progress. So the patch is unlikely to be adopted as written, and likely to be obsoleted by a reshaped capture. Both outcomes need this.

What changed

  • Installing measures rather than infers. A real capture is driven through swap, suspend and resume; only a pytest that loses the swap is patched. A version floor cannot answer that question — a backport, a fork, or a vendored copy each answer for themselves. A probe that cannot be driven reports no defect: patching a class whose behaviour was never established is how a fix becomes an outage.
  • The _pytest.capture import moved inside the call. It sat at module scope, and pytest_plugin imports this module while loading, so a renamed or removed class would raise ImportError inside plugin load and take every suite in the family down on a routine pytest bump. Now its absence costs the fix and nothing else.
  • The install marker is read from the class's own __dict__, so a subclass cannot report a fix that is not on the class in hand.

Tests

Four added, all failing before the change:

  • a pytest that already preserves the swap is left alone
  • a pytest that still drops it is patched
  • a pytest without the class returns False instead of raising
  • the probe gives back the stream it found

The existing seven are unchanged, including the two pytester subprocess runs that drive a real CliRunner under log_cli=true — one asserting the fix works, one asserting the failure returns with -p no:provide_testkit.

391 passed, 1 skipped single-process; ruff, ruff format and mypy strict clean.

…eds it

The patch is a stopgap for a pytest defect, and it carried no way to stop.
`install_capture_swap_fix` checked that the two methods still came from
`_pytest.capture` -- which catches another patcher and nothing else. A pytest
that fixed this upstream would keep the methods exactly where they are, so the
check passes and the patch lands on the corrected implementation, putting the
`sys.stdout` reassignment back into a capture that had stopped doing it. That
failure is silent, which is the worst shape available for it.

Installing now measures instead: drive a real capture through swap, suspend and
resume, and patch only a pytest that loses the swap. A version floor could not
answer the same question -- a backport, a fork or a vendored copy each answer
for themselves -- and a probe that cannot be driven reports no defect, because
patching a class whose behaviour was never established is how a fix becomes an
outage.

`from _pytest.capture import SysCaptureBase` also sat at module scope, and
`pytest_plugin` imports this module while loading. A pytest that renames or
drops the class would therefore raise ImportError inside plugin load and take
every suite in the family with it, on nothing more than a version bump. The
import moved inside the call, where its absence costs the fix alone.

The marker is read from the class's own `__dict__` rather than through
inheritance, so a subclass cannot report a fix that is not on the class in hand.
@livingstaccato
livingstaccato merged commit 5369de6 into main Sep 9, 2026
13 checks passed
@livingstaccato
livingstaccato deleted the fix/capture-fix-self-retires branch September 9, 2026 22:36
@livingstaccato livingstaccato mentioned this pull request Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant