https://github.com/python/cpython/commit/8b34557537ea044bdf499697b8c2309dc55b40a3
commit: 8b34557537ea044bdf499697b8c2309dc55b40a3
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-29T15:46:51+03:00
summary:

gh-157485: Add structured version info for readline (GH-157486)

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) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst
M Doc/library/readline.rst
M Doc/whatsnew/3.16.rst
M Lib/test/pythoninfo.py
M Lib/test/test_readline.py
M Modules/readline.c

diff --git a/Doc/library/readline.rst b/Doc/library/readline.rst
index 234af8d191e3e3..58471b794df203 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 0bdd55d8221402..460c2dfff9a121 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -530,6 +530,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 2885ad25d8ac6a..36b501a3a2492d 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 6af26accc13d71..f999ea6cc1affd 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 00000000000000..49dcdeae48c2bf
--- /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 e0ff7a9ffebf79..649604055ed4b3 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;
     }
 

_______________________________________________
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]

Reply via email to