Skip to content

gh-150544: Fix pdb _pyrepl_available() and make test_gen_colors skip at runtime - #150545

Open
sendaoYan wants to merge 7 commits into
python:mainfrom
sendaoYan:gh-150544
Open

sendaoYan wants to merge 7 commits into
python:mainfrom
sendaoYan:gh-150544

Conversation

@sendaoYan

@sendaoYan sendaoYan commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fix pdb._pyrepl_available() so it checks the actual stdin/stdout streams (including TTY and fileno() support), not only the import-time CAN_USE_PYREPL flag.
Pass self.stdin and self.stdout from Pdb.init into _pyrepl_available().
Update test_gen_colors and PdbTestReadlinePyREPL to evaluate pyrepl availability at test runtime instead of via import-time @unittest.skipIf.
Harden test_pyrepl_available with explicit mock streams.
Fixes gh-150544

Problem

test.test_pdb.PdbTestColorize.test_gen_colors could fail with:

Traceback (most recent call last):
  File �[35m"/home/yansendao/git/cpython-ysd/Lib/test/test_pdb.py"�[0m, line �[35m5008�[0m, in �[35mtest_gen_colors�[0m
    gen_colors = �[1;31mp.pyrepl_input.gen_colors�[0m
                 �[1;31m^^^^^^^^^^^^^^^^^^^^^^^^^�[0m
�[1;35mAttributeError�[0m: �[35m'NoneType' object has no attribute 'gen_colors'�[0m

Root cause

With --junit-xml, regrtest builds its runner with capture_output=True
(Lib/test/libregrtest/single.py), which means TextTestRunner(buffer=True).
Unittest's TestResult._setupStdout() then replaces sys.stdout (and
sys.stderr) with an io.StringIO for the duration of each test.
pdb.Pdb() picks up that stream as self.stdout, PdbPyReplInput.__init__
calls stdout.fileno(), and the resulting io.UnsupportedOperation is
swallowed by Pdb.__init__, leaving pyrepl_input = None.

sys.stdin is not modified and is still a TTY, so the import-time
@unittest.skipIf(not pdb._pyrepl_available()) keeps the test enabled even
though pyrepl cannot be used at runtime.

Changes

  • Lib/pdb.py

Extend _pyrepl_available(stdin=None, stdout=None) to:
honor PYTHON_BASIC_REPL and CAN_USE_PYREPL
verify stdin is a TTY at call time
verify both streams support fileno()
handle fake stdin objects (e.g. doctest FakeInput) without raising
Call _pyrepl_available(self.stdin, self.stdout) from Pdb.init

  • Lib/test/test_pdb.py

test_gen_colors: replace import-time @skipIf with runtime skipTest when pyrepl or pyrepl_input is unavailable
PdbTestReadlinePyREPL: use setUpClass for runtime skipping
test_pyrepl_available: test TTY and non-TTY cases using mock streams

Testing

All the cpython tests shows this PR do not cause new fails.

@sendaoYan

Copy link
Copy Markdown
Contributor Author

/touch

@gaogaotiantian

Copy link
Copy Markdown
Member

Are you able to make the repro easier and more targeted? You mean the testing system somehow swapped sys.stdin between importing the test and running the test? How do pyrepl tests deal with it?

@sendaoYan

Copy link
Copy Markdown
Contributor Author

Are you able to make the repro easier and more targeted? You mean the testing system somehow swapped sys.stdin between importing the test and running the test? How do pyrepl tests deal with it?

Thanks for pushing on this — I dug in, and my earlier description was wrong:
it's sys.stdout that changes, not sys.stdin.

The trigger is --junit-xml:

  • --junit-xml makes regrtest create its runner with capture_output=True
    (Lib/test/libregrtest/single.py, _run_suite), i.e. TextTestRunner(buffer=True).
  • With buffering, unittest.result.TestResult.startTest() calls _setupStdout(),
    which replaces sys.stdout and sys.stderr with io.StringIO() for the
    duration of every test.
  • pdb.Pdb() stores that sys.stdout as self.stdout, and
    PdbPyReplInput.__init__ calls stdout.fileno(). On a StringIO this raises
    io.UnsupportedOperation, which Pdb.__init__ swallows in
    except Exception: pass, leaving pyrepl_input = None.
  • sys.stdin is never touched and is still a real TTY, so CAN_USE_PYREPL
    (and the import-time skipIf) stays True. That's the mismatch.

Minimal repro (run it from a terminal, so stdin is a TTY):

$ ./python -c 'import io, sys, pdb; sys.stdout = io.StringIO(); print(pdb.Pdb().pyrepl_input, file=sys.__stdout__)'
None

Or with regrtest — the junit flag is the only difference:

$ ./python -m test test_pdb -m test_gen_colors                         # passes
$ ./python -m test test_pdb -m test_gen_colors --junit-xml /tmp/x.xml  # errors

On the pyrepl side: those tests don't read the ambient stdin/stdout from
inside a unittest test at all. Most of them build a Reader with a FakeConsole
and scripted events (test_pyrepl.TestPyReplOutput), and the ones that need a
real terminal run the code in a subprocess attached to a pty
(test.support.pty_helper.run_pty). test_stdin_is_tty/test_stdout_is_tty
are diagnostic only. So buffer=True doesn't affect them.

So for test_gen_colors there are two options:

  1. Keep this PR's approach: have _pyrepl_available() validate the actual stdin/stdout streams (the current io.UnsupportedOperation from StringIO is already caught by except (AttributeError, ValueError, OSError)) and skip at runtime when Pdb().pyrepl_input is None.
  2. Make the test independent of ambient stdio: gen_colors only needs the Pdb instance for the do_* lookup, so it could go through a helper that doesn't construct PdbPyReplInput at all, letting the test run under --junit-xml too.

Do you have a preference? Either way I'll correct the issue text, which
currently blames sys.stdin.

The previous wording blamed sys.stdin, but the trigger is
sys.stdout being replaced by unittest's output buffering with --junit-xml.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test.test_pdb.PdbTestColorize.test_gen_colors fails when pyrepl is unavailable at runtime

2 participants