This commit is contained in:
Hugo van Kemenade 2026-03-10 16:41:02 +02:00
commit 7fbdc8fb54
15 changed files with 155 additions and 33 deletions

View file

@ -2247,17 +2247,34 @@ expression support in the :mod:`re` module).
>>> '\t'.isprintable(), '\n'.isprintable()
(False, False)
See also :meth:`isspace`.
.. method:: str.isspace()
Return ``True`` if there are only whitespace characters in the string and there is
at least one character, ``False`` otherwise.
For example:
.. doctest::
>>> ''.isspace()
False
>>> ' '.isspace()
True
>>> '\t\n'.isspace() # TAB and BREAK LINE
True
>>> '\u3000'.isspace() # IDEOGRAPHIC SPACE
True
A character is *whitespace* if in the Unicode character database
(see :mod:`unicodedata`), either its general category is ``Zs``
("Separator, space"), or its bidirectional class is one of ``WS``,
``B``, or ``S``.
See also :meth:`isprintable`.
.. method:: str.istitle()

View file

@ -65,7 +65,17 @@ def __repr__(self):
return "Type %s() to see the full %s text" % ((self.__name,)*2)
def __call__(self):
from _pyrepl.pager import get_pager
try:
from _pyrepl.pager import get_pager
except ModuleNotFoundError:
try:
from pydoc import get_pager
except ModuleNotFoundError:
def get_pager():
def _print(text, title=None):
print(text)
return _print
self.__setup()
pager = get_pager()

View file

@ -12,13 +12,16 @@
import types
import warnings
from _colorize import get_theme
from _pyrepl.console import InteractiveColoredConsole
try:
from _colorize import get_theme
from _pyrepl.console import InteractiveColoredConsole as InteractiveConsole
except ModuleNotFoundError:
from code import InteractiveConsole
from . import futures
class AsyncIOInteractiveConsole(InteractiveColoredConsole):
class AsyncIOInteractiveConsole(InteractiveConsole):
def __init__(self, locals, loop):
super().__init__(locals, filename="<stdin>")
@ -185,7 +188,10 @@ def interrupt(self) -> None:
if os.getenv('PYTHON_BASIC_REPL'):
CAN_USE_PYREPL = False
else:
from _pyrepl.main import CAN_USE_PYREPL
try:
from _pyrepl.main import CAN_USE_PYREPL
except ModuleNotFoundError:
CAN_USE_PYREPL = False
return_code = 0
loop = asyncio.new_event_loop()

View file

@ -97,12 +97,16 @@
import selectors
import threading
import _colorize
import _pyrepl.utils
from contextlib import ExitStack, closing, contextmanager
from types import CodeType
from warnings import deprecated
try:
import _pyrepl.utils
except ModuleNotFoundError:
_pyrepl = None
class Restart(Exception):
"""Causes a debugger to be restarted for the debugged python program."""
@ -1097,7 +1101,7 @@ def handle_command_def(self, line):
return False
def _colorize_code(self, code):
if self.colorize:
if self.colorize and _pyrepl:
colors = list(_pyrepl.utils.gen_colors(code))
chars, _ = _pyrepl.utils.disp_str(code, colors=colors, force_color=True)
code = "".join(chars)

View file

@ -78,20 +78,41 @@ class or function within a module or module in a package. If the
from reprlib import Repr
from traceback import format_exception_only
from _pyrepl.pager import (get_pager, pipe_pager,
plain_pager, tempfile_pager, tty_pager)
try:
from _pyrepl.pager import (get_pager, pipe_pager,
plain_pager, tempfile_pager, tty_pager)
# Expose plain() as pydoc.plain()
from _pyrepl.pager import plain # noqa: F401
# Expose plain() as pydoc.plain()
from _pyrepl.pager import plain # noqa: F401
# --------------------------------------------------------- old names
getpager = get_pager
pipepager = pipe_pager
plainpager = plain_pager
tempfilepager = tempfile_pager
ttypager = tty_pager
# --------------------------------------------------------- old names
except ModuleNotFoundError:
# Minimal alternatives for cases where _pyrepl is absent.
getpager = get_pager
pipepager = pipe_pager
plainpager = plain_pager
tempfilepager = tempfile_pager
ttypager = tty_pager
def plain(text: str) -> str:
"""Remove boldface formatting from text."""
return re.sub('.\b', '', text)
def plain_pager(text: str, title: str = '') -> None:
"""Simply print unformatted text. This is the ultimate fallback."""
encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8'
text = text.encode(encoding, 'backslashreplace').decode(encoding)
text = plain(text)
sys.stdout.write(text)
def get_pager():
"""Unconditionally return the plain pager, since _pyrepl is absent."""
return plain_pager
# --------------------------------------------------------- old names
getpager = get_pager
plainpager = plain_pager
# --------------------------------------------------------- common routines

View file

@ -529,6 +529,8 @@ def register_readline():
import _pyrepl.unix_console
console_errors = _pyrepl.unix_console._error
from _pyrepl.main import CAN_USE_PYREPL
except ModuleNotFoundError:
CAN_USE_PYREPL = False
finally:
sys.path = original_path
except ImportError:

View file

@ -3023,6 +3023,13 @@ def force_color(color: bool):
import _colorize
from .os_helper import EnvironmentVarGuard
if color:
try:
import _pyrepl # noqa: F401
except ModuleNotFoundError:
# Can't force enable color without _pyrepl, so just skip.
raise unittest.SkipTest("_pyrepl is missing")
with (
swap_attr(_colorize, "can_colorize", lambda *, file=None: color),
EnvironmentVarGuard() as env,

View file

@ -204,5 +204,20 @@ def default_factory():
self.assertEqual(test_dict[key], 2)
self.assertEqual(count, 2)
def test_repr_recursive_factory(self):
# gh-145492: defaultdict.__repr__ should not cause infinite recursion
# when the factory's __repr__ calls repr() on the defaultdict.
class ProblematicFactory:
def __call__(self):
return {}
def __repr__(self):
repr(dd)
return "ProblematicFactory()"
dd = defaultdict(ProblematicFactory())
# Should not raise RecursionError
r = repr(dd)
self.assertIn('ProblematicFactory()', r)
if __name__ == "__main__":
unittest.main()

View file

@ -252,7 +252,8 @@ def test_others(self):
ignore=('Union', '_ModuleTarget', '_ScriptTarget', '_ZipTarget', 'curframe_locals',
'_InteractState', 'rlcompleter'),
)
cm('pydoc', ignore=('input', 'output',)) # properties
cm('pydoc', ignore=('input', 'output', # properties
'getpager', 'plainpager', )) # aliases
# Tests for modules inside packages
cm('email.parser')

View file

@ -3,6 +3,9 @@
from test.support import import_helper, load_package_tests
import_helper.import_module("_pyrepl")
if sys.platform != "win32":
import_helper.import_module("termios")

View file

@ -426,6 +426,13 @@ def test_toplevel_contextvars_async(self):
p = spawn_asyncio_repl()
p.stdin.write(user_input)
user_input2 = "async def set_var(): var.set('ok')\n"
try:
import _pyrepl # noqa: F401
except ModuleNotFoundError:
# If we're going to be forced into the regular REPL, then we need an
# extra newline here. Omit it by default to catch any breakage to
# the new REPL's behavior.
user_input2 += "\n"
p.stdin.write(user_input2)
user_input3 = "await set_var()\n"
p.stdin.write(user_input3)

View file

@ -0,0 +1,3 @@
Allows omitting the internal library ``_pyrepl`` with limited loss of
functionality. This allows complete removal of the modern REPL, which is an
unsupported configuration, but still desirable for some distributions.

View file

@ -0,0 +1,3 @@
Fix infinite recursion in :class:`collections.defaultdict` ``__repr__``
when a ``defaultdict`` contains itself. Based on analysis by KowalskiThomas
in :gh:`145492`.

View file

@ -2385,9 +2385,10 @@ defdict_repr(PyObject *op)
}
defrepr = PyUnicode_FromString("...");
}
else
else {
defrepr = PyObject_Repr(dd->default_factory);
Py_ReprLeave(dd->default_factory);
Py_ReprLeave(dd->default_factory);
}
}
if (defrepr == NULL) {
Py_DECREF(baserepr);

View file

@ -562,13 +562,25 @@ pymain_run_stdin(PyConfig *config)
return pymain_exit_err_print();
}
if (!isatty(fileno(stdin))
|| _Py_GetEnv(config->use_environment, "PYTHON_BASIC_REPL")) {
PyCompilerFlags cf = _PyCompilerFlags_INIT;
int run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, &cf);
return (run != 0);
int run;
if (isatty(fileno(stdin))
&& !_Py_GetEnv(config->use_environment, "PYTHON_BASIC_REPL")) {
PyObject *pyrepl = PyImport_ImportModule("_pyrepl");
if (pyrepl != NULL) {
run = pymain_start_pyrepl(0);
Py_DECREF(pyrepl);
return run;
}
if (!PyErr_ExceptionMatches(PyExc_ModuleNotFoundError)) {
fprintf(stderr, "Could not import _pyrepl.main\n");
return pymain_exit_err_print();
}
PyErr_Clear();
}
return pymain_start_pyrepl(0);
PyCompilerFlags cf = _PyCompilerFlags_INIT;
run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, &cf);
return (run != 0);
}
@ -594,14 +606,24 @@ pymain_repl(PyConfig *config, int *exitcode)
return;
}
if (!isatty(fileno(stdin))
|| _Py_GetEnv(config->use_environment, "PYTHON_BASIC_REPL")) {
PyCompilerFlags cf = _PyCompilerFlags_INIT;
int run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, &cf);
*exitcode = (run != 0);
return;
if (isatty(fileno(stdin))
&& !_Py_GetEnv(config->use_environment, "PYTHON_BASIC_REPL")) {
PyObject *pyrepl = PyImport_ImportModule("_pyrepl");
if (pyrepl != NULL) {
int run = pymain_start_pyrepl(1);
*exitcode = (run != 0);
Py_DECREF(pyrepl);
return;
}
if (!PyErr_ExceptionMatches(PyExc_ModuleNotFoundError)) {
PyErr_Clear();
fprintf(stderr, "Could not import _pyrepl.main\n");
return;
}
PyErr_Clear();
}
int run = pymain_start_pyrepl(1);
PyCompilerFlags cf = _PyCompilerFlags_INIT;
int run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, &cf);
*exitcode = (run != 0);
return;
}