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

gh-157473: Add structured version info for pyexpat (GH-157474)

Add constant pyexpat.VERSION_INFO which provides information about the
version of the Expat library that was used for building the module.

Make pyexpat.version_info a named tuple.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst
M Doc/library/pyexpat.rst
M Doc/whatsnew/3.16.rst
M Lib/test/pythoninfo.py
M Lib/test/test_pyexpat.py
M Modules/pyexpat.c

diff --git a/Doc/library/pyexpat.rst b/Doc/library/pyexpat.rst
index 638b8f2862b7d1..cac861e293ec55 100644
--- a/Doc/library/pyexpat.rst
+++ b/Doc/library/pyexpat.rst
@@ -60,10 +60,29 @@ This module provides the following exception, type object 
and data items:
    like ``'expat_2.8.4'``.
 
 
+.. data:: VERSION_INFO
+
+   A named tuple containing the three components of the Expat library
+   version that was used for building the module:
+   *major*, *minor*, and *micro*.
+   All values are integers.
+   The components can also be accessed by name,
+   so ``xml.parsers.expat.VERSION_INFO[0]`` is equivalent to
+   ``xml.parsers.expat.VERSION_INFO.major`` and so on.
+   This may be different from the Expat library actually used at runtime,
+   which is available as :const:`version_info`.
+
+   .. versionadded:: next
+
+
 .. data:: version_info
 
-   The version of the Expat library loaded by the interpreter,
-   as a tuple of three integers: major, minor and micro version.
+   A named tuple containing the version of the Expat library
+   loaded by the interpreter,
+   with the same fields as :const:`VERSION_INFO`.
+
+   .. versionchanged:: next
+      It is now a named tuple.
 
 
 .. data:: features
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 1253e30ee1d3a8..45fba68402fd3d 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -825,6 +825,12 @@ xml
   building a tree for it.
   (Contributed by Serhiy Storchaka in :gh:`63102`.)
 
+* Added constant :const:`~xml.parsers.expat.VERSION_INFO` in the
+  :mod:`XML parser <xml.parsers.expat>` module, which provides information
+  about the version of the Expat library that was used for building the module.
+  :const:`~xml.parsers.expat.version_info` is now a named tuple.
+  (Contributed by Serhiy Storchaka in :gh:`157473`.)
+
 zipfile
 -------
 
diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py
index 54f662c32edfa9..d48a149fd399f4 100644
--- a/Lib/test/pythoninfo.py
+++ b/Lib/test/pythoninfo.py
@@ -748,7 +748,7 @@ def collect_expat(info_add):
     except ImportError:
         return
 
-    attributes = ('EXPAT_VERSION',)
+    attributes = ('EXPAT_VERSION', 'VERSION_INFO', 'version_info')
     copy_attributes(info_add, expat, 'expat.%s', attributes)
 
 
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index fc5c7b311934e5..2c4048f4c9f560 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -1393,5 +1393,34 @@ def 
test_set_maximum_amplification__amplification_not_exceeded(self):
         self.assertIsNotNone(parser.Parse(payload, True))
 
 
+class VersionTest(unittest.TestCase):
+
+    def _test_version_info(self, v):
+        self.assertIsInstance(v[:], tuple)
+        self.assertEqual(len(v), 3)
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], int)
+        self.assertIsInstance(v[2], int)
+        self.assertIsInstance(v.major, int)
+        self.assertIsInstance(v.minor, int)
+        self.assertIsInstance(v.micro, int)
+        self.assertEqual(v[0], v.major)
+        self.assertEqual(v[1], v.minor)
+        self.assertEqual(v[2], v.micro)
+        self.assertGreaterEqual(v.major, 2)
+        self.assertGreaterEqual(v.minor, 0)
+        self.assertGreaterEqual(v.micro, 0)
+
+    def test_version_info(self):
+        if support.verbose:
+            print(f'EXPAT_VERSION = {expat.EXPAT_VERSION}', flush=True)
+            print(f'VERSION_INFO = {expat.VERSION_INFO}', flush=True)
+            print(f'version_info = {expat.version_info}', flush=True)
+        self._test_version_info(expat.VERSION_INFO)
+        self._test_version_info(expat.version_info)
+        self.assertEqual(expat.EXPAT_VERSION, 'expat_%d.%d.%d' % 
expat.version_info)
+        self.assertEqual(expat.VERSION_INFO[0], expat.version_info[0])
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst 
b/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst
new file mode 100644
index 00000000000000..8f8228e52b5597
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst
@@ -0,0 +1,4 @@
+Add constant :const:`xml.parsers.expat.VERSION_INFO` which provides
+information about the version of the Expat library that was used for
+building the module.
+:const:`xml.parsers.expat.version_info` is now a named tuple.
diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c
index e97f6fbc4542c8..4ab076224cdd91 100644
--- a/Modules/pyexpat.c
+++ b/Modules/pyexpat.c
@@ -2436,6 +2436,78 @@ pyexpat_capsule_destructor(PyObject *capsule)
 }
 
 
+PyDoc_STRVAR(version_info__doc__,
+"pyexpat.version_info\n\
+\n\
+Expat version information as a named tuple.");
+
+static PyStructSequence_Field version_info_fields[] = {
+    {"major", "Major release number"},
+    {"minor", "Minor release number"},
+    {"micro", "Micro release number"},
+    {0}
+};
+
+static PyStructSequence_Desc version_info_desc = {
+    "pyexpat.version_info",     /* name */
+    version_info__doc__,        /* doc */
+    version_info_fields,        /* fields */
+    3
+};
+
+static PyObject *
+make_version_info(PyTypeObject *type, int major, int minor, int micro)
+{
+    PyObject *version;
+    int pos = 0;
+
+    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))
+    SetItem(PyLong_FromLong(micro))
+#undef SetItem
+
+    return version;
+}
+
+static int
+add_version_info(PyObject *mod)
+{
+    PyTypeObject *version_type;
+    version_type = PyStructSequence_NewType(&version_info_desc);
+    if (version_type == NULL) {
+        return -1;
+    }
+    if (PyModule_Add(mod, "VERSION_INFO",
+            make_version_info(version_type, XML_MAJOR_VERSION,
+                              XML_MINOR_VERSION, XML_MICRO_VERSION)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    XML_Expat_Version info = XML_ExpatVersionInfo();
+    if (PyModule_Add(mod, "version_info",
+            make_version_info(version_type, info.major,
+                              info.minor, info.micro)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    Py_DECREF(version_type);
+    return 0;
+}
+
 static int
 pyexpat_exec(PyObject *mod)
 {
@@ -2479,15 +2551,8 @@ pyexpat_exec(PyObject *mod)
                                    XML_ExpatVersion()) < 0) {
         return -1;
     }
-    {
-        XML_Expat_Version info = XML_ExpatVersionInfo();
-        PyObject *versionInfo = Py_BuildValue("(iii)",
-                                              info.major,
-                                              info.minor,
-                                              info.micro);
-        if (PyModule_Add(mod, "version_info", versionInfo) < 0) {
-            return -1;
-        }
+    if (add_version_info(mod) < 0) {
+        return -1;
     }
     /* XXX When Expat supports some way of figuring out how it was
        compiled, this should check and set native_encoding

_______________________________________________
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