From 92cd0269c38de41bcfac896be869e004607b16c4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Sep 2026 14:41:40 +0300 Subject: [PATCH] gh-157485: Add structured version info for readline Add constants readline.READLINE_VERSION_INFO, readline.readline_version_info and readline.readline_version, which provide information about the version of the Readline library in use. The private names _READLINE_VERSION, _READLINE_RUNTIME_VERSION and _READLINE_LIBRARY_VERSION are kept for backward compatibility. Co-authored-by: Claude Opus 5 (1M context) --- Doc/library/readline.rst | 32 ++++++ Doc/whatsnew/3.16.rst | 10 ++ Lib/test/pythoninfo.py | 24 +---- Lib/test/test_readline.py | 48 ++++++--- ...-09-14-11-38-23.gh-issue-157485.yAK9LN.rst | 3 + Modules/readline.c | 102 ++++++++++++++++-- 6 files changed, 176 insertions(+), 43 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst diff --git a/Doc/library/readline.rst b/Doc/library/readline.rst index 234af8d191e3e35..58471b794df2035 100644 --- a/Doc/library/readline.rst +++ b/Doc/library/readline.rst @@ -57,6 +57,38 @@ Readline library in general. .. versionadded:: 3.13 +.. data:: READLINE_VERSION_INFO + + A named tuple containing the two components of the Readline library + version that was used for building the module: *major* and *minor*. + Both values are integers. + The components can also be accessed by name, + so ``readline.READLINE_VERSION_INFO[0]`` is equivalent to + ``readline.READLINE_VERSION_INFO.major`` and so on. + This may be different from the Readline library actually used at runtime, + which is available as :const:`readline_version_info`. + + With the ``editline`` backend, this is the version of the Readline + interface emulated by libedit, not the version of libedit. + + .. versionadded:: next + +.. data:: readline_version_info + + A named tuple containing the version of the Readline library + actually loaded by the interpreter, + with the same fields as :const:`READLINE_VERSION_INFO`. + + .. versionadded:: next + +.. data:: readline_version + + The version string of the Readline library actually loaded by the + interpreter, like ``'8.3'``. + With the ``editline`` backend, this is ``'EditLine wrapper'``. + + .. versionadded:: next + Init file --------- diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 53983637f520c8a..017fbf2a9b28fce 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -514,6 +514,16 @@ pydoc (Contributed by Serhiy Storchaka in :gh:`153906`.) +readline +-------- + +* Added constants :const:`~readline.READLINE_VERSION_INFO`, + :const:`~readline.readline_version_info` and + :const:`~readline.readline_version`, which provide information about + the version of the Readline library in use. + (Contributed by Serhiy Storchaka in :gh:`157485`.) + + re -- diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index b59e2acb9376f1e..02b739078ed043d 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -427,27 +427,13 @@ def collect_readline(info_add): except ImportError: return - def format_attr(attr, value): - if isinstance(value, int): - return "%#x" % value - else: - return value - attributes = ( - "_READLINE_VERSION", - "_READLINE_RUNTIME_VERSION", - "_READLINE_LIBRARY_VERSION", + "backend", + "READLINE_VERSION_INFO", + "readline_version_info", + "readline_version", ) - copy_attributes(info_add, readline, 'readline.%s', attributes, - formatter=format_attr) - - if not hasattr(readline, "_READLINE_LIBRARY_VERSION"): - # _READLINE_LIBRARY_VERSION has been added to CPython 3.7 - doc = getattr(readline, '__doc__', '') - if 'libedit readline' in doc: - info_add('readline.library', 'libedit readline') - elif 'GNU readline' in doc: - info_add('readline.library', 'GNU readline') + copy_attributes(info_add, readline, 'readline.%s', attributes) def run_command(cmd, check=True, **kwargs): diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 6af26accc13d71a..f999ea6cc1affdd 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -19,21 +19,14 @@ # Skip tests if there is no readline module readline = import_module('readline') -if hasattr(readline, "_READLINE_LIBRARY_VERSION"): - is_editline = ("EditLine wrapper" in readline._READLINE_LIBRARY_VERSION) -else: - is_editline = readline.backend == "editline" +is_editline = readline.backend == "editline" def setUpModule(): if verbose: - # Python implementations other than CPython may not have - # these private attributes - if hasattr(readline, "_READLINE_VERSION"): - print(f"readline version: {readline._READLINE_VERSION:#x}") - print(f"readline runtime version: {readline._READLINE_RUNTIME_VERSION:#x}") - if hasattr(readline, "_READLINE_LIBRARY_VERSION"): - print(f"readline library version: {readline._READLINE_LIBRARY_VERSION!r}") + print(f"readline version: {readline.READLINE_VERSION_INFO}") + print(f"readline runtime version: {readline.readline_version_info}") + print(f"readline library version: {readline.readline_version!r}") print(f"use libedit emulation? {is_editline}") @@ -210,7 +203,36 @@ def test_append_limited_history(self): class TestReadline(unittest.TestCase): - @unittest.skipIf(readline._READLINE_VERSION < 0x0601 and not is_editline, + def _test_readline_version(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 2) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertGreaterEqual(v.major, 4) + self.assertGreaterEqual(v.minor, 0) + + def test_readline_version(self): + self._test_readline_version(readline.READLINE_VERSION_INFO) + self._test_readline_version(readline.readline_version_info) + self.assertIsInstance(readline.readline_version, str) + if not is_editline: + self.assertEqual(readline.readline_version, + '%d.%d' % readline.readline_version_info) + # Private names kept for backward compatibility. + self.assertEqual(readline._READLINE_VERSION, + readline.READLINE_VERSION_INFO.major << 8 | + readline.READLINE_VERSION_INFO.minor) + self.assertEqual(readline._READLINE_RUNTIME_VERSION, + readline.readline_version_info.major << 8 | + readline.readline_version_info.minor) + self.assertIs(readline._READLINE_LIBRARY_VERSION, + readline.readline_version) + + @unittest.skipIf(readline.READLINE_VERSION_INFO < (6, 1) and not is_editline, "not supported in this library version") def test_init(self): # Issue #19884: Ensure that the ANSI sequence "\033[1034h" is not @@ -366,7 +388,7 @@ def display(substitution, matches, longest_match_length): # See https://cnswww.cns.cwru.edu/php/chet/readline/CHANGES # - editline: history size is broken on OS X 10.11.6. # Newer versions were not tested yet. - @unittest.skipIf(readline._READLINE_VERSION < 0x600, + @unittest.skipIf(readline.READLINE_VERSION_INFO < (6, 0), "this readline version does not support history-size") @unittest.skipIf(is_editline, "editline history size configuration is broken") diff --git a/Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst b/Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst new file mode 100644 index 000000000000000..49dcdeae48c2bf1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst @@ -0,0 +1,3 @@ +Add constants :const:`readline.READLINE_VERSION_INFO`, +:const:`readline.readline_version_info` and :const:`readline.readline_version`, +which provide information about the version of the Readline library in use. diff --git a/Modules/readline.c b/Modules/readline.c index e0ff7a9ffebf794..649604055ed4b34 100644 --- a/Modules/readline.c +++ b/Modules/readline.c @@ -1802,6 +1802,96 @@ call_readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } +PyDoc_STRVAR(readline_version_info__doc__, +"readline.readline_version_info\n\ +\n\ +Readline version information as a named tuple."); + +static PyStructSequence_Field readline_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {0} +}; + +static PyStructSequence_Desc readline_version_info_desc = { + "readline.readline_version_info", /* name */ + readline_version_info__doc__, /* doc */ + readline_version_info_fields, /* fields */ + 2 +}; + +static PyObject * +make_readline_version_info(PyTypeObject *type, int number) +{ + PyObject *version; + int pos = 0; + int major = (number >> 8) & 0xff; + int minor = number & 0xff; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) +#undef SetItem + + return version; +} + +static int +add_version_constants(PyObject *m) +{ + if (PyModule_AddIntConstant(m, "_READLINE_VERSION", + RL_READLINE_VERSION) < 0) { + return -1; + } + if (PyModule_AddIntConstant(m, "_READLINE_RUNTIME_VERSION", + rl_readline_version) < 0) { + return -1; + } + PyObject *obj = PyUnicode_FromString(rl_library_version); + if (obj == NULL) { + return -1; + } + if (PyModule_AddObjectRef(m, "readline_version", obj) < 0 || + PyModule_AddObjectRef(m, "_READLINE_LIBRARY_VERSION", obj) < 0) + { + Py_DECREF(obj); + return -1; + } + Py_DECREF(obj); + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&readline_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(m, "READLINE_VERSION_INFO", + make_readline_version_info(version_type, + RL_READLINE_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(m, "readline_version_info", + make_readline_version_info(version_type, + rl_readline_version)) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} + /* Initialize the module */ PyDoc_STRVAR(doc_module, @@ -1853,17 +1943,7 @@ PyInit_readline(void) PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif - if (PyModule_AddIntConstant(m, "_READLINE_VERSION", - RL_READLINE_VERSION) < 0) { - goto error; - } - if (PyModule_AddIntConstant(m, "_READLINE_RUNTIME_VERSION", - rl_readline_version) < 0) { - goto error; - } - if (PyModule_AddStringConstant(m, "_READLINE_LIBRARY_VERSION", - rl_library_version) < 0) - { + if (add_version_constants(m) < 0) { goto error; }