https://github.com/python/cpython/commit/ae0d2875bcfa111383e39d9ba0d24c1c317ee597
commit: ae0d2875bcfa111383e39d9ba0d24c1c317ee597
branch: main
author: Steve Dower <[email protected]>
committer: ambv <[email protected]>
date: 2026-03-10T14:58:32+01:00
summary:
gh-145035: Allows removing the _pyrepl module to completely disable the modern
REPL (GH-145159)
files:
A Misc/NEWS.d/next/Library/2026-02-23-21-28-12.gh-issue-145035.J5UjS6.rst
M Lib/_sitebuiltins.py
M Lib/asyncio/__main__.py
M Lib/pdb.py
M Lib/pydoc.py
M Lib/site.py
M Lib/test/support/__init__.py
M Lib/test/test_pyclbr.py
M Lib/test/test_pyrepl/__init__.py
M Lib/test/test_repl.py
M Modules/main.c
diff --git a/Lib/_sitebuiltins.py b/Lib/_sitebuiltins.py
index 81b36efc6c285f..84551e3546eb6e 100644
--- a/Lib/_sitebuiltins.py
+++ b/Lib/_sitebuiltins.py
@@ -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()
diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py
index 44667efc522556..0bf3bdded40200 100644
--- a/Lib/asyncio/__main__.py
+++ b/Lib/asyncio/__main__.py
@@ -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()
diff --git a/Lib/pdb.py b/Lib/pdb.py
index b5d8f827827415..7b08d2bb70183d 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -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)
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 69c83e085113c9..a1a6aad434ddf4 100644
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -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)
-
-# 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
+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
+
+ # --------------------------------------------------------- old names
+ getpager = get_pager
+ pipepager = pipe_pager
+ plainpager = plain_pager
+ tempfilepager = tempfile_pager
+ ttypager = tty_pager
+
+except ModuleNotFoundError:
+ # Minimal alternatives for cases where _pyrepl is absent.
+
+ 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
diff --git a/Lib/site.py b/Lib/site.py
index 5f09a7dd8c91c0..30015b3f26b4b3 100644
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -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:
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index d4d3c7f1aefa66..3da662b0c4d50a 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -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,
diff --git a/Lib/test/test_pyclbr.py b/Lib/test/test_pyclbr.py
index 79ef178f3807f4..b5ec41b17f793b 100644
--- a/Lib/test/test_pyclbr.py
+++ b/Lib/test/test_pyclbr.py
@@ -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')
diff --git a/Lib/test/test_pyrepl/__init__.py b/Lib/test/test_pyrepl/__init__.py
index 2f37bff6df8b4a..1534d63352cc55 100644
--- a/Lib/test/test_pyrepl/__init__.py
+++ b/Lib/test/test_pyrepl/__init__.py
@@ -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")
diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py
index 40965835bcec00..27cd125078ea69 100644
--- a/Lib/test/test_repl.py
+++ b/Lib/test/test_repl.py
@@ -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)
diff --git
a/Misc/NEWS.d/next/Library/2026-02-23-21-28-12.gh-issue-145035.J5UjS6.rst
b/Misc/NEWS.d/next/Library/2026-02-23-21-28-12.gh-issue-145035.J5UjS6.rst
new file mode 100644
index 00000000000000..b20da3b54c0415
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-23-21-28-12.gh-issue-145035.J5UjS6.rst
@@ -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.
diff --git a/Modules/main.c b/Modules/main.c
index 95ba541d98c2e8..7731fa0c7c6717 100644
--- a/Modules/main.c
+++ b/Modules/main.c
@@ -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;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]