Skip to content

Commit

Permalink
Add readline workaround for libedit
Browse files Browse the repository at this point in the history
We had a very similar workaround before for pyreadline, which had a similar
issue:

- Introduced in pytest-dev#1281
- Removed in pytest-dev#8848 for pytest-dev#8733 and pytest-dev#8847

This technically will regress the issues above, but those issues just mean that
`import readline` is broken in general, so the user should fix it instead (by
e.g. uninstalling pyreadline).

Fixes pytest-dev#12888
Fixes pytest-dev#13170
  • Loading branch information
The-Compiler committed Jan 30, 2025
1 parent 2f36984 commit 7afd600
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 0 deletions.
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.
15 changes: 15 additions & 0 deletions src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ 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.
"""
import readline # noqa: F401


def _windowsconsoleio_workaround(stream: TextIO) -> None:
"""Workaround for Windows Unicode console handling.
Expand Down Expand Up @@ -141,6 +155,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 @@ -3,6 +3,7 @@

from collections.abc import Generator
import contextlib
import re
import io
from io import UnsupportedOperation
import os
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()
""")
import 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"

0 comments on commit 7afd600

Please sign in to comment.