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) <noreply@anthropic.com>
This commit is contained in:
Serhiy StorchakaandClaude Opus 5 authored and GitHub committed 2026-09-29 15:54:09 +03:00
1 parent 3b0c39cb8b
commit cbecd5edd9
6 files changed
+135 -12

No files matched your search

+21 -2
View File
@@ -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
+6
View File
@@ -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
-------
+1 -1
View File
@@ -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)
+29
View File
@@ -1393,5 +1393,34 @@ class MemoryProtectionTest(AttackProtectionTestBase, unittest.TestCase):
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()
@@ -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.
+74 -9
View File
@@ -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