Skip to content

Commit 1439b81

Browse files
authored
pythongh-128629: Add Py_PACK_VERSION and Py_PACK_FULL_VERSION (pythonGH-128630)
1 parent 4685401 commit 1439b81

File tree

18 files changed

+358
-33
lines changed

18 files changed

+358
-33
lines changed

Doc/c-api/apiabiversion.rst

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
API and ABI Versioning
77
***********************
88

9+
10+
Build-time version constants
11+
----------------------------
12+
913
CPython exposes its version number in the following macros.
10-
Note that these correspond to the version code is **built** with,
11-
not necessarily the version used at **run time**.
14+
Note that these correspond to the version code is **built** with.
15+
See :c:var:`Py_Version` for the version used at **run time**.
1216

1317
See :ref:`stable` for a discussion of API and ABI stability across versions.
1418

@@ -37,37 +41,83 @@ See :ref:`stable` for a discussion of API and ABI stability across versions.
3741
.. c:macro:: PY_VERSION_HEX
3842
3943
The Python version number encoded in a single integer.
44+
See :c:func:`Py_PACK_FULL_VERSION` for the encoding details.
4045

41-
The underlying version information can be found by treating it as a 32 bit
42-
number in the following manner:
43-
44-
+-------+-------------------------+-------------------------+--------------------------+
45-
| Bytes | Bits (big endian order) | Meaning | Value for ``3.4.1a2`` |
46-
+=======+=========================+=========================+==========================+
47-
| 1 | 1-8 | ``PY_MAJOR_VERSION`` | ``0x03`` |
48-
+-------+-------------------------+-------------------------+--------------------------+
49-
| 2 | 9-16 | ``PY_MINOR_VERSION`` | ``0x04`` |
50-
+-------+-------------------------+-------------------------+--------------------------+
51-
| 3 | 17-24 | ``PY_MICRO_VERSION`` | ``0x01`` |
52-
+-------+-------------------------+-------------------------+--------------------------+
53-
| 4 | 25-28 | ``PY_RELEASE_LEVEL`` | ``0xA`` |
54-
+ +-------------------------+-------------------------+--------------------------+
55-
| | 29-32 | ``PY_RELEASE_SERIAL`` | ``0x2`` |
56-
+-------+-------------------------+-------------------------+--------------------------+
46+
Use this for numeric comparisons, for example,
47+
``#if PY_VERSION_HEX >= ...``.
5748

58-
Thus ``3.4.1a2`` is hexversion ``0x030401a2`` and ``3.10.0`` is
59-
hexversion ``0x030a00f0``.
6049

61-
Use this for numeric comparisons, e.g. ``#if PY_VERSION_HEX >= ...``.
62-
63-
This version is also available via the symbol :c:var:`Py_Version`.
50+
Run-time version
51+
----------------
6452

6553
.. c:var:: const unsigned long Py_Version
6654
67-
The Python runtime version number encoded in a single constant integer, with
68-
the same format as the :c:macro:`PY_VERSION_HEX` macro.
55+
The Python runtime version number encoded in a single constant integer.
56+
See :c:func:`Py_PACK_FULL_VERSION` for the encoding details.
6957
This contains the Python version used at run time.
7058

59+
Use this for numeric comparisons, for example, ``if (Py_Version >= ...)``.
60+
7161
.. versionadded:: 3.11
7262

73-
All the given macros are defined in :source:`Include/patchlevel.h`.
63+
64+
Bit-packing macros
65+
------------------
66+
67+
.. c:function:: uint32_t Py_PACK_FULL_VERSION(int major, int minor, int micro, int release_level, int release_serial)
68+
69+
Return the given version, encoded as a single 32-bit integer with
70+
the following structure:
71+
72+
+------------------+-------+----------------+-----------+--------------------------+
73+
| | No. | | | Example values |
74+
| | of | | +-------------+------------+
75+
| Argument | bits | Bit mask | Bit shift | ``3.4.1a2`` | ``3.10.0`` |
76+
+==================+=======+================+===========+=============+============+
77+
| *major* | 8 | ``0xFF000000`` | 24 | ``0x03`` | ``0x03`` |
78+
+------------------+-------+----------------+-----------+-------------+------------+
79+
| *minor* | 8 | ``0x00FF0000`` | 16 | ``0x04`` | ``0x0A`` |
80+
+------------------+-------+----------------+-----------+-------------+------------+
81+
| *micro* | 8 | ``0x0000FF00`` | 8 | ``0x01`` | ``0x00`` |
82+
+------------------+-------+----------------+-----------+-------------+------------+
83+
| *release_level* | 4 | ``0x000000F0`` | 4 | ``0xA`` | ``0xF`` |
84+
+------------------+-------+----------------+-----------+-------------+------------+
85+
| *release_serial* | 4 | ``0x0000000F`` | 0 | ``0x2`` | ``0x0`` |
86+
+------------------+-------+----------------+-----------+-------------+------------+
87+
88+
For example:
89+
90+
+-------------+------------------------------------+-----------------+
91+
| Version | ``Py_PACK_FULL_VERSION`` arguments | Encoded version |
92+
+=============+====================================+=================+
93+
| ``3.4.1a2`` | ``(3, 4, 1, 0xA, 2)`` | ``0x030401a2`` |
94+
+-------------+------------------------------------+-----------------+
95+
| ``3.10.0`` | ``(3, 10, 0, 0xF, 0)`` | ``0x030a00f0`` |
96+
+-------------+------------------------------------+-----------------+
97+
98+
Out-of range bits in the arguments are ignored.
99+
That is, the macro can be defined as:
100+
101+
.. code-block:: c
102+
103+
#ifndef Py_PACK_FULL_VERSION
104+
#define Py_PACK_FULL_VERSION(X, Y, Z, LEVEL, SERIAL) ( \
105+
(((X) & 0xff) << 24) | \
106+
(((Y) & 0xff) << 16) | \
107+
(((Z) & 0xff) << 8) | \
108+
(((LEVEL) & 0xf) << 4) | \
109+
(((SERIAL) & 0xf) << 0))
110+
#endif
111+
112+
``Py_PACK_FULL_VERSION`` is primarily a macro, intended for use in
113+
``#if`` directives, but it is also available as an exported function.
114+
115+
.. versionadded:: 3.14
116+
117+
.. c:function:: uint32_t Py_PACK_VERSION(int major, int minor)
118+
119+
Equivalent to ``Py_PACK_FULL_VERSION(major, minor, 0, 0, 0)``.
120+
The result does not correspond to any Python release, but is useful
121+
in numeric comparisons.
122+
123+
.. versionadded:: 3.14

Doc/data/stable_abi.dat

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Doc/whatsnew/3.14.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,10 @@ New features
12431243
file.
12441244
(Contributed by Victor Stinner in :gh:`127350`.)
12451245

1246+
* Add macros :c:func:`Py_PACK_VERSION` and :c:func:`Py_PACK_FULL_VERSION` for
1247+
bit-packing Python version numbers.
1248+
(Contributed by Petr Viktorin in :gh:`128629`.)
1249+
12461250

12471251
Porting to Python 3.14
12481252
----------------------

Include/patchlevel.h

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
1+
#ifndef _Py_PATCHLEVEL_H
2+
#define _Py_PATCHLEVEL_H
23
/* Python version identification scheme.
34
45
When the major or minor version changes, the VERSION variable in
@@ -26,10 +27,23 @@
2627
#define PY_VERSION "3.14.0a3+"
2728
/*--end constants--*/
2829

30+
31+
#define _Py_PACK_FULL_VERSION(X, Y, Z, LEVEL, SERIAL) ( \
32+
(((X) & 0xff) << 24) | \
33+
(((Y) & 0xff) << 16) | \
34+
(((Z) & 0xff) << 8) | \
35+
(((LEVEL) & 0xf) << 4) | \
36+
(((SERIAL) & 0xf) << 0))
37+
2938
/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2.
3039
Use this for numeric comparisons, e.g. #if PY_VERSION_HEX >= ... */
31-
#define PY_VERSION_HEX ((PY_MAJOR_VERSION << 24) | \
32-
(PY_MINOR_VERSION << 16) | \
33-
(PY_MICRO_VERSION << 8) | \
34-
(PY_RELEASE_LEVEL << 4) | \
35-
(PY_RELEASE_SERIAL << 0))
40+
#define PY_VERSION_HEX _Py_PACK_FULL_VERSION( \
41+
PY_MAJOR_VERSION, \
42+
PY_MINOR_VERSION, \
43+
PY_MICRO_VERSION, \
44+
PY_RELEASE_LEVEL, \
45+
PY_RELEASE_SERIAL)
46+
47+
// Public Py_PACK_VERSION is declared in pymacro.h; it needs <inttypes.h>.
48+
49+
#endif //_Py_PATCHLEVEL_H

Include/pymacro.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,13 @@
190190
// "comparison of unsigned expression in '< 0' is always false".
191191
#define _Py_IS_TYPE_SIGNED(type) ((type)(-1) <= 0)
192192

193+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000 // 3.14
194+
// Version helpers. These are primarily macros, but have exported equivalents.
195+
PyAPI_FUNC(uint32_t) Py_PACK_FULL_VERSION(int x, int y, int z, int level, int serial);
196+
PyAPI_FUNC(uint32_t) Py_PACK_VERSION(int x, int y);
197+
#define Py_PACK_FULL_VERSION _Py_PACK_FULL_VERSION
198+
#define Py_PACK_VERSION(X, Y) Py_PACK_FULL_VERSION(X, Y, 0, 0, 0)
199+
#endif // Py_LIMITED_API < 3.14
200+
201+
193202
#endif /* Py_PYMACRO_H */

Lib/test/test_capi/test_misc.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3335,6 +3335,49 @@ def run(self):
33353335
self.assertEqual(len(set(py_thread_ids)), len(py_thread_ids),
33363336
py_thread_ids)
33373337

3338+
class TestVersions(unittest.TestCase):
3339+
full_cases = (
3340+
(3, 4, 1, 0xA, 2, 0x030401a2),
3341+
(3, 10, 0, 0xF, 0, 0x030a00f0),
3342+
(0x103, 0x10B, 0xFF00, -1, 0xF0, 0x030b00f0), # test masking
3343+
)
3344+
xy_cases = (
3345+
(3, 4, 0x03040000),
3346+
(3, 10, 0x030a0000),
3347+
(0x103, 0x10B, 0x030b0000), # test masking
3348+
)
3349+
3350+
def test_pack_full_version(self):
3351+
for *args, expected in self.full_cases:
3352+
with self.subTest(hexversion=hex(expected)):
3353+
result = _testlimitedcapi.pack_full_version(*args)
3354+
self.assertEqual(result, expected)
3355+
3356+
def test_pack_version(self):
3357+
for *args, expected in self.xy_cases:
3358+
with self.subTest(hexversion=hex(expected)):
3359+
result = _testlimitedcapi.pack_version(*args)
3360+
self.assertEqual(result, expected)
3361+
3362+
def test_pack_full_version_ctypes(self):
3363+
ctypes = import_helper.import_module('ctypes')
3364+
ctypes_func = ctypes.pythonapi.Py_PACK_FULL_VERSION
3365+
ctypes_func.restype = ctypes.c_uint32
3366+
ctypes_func.argtypes = [ctypes.c_int] * 5
3367+
for *args, expected in self.full_cases:
3368+
with self.subTest(hexversion=hex(expected)):
3369+
result = ctypes_func(*args)
3370+
self.assertEqual(result, expected)
3371+
3372+
def test_pack_version_ctypes(self):
3373+
ctypes = import_helper.import_module('ctypes')
3374+
ctypes_func = ctypes.pythonapi.Py_PACK_VERSION
3375+
ctypes_func.restype = ctypes.c_uint32
3376+
ctypes_func.argtypes = [ctypes.c_int] * 2
3377+
for *args, expected in self.xy_cases:
3378+
with self.subTest(hexversion=hex(expected)):
3379+
result = ctypes_func(*args)
3380+
self.assertEqual(result, expected)
33383381

33393382
if __name__ == "__main__":
33403383
unittest.main()

Lib/test/test_stable_abi_ctypes.py

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add macros :c:func:`Py_PACK_VERSION` and :c:func:`Py_PACK_FULL_VERSION` for
2+
bit-packing Python version numbers.

Misc/stable_abi.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2540,3 +2540,7 @@
25402540
added = '3.14'
25412541
[function.PyType_Freeze]
25422542
added = '3.14'
2543+
[function.Py_PACK_FULL_VERSION]
2544+
added = '3.14'
2545+
[function.Py_PACK_VERSION]
2546+
added = '3.14'

Modules/Setup.stdlib.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
164164
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
165165
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c
166-
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c
166+
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c
167167
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
168168
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
169169

Modules/_testlimitedcapi.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,8 @@ PyInit__testlimitedcapi(void)
8383
if (_PyTestLimitedCAPI_Init_VectorcallLimited(mod) < 0) {
8484
return NULL;
8585
}
86+
if (_PyTestLimitedCAPI_Init_Version(mod) < 0) {
87+
return NULL;
88+
}
8689
return mod;
8790
}

Modules/_testlimitedcapi/clinic/version.c.h

Lines changed: 93 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testlimitedcapi/parts.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ int _PyTestLimitedCAPI_Init_Sys(PyObject *module);
4040
int _PyTestLimitedCAPI_Init_Tuple(PyObject *module);
4141
int _PyTestLimitedCAPI_Init_Unicode(PyObject *module);
4242
int _PyTestLimitedCAPI_Init_VectorcallLimited(PyObject *module);
43+
int _PyTestLimitedCAPI_Init_Version(PyObject *module);
4344

4445
#endif // Py_TESTLIMITEDCAPI_PARTS_H

0 commit comments

Comments
 (0)