From 56a960c755893609dc6d4ea5a441661567369627 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Sep 2026 12:04:26 +0300 Subject: [PATCH 1/2] gh-157473: Add structured version info for pyexpat 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) --- Doc/library/pyexpat.rst | 23 ++++- Doc/whatsnew/3.16.rst | 6 ++ Lib/test/pythoninfo.py | 2 +- Lib/test/test_pyexpat.py | 29 +++++++ ...-09-14-09-02-19.gh-issue-157473.LlzPyM.rst | 3 + Modules/pyexpat.c | 83 +++++++++++++++++-- 6 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst diff --git a/Doc/library/pyexpat.rst b/Doc/library/pyexpat.rst index 638b8f2862b7d19..0469beafa0e1d71 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 ``pyexpat.VERSION_INFO[0]`` is equivalent to + ``pyexpat.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 53983637f520c8a..00ff1321bcd15a3 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -758,6 +758,12 @@ xml building a tree for it. (Contributed by Serhiy Storchaka in :gh:`63102`.) +* Added constant :const:`~pyexpat.VERSION_INFO` in the + :mod:`XML parser ` module, which provides information + about the version of the Expat library that was used for building the module. + :const:`~pyexpat.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 b59e2acb9376f1e..4b96ecfe5ba6f15 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -751,7 +751,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 baa4f178427d532..6f292f042538bdf 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -1402,5 +1402,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 000000000000000..5248ae6d8752de6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst @@ -0,0 +1,3 @@ +Add constant :const:`pyexpat.VERSION_INFO` which provides information about +the version of the Expat library that was used for building the module. +:const:`pyexpat.version_info` is now a named tuple. diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 9eb0dddb2488e23..2a4595e879a5d96 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 From 6be31404622b5bef0c1d22f4aa81f48b6a1e40d4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Sep 2026 12:57:34 +0300 Subject: [PATCH 2/2] Fix references in the docs --- Doc/library/pyexpat.rst | 4 ++-- Doc/whatsnew/3.16.rst | 4 ++-- .../Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Doc/library/pyexpat.rst b/Doc/library/pyexpat.rst index 0469beafa0e1d71..cac861e293ec555 100644 --- a/Doc/library/pyexpat.rst +++ b/Doc/library/pyexpat.rst @@ -67,8 +67,8 @@ This module provides the following exception, type object and data items: *major*, *minor*, and *micro*. All values are integers. The components can also be accessed by name, - so ``pyexpat.VERSION_INFO[0]`` is equivalent to - ``pyexpat.VERSION_INFO.major`` and so on. + 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`. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 00ff1321bcd15a3..bd6bb4902f370bd 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -758,10 +758,10 @@ xml building a tree for it. (Contributed by Serhiy Storchaka in :gh:`63102`.) -* Added constant :const:`~pyexpat.VERSION_INFO` in the +* Added constant :const:`~xml.parsers.expat.VERSION_INFO` in the :mod:`XML parser ` module, which provides information about the version of the Expat library that was used for building the module. - :const:`~pyexpat.version_info` is now a named tuple. + :const:`~xml.parsers.expat.version_info` is now a named tuple. (Contributed by Serhiy Storchaka in :gh:`157473`.) zipfile 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 index 5248ae6d8752de6..8f8228e52b5597e 100644 --- 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 @@ -1,3 +1,4 @@ -Add constant :const:`pyexpat.VERSION_INFO` which provides information about -the version of the Expat library that was used for building the module. -:const:`pyexpat.version_info` is now a named tuple. +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.