From 0f565c24db40dbc58366337f37ccd6cfc447e9f2 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 23 Feb 2026 21:28:53 +0000 Subject: [PATCH 1/5] gh-145035: Allows removing the _pyrepl module to completely disable the modern REPL. --- Lib/_sitebuiltins.py | 12 ++++- Lib/asyncio/__main__.py | 14 ++++-- Lib/pdb.py | 8 ++- Lib/pydoc.py | 49 +++++++++++++------ Lib/site.py | 2 + Lib/test/support/__init__.py | 7 +++ Lib/test/test_pyclbr.py | 3 +- Lib/test/test_pyrepl/__init__.py | 3 ++ Lib/test/test_repl.py | 7 +++ ...-02-23-21-28-12.gh-issue-145035.J5UjS6.rst | 3 ++ Modules/main.c | 48 +++++++++++++----- 11 files changed, 121 insertions(+), 35 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-23-21-28-12.gh-issue-145035.J5UjS6.rst diff --git a/Lib/_sitebuiltins.py b/Lib/_sitebuiltins.py index 81b36efc6c285f..6931a971f2e655 100644 --- a/Lib/_sitebuiltins.py +++ b/Lib/_sitebuiltins.py @@ -65,10 +65,20 @@ 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) + self.__setup() pager = get_pager() + text = "\n".join(self.__lines) pager(text, title=self.__name) 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="") @@ -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 1b7a656551b853..9fe74442e9b68e 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -527,6 +527,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 307bac65ae50a8..f3db232a794bbb 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -3022,6 +3022,13 @@ def force_color(color: bool): import _colorize from .os_helper import EnvironmentVarGuard + if color: + try: + import _pyrepl + 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..ae15c8ab066358 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 + 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 74e48c94732565..48f4ca934f598c 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -561,13 +561,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, "", 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, "", 0, &cf); + return (run != 0); } @@ -593,14 +605,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, "", 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, "", 0, &cf); *exitcode = (run != 0); return; } From 9ebb6a2465bcfd05916e8ad036c40a5756ee8147 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 23 Feb 2026 22:18:50 +0000 Subject: [PATCH 2/5] Skip lint on unused imports --- Lib/test/support/__init__.py | 2 +- Lib/test/test_repl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index f3db232a794bbb..de193cc17a3d98 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -3024,7 +3024,7 @@ def force_color(color: bool): if color: try: - import _pyrepl + import _pyrepl # noqa: F401 except ModuleNotFoundError: # Can't force enable color without _pyrepl, so just skip. raise unittest.SkipTest("_pyrepl is missing") diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index ae15c8ab066358..27cd125078ea69 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -427,7 +427,7 @@ def test_toplevel_contextvars_async(self): p.stdin.write(user_input) user_input2 = "async def set_var(): var.set('ok')\n" try: - import _pyrepl + 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 From 44afb6a379e919fa4866b796a8c730e7bb04f284 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 23 Feb 2026 22:21:53 +0000 Subject: [PATCH 3/5] Simulate missing _pyrepl for CI --- Lib/_pyrepl/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/_pyrepl/__init__.py b/Lib/_pyrepl/__init__.py index 1693cbd0b98b74..b24a7206125d0d 100644 --- a/Lib/_pyrepl/__init__.py +++ b/Lib/_pyrepl/__init__.py @@ -17,3 +17,5 @@ # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +raise ModuleNotFoundError("Simulated absence") From f5e264aa745bed468bd41a1f6dfcc26009fd84b5 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 23 Feb 2026 22:58:53 +0000 Subject: [PATCH 4/5] Revert "Simulate missing _pyrepl for CI" This reverts commit 44afb6a379e919fa4866b796a8c730e7bb04f284. --- Lib/_pyrepl/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/_pyrepl/__init__.py b/Lib/_pyrepl/__init__.py index b24a7206125d0d..1693cbd0b98b74 100644 --- a/Lib/_pyrepl/__init__.py +++ b/Lib/_pyrepl/__init__.py @@ -17,5 +17,3 @@ # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -raise ModuleNotFoundError("Simulated absence") From d7df609d548ee2df35be8a994ebd1e3558cdcdda Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 23 Feb 2026 23:22:20 +0000 Subject: [PATCH 5/5] Fix _sitebuiltins fallbacks --- Lib/_sitebuiltins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_sitebuiltins.py b/Lib/_sitebuiltins.py index 6931a971f2e655..84551e3546eb6e 100644 --- a/Lib/_sitebuiltins.py +++ b/Lib/_sitebuiltins.py @@ -74,11 +74,11 @@ def __call__(self): def get_pager(): def _print(text, title=None): print(text) + return _print self.__setup() pager = get_pager() - text = "\n".join(self.__lines) pager(text, title=self.__name)