diff --git a/Lib/pdb.py b/Lib/pdb.py index 01451f0229cacb..ee077119a03e0d 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -374,16 +374,34 @@ def get_default_backend(): return _default_backend -def _pyrepl_available(): - """return whether pdb should use _pyrepl for input""" +def _pyrepl_available(stdin=None, stdout=None): + """Return whether pdb should use _pyrepl for input. + + stdin and stdout default to sys.stdin and sys.stdout. Callers that pass + explicit streams (such as Pdb) should pass the streams they will use. + """ if os.getenv("PYTHON_BASIC_REPL"): - CAN_USE_PYREPL = False - else: - try: - from _pyrepl.main import CAN_USE_PYREPL - except ModuleNotFoundError: - CAN_USE_PYREPL = False - return CAN_USE_PYREPL + return False + try: + from _pyrepl.main import CAN_USE_PYREPL + except ModuleNotFoundError: + return False + if not CAN_USE_PYREPL: + return False + if stdin is None: + stdin = sys.stdin + if stdout is None: + stdout = sys.stdout + # CAN_USE_PYREPL is fixed at import time; streams may no longer be usable + # when this is called (e.g. in regrtest worker subprocesses). Doctests + # may replace sys.stdin with a fake object that has no fileno(). + try: + if not os.isatty(stdin.fileno()): + return False + stdout.fileno() + return True + except (AttributeError, ValueError, OSError): + return False class PdbPyReplInput: @@ -520,7 +538,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, pass self.pyrepl_input = None - if _pyrepl_available(): + if _pyrepl_available(self.stdin, self.stdout): try: self.pyrepl_input = PdbPyReplInput(self, self.stdin, self.stdout, self.prompt) except Exception: diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 410f1436ed4d20..88d2218316e593 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -25,7 +25,7 @@ from test.support.import_helper import import_module from test.support.pty_helper import run_pty, FakeInput from test.support.script_helper import kill_python -from unittest.mock import patch +from unittest.mock import Mock, patch SKIP_CORO_TESTS = False @@ -4775,14 +4775,27 @@ def foo(self): self.assertIn("The specified object 'C.foo' is not a function", stdout) def test_pyrepl_available(self): + tty_stdin = Mock() + tty_stdin.fileno.return_value = 0 + tty_stdout = Mock() + tty_stdout.fileno.return_value = 1 + with patch.dict(os.environ, {"PYTHON_BASIC_REPL": "1"}): - self.assertFalse(pdb._pyrepl_available()) + self.assertFalse(pdb._pyrepl_available(tty_stdin, tty_stdout)) + + with patch.dict(os.environ, {}, clear=True): + mod = types.ModuleType("_pyrepl.main") + mod.CAN_USE_PYREPL = True + with patch.dict("sys.modules", {"_pyrepl.main": mod}), \ + patch.object(os, "isatty", return_value=True): + self.assertTrue(pdb._pyrepl_available(tty_stdin, tty_stdout)) with patch.dict(os.environ, {}, clear=True): mod = types.ModuleType("_pyrepl.main") mod.CAN_USE_PYREPL = True - with patch.dict("sys.modules", {"_pyrepl.main": mod}): - self.assertTrue(pdb._pyrepl_available()) + with patch.dict("sys.modules", {"_pyrepl.main": mod}), \ + patch.object(os, "isatty", return_value=False): + self.assertFalse(pdb._pyrepl_available(tty_stdin, tty_stdout)) class ChecklineTests(unittest.TestCase): @@ -5002,9 +5015,14 @@ def test_stack_entry(self): p.set_trace(commands=['w', 'c']) self.assertIn("\x1b", output.getvalue()) - @unittest.skipIf(not pdb._pyrepl_available(), "pyrepl is not available") def test_gen_colors(self): + # Do not use @unittest.skipIf(pdb._pyrepl_available()): that is + # evaluated at import time, before regrtest may redirect stdin. + if not pdb._pyrepl_available(): + self.skipTest("pyrepl is not available") p = pdb.Pdb() + if p.pyrepl_input is None: + self.skipTest("pyrepl input is not available") gen_colors = p.pyrepl_input.gen_colors test_cases = [ @@ -5265,8 +5283,12 @@ def test_interact_completion(self): self.assertIn('84', output) -@unittest.skipIf(not pdb._pyrepl_available(), "pyrepl is not available") class PdbTestReadlinePyREPL(PdbTestReadline): + @classmethod + def setUpClass(cls): + if not pdb._pyrepl_available(): + raise unittest.SkipTest("pyrepl is not available") + def _run_pty(self, script, input): # Override the env to make sure pyrepl is used in this test class return super()._run_pty(script, input, env={**os.environ}) diff --git a/Misc/NEWS.d/next/Library/2026-05-28-15-46-46.gh-issue-150544.SNsZ8z.rst b/Misc/NEWS.d/next/Library/2026-05-28-15-46-46.gh-issue-150544.SNsZ8z.rst new file mode 100644 index 00000000000000..22f18affcb3ef7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-28-15-46-46.gh-issue-150544.SNsZ8z.rst @@ -0,0 +1,8 @@ +Fix ``pdb._pyrepl_available()`` to check whether the given stdin/stdout +streams support pyrepl at call time, instead of relying only on the +import-time ``_pyrepl.main.CAN_USE_PYREPL`` flag. :class:`pdb.Pdb` now +passes its streams to ``pdb._pyrepl_available()``. This prevents +:exc:`AttributeError` when ``pyrepl_input`` is ``None`` in non-TTY +environments (such as :mod:`test` worker subprocesses) and avoids failures +when ``stdin`` is replaced by doctest helpers without +:meth:`~io.IOBase.fileno`. diff --git a/Misc/NEWS.d/next/Tests/2026-05-28-15-49-57.gh-issue-150544.iN6-jy.rst b/Misc/NEWS.d/next/Tests/2026-05-28-15-49-57.gh-issue-150544.iN6-jy.rst new file mode 100644 index 00000000000000..cadbeecd984a44 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-05-28-15-49-57.gh-issue-150544.iN6-jy.rst @@ -0,0 +1,3 @@ +Fix ``test.test_pdb`` to skip pyrepl-dependent tests at runtime instead of +using import-time :func:`unittest.skipIf` on ``pdb._pyrepl_available()``. +This fixes ``PdbTestColorize.test_gen_colors`` errors under ``make test``.