From 7afd600052180ab4adcbd823d81485ac343ebfe9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 30 Jan 2025 12:05:15 +0100 Subject: [PATCH 1/3] Add readline workaround for libedit We had a very similar workaround before for pyreadline, which had a similar issue: - Introduced in #1281 - Removed in #8848 for #8733 and #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 #12888 Fixes #13170 --- changelog/12888.bugfix.rst | 1 + src/_pytest/capture.py | 15 +++++++++++++++ testing/test_capture.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 changelog/12888.bugfix.rst diff --git a/changelog/12888.bugfix.rst b/changelog/12888.bugfix.rst new file mode 100644 index 00000000000..635e35a11ea --- /dev/null +++ b/changelog/12888.bugfix.rst @@ -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. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 5c21590c937..8487af6a360 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -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. @@ -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") diff --git a/testing/test_capture.py b/testing/test_capture.py index 98986af6f1f..ac11938190b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -3,6 +3,7 @@ from collections.abc import Generator import contextlib +import re import io from io import UnsupportedOperation import os @@ -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" From ec011d4b14600957c6ce09c0204c94c906a3dff9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:11:12 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_capture.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index ac11938190b..cb659202ec8 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -3,10 +3,10 @@ from collections.abc import Generator import contextlib -import re import io from io import UnsupportedOperation import os +import re import subprocess import sys import textwrap @@ -1685,6 +1685,7 @@ def pytest_terminal_summary(config): capture.resume_global_capture() """) import readline + backend = getattr(readline, "backend", readline.__doc__) # added in Python 3.13 print(f"Readline backend: {backend}") From acc4b8f6ee7743a6523a44ab60df3a075b795711 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 30 Jan 2025 12:44:27 +0100 Subject: [PATCH 3/3] Handle no readline on Windows --- src/_pytest/capture.py | 5 ++++- testing/test_capture.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 8487af6a360..5b64c34e772 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -91,7 +91,10 @@ def _readline_workaround() -> None: 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 + try: + import readline # noqa: F401 + except ImportError: + pass def _windowsconsoleio_workaround(stream: TextIO) -> None: diff --git a/testing/test_capture.py b/testing/test_capture.py index cb659202ec8..a59273734c4 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1684,8 +1684,7 @@ def pytest_terminal_summary(config): capture.resume_global_capture() """) - import readline - + readline = pytest.importorskip("readline") backend = getattr(readline, "backend", readline.__doc__) # added in Python 3.13 print(f"Readline backend: {backend}")