Skip to content

Commit 56a960c

Browse files
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) <noreply@anthropic.com>
1 parent a844747 commit 56a960c

6 files changed

Lines changed: 134 additions & 12 deletions

File tree

Doc/library/pyexpat.rst

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,29 @@ This module provides the following exception, type object and data items:
6060
like ``'expat_2.8.4'``.
6161

6262

63+
.. data:: VERSION_INFO
64+
65+
A named tuple containing the three components of the Expat library
66+
version that was used for building the module:
67+
*major*, *minor*, and *micro*.
68+
All values are integers.
69+
The components can also be accessed by name,
70+
so ``pyexpat.VERSION_INFO[0]`` is equivalent to
71+
``pyexpat.VERSION_INFO.major`` and so on.
72+
This may be different from the Expat library actually used at runtime,
73+
which is available as :const:`version_info`.
74+
75+
.. versionadded:: next
76+
77+
6378
.. data:: version_info
6479

65-
The version of the Expat library loaded by the interpreter,
66-
as a tuple of three integers: major, minor and micro version.
80+
A named tuple containing the version of the Expat library
81+
loaded by the interpreter,
82+
with the same fields as :const:`VERSION_INFO`.
83+
84+
.. versionchanged:: next
85+
It is now a named tuple.
6786

6887

6988
.. data:: features

Doc/whatsnew/3.16.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,12 @@ xml
758758
building a tree for it.
759759
(Contributed by Serhiy Storchaka in :gh:`63102`.)
760760

761+
* Added constant :const:`~pyexpat.VERSION_INFO` in the
762+
:mod:`XML parser <xml.parsers.expat>` module, which provides information
763+
about the version of the Expat library that was used for building the module.
764+
:const:`~pyexpat.version_info` is now a named tuple.
765+
(Contributed by Serhiy Storchaka in :gh:`157473`.)
766+
761767
zipfile
762768
-------
763769

Lib/test/pythoninfo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ def collect_expat(info_add):
751751
except ImportError:
752752
return
753753

754-
attributes = ('EXPAT_VERSION',)
754+
attributes = ('EXPAT_VERSION', 'VERSION_INFO', 'version_info')
755755
copy_attributes(info_add, expat, 'expat.%s', attributes)
756756

757757

Lib/test/test_pyexpat.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,5 +1402,34 @@ def test_set_maximum_amplification__amplification_not_exceeded(self):
14021402
self.assertIsNotNone(parser.Parse(payload, True))
14031403

14041404

1405+
class VersionTest(unittest.TestCase):
1406+
1407+
def _test_version_info(self, v):
1408+
self.assertIsInstance(v[:], tuple)
1409+
self.assertEqual(len(v), 3)
1410+
self.assertIsInstance(v[0], int)
1411+
self.assertIsInstance(v[1], int)
1412+
self.assertIsInstance(v[2], int)
1413+
self.assertIsInstance(v.major, int)
1414+
self.assertIsInstance(v.minor, int)
1415+
self.assertIsInstance(v.micro, int)
1416+
self.assertEqual(v[0], v.major)
1417+
self.assertEqual(v[1], v.minor)
1418+
self.assertEqual(v[2], v.micro)
1419+
self.assertGreaterEqual(v.major, 2)
1420+
self.assertGreaterEqual(v.minor, 0)
1421+
self.assertGreaterEqual(v.micro, 0)
1422+
1423+
def test_version_info(self):
1424+
if support.verbose:
1425+
print(f'EXPAT_VERSION = {expat.EXPAT_VERSION}', flush=True)
1426+
print(f'VERSION_INFO = {expat.VERSION_INFO}', flush=True)
1427+
print(f'version_info = {expat.version_info}', flush=True)
1428+
self._test_version_info(expat.VERSION_INFO)
1429+
self._test_version_info(expat.version_info)
1430+
self.assertEqual(expat.EXPAT_VERSION, 'expat_%d.%d.%d' % expat.version_info)
1431+
self.assertEqual(expat.VERSION_INFO[0], expat.version_info[0])
1432+
1433+
14051434
if __name__ == "__main__":
14061435
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add constant :const:`pyexpat.VERSION_INFO` which provides information about
2+
the version of the Expat library that was used for building the module.
3+
:const:`pyexpat.version_info` is now a named tuple.

Modules/pyexpat.c

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2436,6 +2436,78 @@ pyexpat_capsule_destructor(PyObject *capsule)
24362436
}
24372437

24382438

2439+
PyDoc_STRVAR(version_info__doc__,
2440+
"pyexpat.version_info\n\
2441+
\n\
2442+
Expat version information as a named tuple.");
2443+
2444+
static PyStructSequence_Field version_info_fields[] = {
2445+
{"major", "Major release number"},
2446+
{"minor", "Minor release number"},
2447+
{"micro", "Micro release number"},
2448+
{0}
2449+
};
2450+
2451+
static PyStructSequence_Desc version_info_desc = {
2452+
"pyexpat.version_info", /* name */
2453+
version_info__doc__, /* doc */
2454+
version_info_fields, /* fields */
2455+
3
2456+
};
2457+
2458+
static PyObject *
2459+
make_version_info(PyTypeObject *type, int major, int minor, int micro)
2460+
{
2461+
PyObject *version;
2462+
int pos = 0;
2463+
2464+
version = PyStructSequence_New(type);
2465+
if (version == NULL) {
2466+
return NULL;
2467+
}
2468+
2469+
#define SetItem(VALUE) \
2470+
PyStructSequence_SET_ITEM(version, pos++, VALUE); \
2471+
if (PyErr_Occurred()) { \
2472+
Py_DECREF(version); \
2473+
return NULL; \
2474+
}
2475+
2476+
SetItem(PyLong_FromLong(major))
2477+
SetItem(PyLong_FromLong(minor))
2478+
SetItem(PyLong_FromLong(micro))
2479+
#undef SetItem
2480+
2481+
return version;
2482+
}
2483+
2484+
static int
2485+
add_version_info(PyObject *mod)
2486+
{
2487+
PyTypeObject *version_type;
2488+
version_type = PyStructSequence_NewType(&version_info_desc);
2489+
if (version_type == NULL) {
2490+
return -1;
2491+
}
2492+
if (PyModule_Add(mod, "VERSION_INFO",
2493+
make_version_info(version_type, XML_MAJOR_VERSION,
2494+
XML_MINOR_VERSION, XML_MICRO_VERSION)) < 0)
2495+
{
2496+
Py_DECREF(version_type);
2497+
return -1;
2498+
}
2499+
XML_Expat_Version info = XML_ExpatVersionInfo();
2500+
if (PyModule_Add(mod, "version_info",
2501+
make_version_info(version_type, info.major,
2502+
info.minor, info.micro)) < 0)
2503+
{
2504+
Py_DECREF(version_type);
2505+
return -1;
2506+
}
2507+
Py_DECREF(version_type);
2508+
return 0;
2509+
}
2510+
24392511
static int
24402512
pyexpat_exec(PyObject *mod)
24412513
{
@@ -2479,15 +2551,8 @@ pyexpat_exec(PyObject *mod)
24792551
XML_ExpatVersion()) < 0) {
24802552
return -1;
24812553
}
2482-
{
2483-
XML_Expat_Version info = XML_ExpatVersionInfo();
2484-
PyObject *versionInfo = Py_BuildValue("(iii)",
2485-
info.major,
2486-
info.minor,
2487-
info.micro);
2488-
if (PyModule_Add(mod, "version_info", versionInfo) < 0) {
2489-
return -1;
2490-
}
2554+
if (add_version_info(mod) < 0) {
2555+
return -1;
24912556
}
24922557
/* XXX When Expat supports some way of figuring out how it was
24932558
compiled, this should check and set native_encoding

0 commit comments

Comments
 (0)