Skip to content
Open
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
38 changes: 28 additions & 10 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
34 changes: 28 additions & 6 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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})
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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``.
Loading