Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add readline workaround for libedit #13176

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/12888.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed broken input when using Python 3.13+ and a ``libedit`` build of Python, such as on macOS or with uv-managed Python binaries from the ``python-build-standalone`` project. This could manifest e.g. by a broken prompt when using ``Pdb``, or seeing empty inputs with manual usage of ``input()`` and suspended capturing.
18 changes: 18 additions & 0 deletions src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ def _colorama_workaround() -> None:
pass


def _readline_workaround() -> None:
"""Ensure readline is imported early so it attaches to the correct stdio handles.
This isn't a problem with the default GNU readline implementation, but in
some configurations, Python uses libedit instead (on macOS, and for prebuilt
binaries such as used by uv).
In theory this is only needed if readline.backend == "libedit", but the
workaround consists of importing readline here, so we already worked around
the issue by the time we could check if we need to.
"""
try:
import readline # noqa: F401
except ImportError:
pass


def _windowsconsoleio_workaround(stream: TextIO) -> None:
"""Workaround for Windows Unicode console handling.
Expand Down Expand Up @@ -141,6 +158,7 @@ def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
if ns.capture == "fd":
_windowsconsoleio_workaround(sys.stdout)
_colorama_workaround()
_readline_workaround()
pluginmanager = early_config.pluginmanager
capman = CaptureManager(ns.capture)
pluginmanager.register(capman, "capturemanager")
Expand Down
30 changes: 30 additions & 0 deletions testing/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io
from io import UnsupportedOperation
import os
import re
import subprocess
import sys
import textwrap
Expand Down Expand Up @@ -1666,3 +1667,32 @@ def test_logging():
)
result.stdout.no_fnmatch_line("*Captured stderr call*")
result.stdout.no_fnmatch_line("*during collection*")


def test_libedit_workaround(pytester: Pytester) -> None:
pytester.makeconftest("""
import pytest


def pytest_terminal_summary(config):
capture = config.pluginmanager.getplugin("capturemanager")
capture.suspend_global_capture(in_=True)

print("Enter 'hi'")
value = input()
print(f"value: {value!r}")

capture.resume_global_capture()
""")
readline = pytest.importorskip("readline")
backend = getattr(readline, "backend", readline.__doc__) # added in Python 3.13
print(f"Readline backend: {backend}")

child = pytester.spawn_pytest("")
child.expect(r"Enter 'hi'")
child.sendline("hi")
rest = child.read().decode("utf8")
print(rest)
match = re.search(r"^value: '(.*)'\r?$", rest, re.MULTILINE)
assert match is not None
assert match.group(1) == "hi"