From 76354de2397930934dc73ce73b3284363e83f63e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 May 2026 09:59:42 +0300 Subject: [PATCH 1/3] gh-117404: Add structured version info for compression modules Add a number of constants in modules zlib, bz2, lzma, and compression.zstd, which provide information about the version of libraries that were used for building the module and that are actually loaded: * zlib.zlib_version -- an alias of zlib.ZLIB_RUNTIME_VERSION * zlib.ZLIB_VERSION_INFO * zlib.zlib_version_info * zlib.zlibng_version * zlib.ZLIBNG_VERSION_INFO * zlib.zlibng_version_info * bz2.bzlib_version * bz2.bzlib_version_info * lzma.LZMA_VERSION * lzma.lzma_version * lzma.LZMA_VERSION_INFO * lzma.lzma_version_info * compression.zstd.ZSTD_VERSION * compression.zstd.ZSTD_VERSION_INFO Make compression.zstd.zstd_version_info a named tuple. --- Doc/library/bz2.rst | 25 +++ Doc/library/compression.zstd.rst | 39 ++++- Doc/library/lzma.rst | 42 +++++ Doc/library/zlib.rst | 59 ++++++- Doc/whatsnew/3.16.rst | 34 ++++ Lib/bz2.py | 4 +- Lib/compression/zstd/__init__.py | 9 +- Lib/lzma.py | 1 + Lib/test/pythoninfo.py | 24 ++- Lib/test/test_bz2.py | 25 +++ Lib/test/test_lzma.py | 35 +++++ Lib/test/test_zlib.py | 92 +++++++++-- Lib/test/test_zstd.py | 33 +++- ...-05-29-10-11-34.gh-issue-117404.EeP6xe.rst | 5 + Modules/_bz2module.c | 68 ++++++++ Modules/_lzmamodule.c | 83 ++++++++++ Modules/_zstd/_zstdmodule.c | 72 ++++++++- Modules/zlibmodule.c | 148 ++++++++++++++++++ 18 files changed, 762 insertions(+), 36 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst diff --git a/Doc/library/bz2.rst b/Doc/library/bz2.rst index 6c20e9c94a3eaee..75221833291437e 100644 --- a/Doc/library/bz2.rst +++ b/Doc/library/bz2.rst @@ -307,6 +307,31 @@ One-shot (de)compression .. versionchanged:: 3.3 Support for multi-stream inputs was added. + +Miscellaneous +------------- + +Information about the version of the bzip2 compression library in use +is available through the following constants: + + +.. data:: bzlib_version + + The version string of the bzip2 compression library. + + .. versionadded:: next + + +.. data:: bzlib_version_info + + A named tuple containing the three components of the bzip2 compression + library version: *major*, *minor*, and *patch*. All values are integers. + The components can also be accessed by name, so ``bz2.bzlib_version_info[0]`` + is equivalent to ``bz2.bzlib_version_info.major`` and so on. + + .. versionadded:: next + + .. _bz2-usage-examples: Examples of usage diff --git a/Doc/library/compression.zstd.rst b/Doc/library/compression.zstd.rst index 6d99e36e1e5bb65..45cefdebf778ab6 100644 --- a/Doc/library/compression.zstd.rst +++ b/Doc/library/compression.zstd.rst @@ -827,10 +827,43 @@ Miscellaneous The default compression level for Zstandard: ``3``. -.. attribute:: zstd_version_info +Information about the version of the zstd library in use is available through +the following constants: - Version number of the runtime zstd library as a tuple of integers - (major, minor, release). + +.. data:: ZSTD_VERSION + + The version string of the zstd library that was used for building the module. + This may be different from the zstd library actually used at runtime, which + is available as :const:`zstd_version`. + + .. versionadded:: next + + +.. data:: zstd_version + + The version string of the zstd library actually loaded by the interpreter. + + +.. data:: ZSTD_VERSION_INFO + + A named tuple containing the four components of the zstd library + version that was used for building the module: + *major*, *minor*, and *patch*. All values are integers. + The components can also be accessed by name, so ``zstd.ZSTD_VERSION_INFO[0]`` + is equivalent to ``zstd.ZSTD_VERSION_INFO.major`` and so on. + This may be different from the zstd library actually used at runtime, which + is available as :const:`zstd_version_info`. + + .. versionadded:: next + + +.. data:: zstd_version_info + + A named tuple containing the zstd library version actually loaded by the interpreter. + + .. versionchanged:: next + It is now a named tuple. Examples diff --git a/Doc/library/lzma.rst b/Doc/library/lzma.rst index cd72174d54f6e62..6cb2c354ad89d2f 100644 --- a/Doc/library/lzma.rst +++ b/Doc/library/lzma.rst @@ -335,6 +335,48 @@ Miscellaneous feature set. +Information about the version of the lzma library in use is available through +the following constants: + + +.. data:: LZMA_VERSION + + The version string of the lzma library that was used for building the module. + This may be different from the lzma library actually used at runtime, which + is available as :const:`lzma_version`. + + .. versionadded:: next + + +.. data:: lzma_version + + The version string of the lzma library actually loaded by the interpreter. + + .. versionadded:: next + + +.. data:: LZMA_VERSION_INFO + + A named tuple containing the four components of the lzma library + version that was used for building the module: + *major*, *minor*, *patch*, and *stability*. + All values except *stability* are integers; *stability* is ``'alpha'``, + ``'beta'``, or ``'stable'``. + The components can also be accessed by name, so ``lzma.LZMA_VERSION_INFO[0]`` + is equivalent to ``lzma.LZMA_VERSION_INFO.major`` and so on. + This may be different from the lzma library actually used at runtime, which + is available as :const:`lzma_version_info`. + + .. versionadded:: next + + +.. data:: lzma_version_info + + A named tuple containing the lzma library version actually loaded by the interpreter. + + .. versionadded:: next + + .. _filter-chain-specs: Specifying custom filter chains diff --git a/Doc/library/zlib.rst b/Doc/library/zlib.rst index f043915c0f4b94e..da6ef40558ca40e 100644 --- a/Doc/library/zlib.rst +++ b/Doc/library/zlib.rst @@ -479,27 +479,76 @@ the following constants: The version string of the zlib library that was used for building the module. This may be different from the zlib library actually used at runtime, which - is available as :const:`ZLIB_RUNTIME_VERSION`. + is available as :const:`zlib_version`. .. data:: ZLIB_RUNTIME_VERSION +.. data:: zlib_version The version string of the zlib library actually loaded by the interpreter. .. versionadded:: 3.3 + .. versionchanged:: next + Added alias :const:`!zlib_version`. + + +.. data:: ZLIB_VERSION_INFO + + A named tuple containing the four components of the zlib library + version that was used for building the module: + *major*, *minor*, *revision*, and *subversion*. + All values are integers. + The components can also be accessed by name, so ``zlib.VERSION_INFO[0]`` + is equivalent to ``zlib.VERSION_INFO.major`` and so on. + This may be different from the zlib library actually used at runtime, which + is available as :const:`zlib_version_info`. + + .. versionadded:: next + + +.. data:: zlib_version_info + + A named tuple containing the zlib library version actually loaded by the interpreter. + + .. versionadded:: next + + +The following constants are only present if zlib-ng was used to build +the module: .. data:: ZLIBNG_VERSION The version string of the zlib-ng library that was used for building the - module if zlib-ng was used. When present, the :data:`ZLIB_VERSION` and - :data:`ZLIB_RUNTIME_VERSION` constants reflect the version of the zlib API + module if zlib-ng was used. When present, the :const:`ZLIB_VERSION` and + :const:`zlib_version` constants reflect the version of the zlib API provided by zlib-ng. - If zlib-ng was not used to build the module, this constant will be absent. - .. versionadded:: 3.14 +.. data:: zlibng_version + + The version string of the zlib-ng library actually loaded + by the interpreter. + + .. versionadded:: next + + +.. data:: ZLIBNG_VERSION_INFO + + A named tuple containing the version of the zlib-ng library that was + used for building the module if zlib-ng was used. + + .. versionadded:: next + + +.. data:: zlibng_version_info + + A named tuple containing the zlib-ng library version actually loaded + by the interpreter. + + .. versionadded:: next + .. seealso:: diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index a6911b68c2eb756..02ef6e9e22f4820 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -86,6 +86,23 @@ New modules Improved modules ================ +bz2 +--- + +* Added constants :const:`~bz2.bzlib_version` and + :const:`~bz2.bzlib_version_info` which provide information about + the version of the bzip2 compression library in use. + (Contributed by Serhiy Storchaka in :gh:`117404`.) + +compression.zstd +---------------- + +* Added constants :const:`~compression.zstd.ZSTD_VERSION` and + :const:`~compression.zstd.ZSTD_VERSION_INFO` which provide information + about the version of the zstd library that was used for building the module. + :const:`~compression.zstd.zstd_version_info` is now a named tuple. + (Contributed by Serhiy Storchaka in :gh:`117404`.) + gzip ---- @@ -102,6 +119,11 @@ lzma requires ``lzma`` 5.4.0 or newer while RISC-V requires 5.6.0 or newer. (Contributed by Chien Wong in :gh:`115988`.) +* Added constants :const:`~lzma.LZMA_VERSION`, :const:`~lzma.lzma_version`, + :const:`~lzma.LZMA_VERSION_INFO`, and :const:`~lzma.lzma_version_info`, + which provide information about the version of the lzma library in use. + (Contributed by Serhiy Storchaka in :gh:`117404`.) + os -- @@ -124,6 +146,18 @@ xml instead of failing later, when encounter non-ASCII data. (Contributed by Serhiy Storchaka in :gh:`62259`.) +zlib +---- + +* Added constants :const:`~zlib.ZLIB_VERSION_INFO`, + :const:`~zlib.zlib_version_info`, :const:`~zlib.zlibng_version`, + :const:`~zlib.ZLIBNG_VERSION_INFO`, and :const:`~zlib.zlibng_version_info`, + which provide information about the version of the zlib and the zlib-ng + libraries in use. + Added :const:`~zlib.zlib_version` as an alias of + :const:`~zlib.ZLIB_RUNTIME_VERSION`. + (Contributed by Serhiy Storchaka in :gh:`117404`.) + .. Add improved modules above alphabetically, not here at the end. Optimizations diff --git a/Lib/bz2.py b/Lib/bz2.py index eb58f4da596ea18..1726400115d2602 100644 --- a/Lib/bz2.py +++ b/Lib/bz2.py @@ -5,7 +5,7 @@ """ __all__ = ["BZ2File", "BZ2Compressor", "BZ2Decompressor", - "open", "compress", "decompress"] + "open", "compress", "decompress", "bzlib_version", "bzlib_version_info"] __author__ = "Nadeem Vawda " @@ -14,7 +14,7 @@ import io import os -from _bz2 import BZ2Compressor, BZ2Decompressor +from _bz2 import BZ2Compressor, BZ2Decompressor, bzlib_version, bzlib_version_info # Value 0 no longer used diff --git a/Lib/compression/zstd/__init__.py b/Lib/compression/zstd/__init__.py index 5326cf9b19cf883..22ce028c435caa8 100644 --- a/Lib/compression/zstd/__init__.py +++ b/Lib/compression/zstd/__init__.py @@ -20,6 +20,8 @@ 'get_frame_size', 'zstd_version', 'zstd_version_info', + 'ZSTD_VERSION', + 'ZSTD_VERSION_INFO', 'ZstdCompressor', 'ZstdDecompressor', 'ZstdDict', @@ -29,13 +31,10 @@ import _zstd import enum from _zstd import (ZstdCompressor, ZstdDecompressor, ZstdDict, ZstdError, - get_frame_size, zstd_version) + get_frame_size, zstd_version, ZSTD_VERSION, + zstd_version_info, ZSTD_VERSION_INFO) from compression.zstd._zstdfile import ZstdFile, open, _nbytes -# zstd_version_number is (MAJOR * 100 * 100 + MINOR * 100 + RELEASE) -zstd_version_info = (*divmod(_zstd.zstd_version_number // 100, 100), - _zstd.zstd_version_number % 100) -"""Version number of the runtime zstd library as a tuple of integers.""" COMPRESSION_LEVEL_DEFAULT = _zstd.ZSTD_CLEVEL_DEFAULT """The default compression level for Zstandard, currently '3'.""" diff --git a/Lib/lzma.py b/Lib/lzma.py index fb4bbf650f849a4..73a7c702e037112 100644 --- a/Lib/lzma.py +++ b/Lib/lzma.py @@ -20,6 +20,7 @@ "LZMACompressor", "LZMADecompressor", "LZMAFile", "LZMAError", "open", "compress", "decompress", "is_check_supported", + "lzma_version", "lzma_version_info", "LZMA_VERSION", "LZMA_VERSION_INFO", ] import builtins diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 7f735d75b318e7f..d4245a6be57ff84 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -660,17 +660,37 @@ def collect_zlib(info_add): except ImportError: return - attributes = ('ZLIB_VERSION', 'ZLIB_RUNTIME_VERSION', 'ZLIBNG_VERSION') + attributes = ('ZLIB_VERSION', 'zlib_version', 'ZLIBNG_VERSION', 'zlibng_version') copy_attributes(info_add, zlib, 'zlib.%s', attributes) +def collect_bz2(info_add): + try: + import _bz2 + except ImportError: + return + + attributes = ('bzlib_version',) + copy_attributes(info_add, _bz2, 'bz2.%s', attributes) + + +def collect_lzma(info_add): + try: + import _lzma + except ImportError: + return + + attributes = ('LZMA_VERSION', 'lzma_version') + copy_attributes(info_add, _lzma, 'lzma.%s', attributes) + + def collect_zstd(info_add): try: import _zstd except ImportError: return - attributes = ('zstd_version',) + attributes = ('ZSTD_VERSION', 'zstd_version') copy_attributes(info_add, _zstd, 'zstd.%s', attributes) diff --git a/Lib/test/test_bz2.py b/Lib/test/test_bz2.py index d8e3b671ec229f9..151dd96726e698e 100644 --- a/Lib/test/test_bz2.py +++ b/Lib/test/test_bz2.py @@ -1207,6 +1207,31 @@ def test_newline(self): self.assertEqual(f.readlines(), [text]) +class MiscTests(unittest.TestCase): + + def test_bzlib_version(self): + if support.verbose: + print(f'bzlib_version = {bz2.bzlib_version}', flush=True) + print(f'bzlib_version_info = {bz2.bzlib_version_info}', flush=True) + v = bz2.bzlib_version_info + 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.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + + self.assertEqual(bz2.bzlib_version.split(',')[0], '%d.%d.%d' % v) + + def tearDownModule(): support.reap_children() diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py index d4f954b34c12526..d20f4f4001ca74c 100644 --- a/Lib/test/test_lzma.py +++ b/Lib/test/test_lzma.py @@ -1513,6 +1513,41 @@ def test_x_mode(self): class MiscellaneousTestCase(unittest.TestCase): + def _test_lzma_version(self, v, string): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 4) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v[3], str) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertIsInstance(v.stability, str) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertEqual(v[3], v.stability) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + self.assertIn(v.stability, {'alpha', 'beta', 'stable'}) + + if v.stability == 'stable': + self.assertEqual(string, '%d.%d.%d' % v[:3]) + else: + self.assertTrue(string.startswith('%d.%d.%d%s' % v)) + + def test_lzma_version(self): + if support.verbose: + print(f'LZMA_VERSION = {lzma.LZMA_VERSION}', flush=True) + print(f'lzma_version = {lzma.lzma_version}', flush=True) + print(f'LZMA_VERSION_INFO = {lzma.LZMA_VERSION_INFO}', flush=True) + print(f'lzma_version_info = {lzma.lzma_version_info}', flush=True) + self._test_lzma_version(lzma.LZMA_VERSION_INFO, lzma.LZMA_VERSION) + self._test_lzma_version(lzma.lzma_version_info, lzma.lzma_version) + self.assertEqual(lzma.LZMA_VERSION_INFO[0], lzma.lzma_version_info[0]) + def test_is_check_supported(self): # CHECK_NONE and CHECK_CRC32 should always be supported, # regardless of the options liblzma was compiled with. diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py index ed9d85408159b28..ac89c8795ff8036 100644 --- a/Lib/test/test_zlib.py +++ b/Lib/test/test_zlib.py @@ -19,20 +19,17 @@ 'requires Decompress.copy()') -def _zlib_runtime_version_tuple(zlib_version=zlib.ZLIB_RUNTIME_VERSION): +def _parse_version(version): # Register "1.2.3" as "1.2.3.0" # or "1.2.0-linux","1.2.0.f","1.2.0.f-linux" - v = zlib_version.split('-', 1)[0].split('.') - if len(v) < 4: + v = version.split('-', 1)[0].split('.') + if not v[-1].isnumeric(): + v.pop() + while len(v) < 4: v.append('0') - elif not v[-1].isnumeric(): - v[-1] = '0' return tuple(map(int, v)) -ZLIB_RUNTIME_VERSION_TUPLE = _zlib_runtime_version_tuple() - - # bpo-46623: When a hardware accelerator is used (currently only on s390x), # using different ways to compress data with zlib can produce different # compressed data. @@ -71,7 +68,80 @@ def test_library_version(self): # the minor versions will match (even on the machine on which the module # was compiled), and the API is stable between minor versions, so # testing only the major versions avoids spurious failures. - self.assertEqual(zlib.ZLIB_RUNTIME_VERSION[0], zlib.ZLIB_VERSION[0]) + self.assertEqual(zlib.zlib_version.split('.')[0], + zlib.ZLIB_VERSION.split('.')[0]) + self.assertEqual(zlib.zlib_version_info[0], + zlib.ZLIB_VERSION_INFO[0]) + if hasattr(zlib, 'ZLIBNG_VERSION'): + self.assertEqual(zlib.zlibng_version.split('.')[0], + zlib.ZLIBNG_VERSION.split('.')[0]) + self.assertEqual(zlib.zlibng_version_info[0], + zlib.ZLIBNG_VERSION_INFO[0]) + else: + self.assertNotHasAttr(zlib, 'ZLIBNG_VERSION') + self.assertNotHasAttr(zlib, 'zlibng_version') + self.assertNotHasAttr(zlib, 'ZLIBNG_VERSION_INFO') + self.assertNotHasAttr(zlib, 'zlibng_version_info') + + def _test_zlib_version(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 4) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v[3], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.revision, int) + self.assertIsInstance(v.subversion, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.revision) + self.assertEqual(v[3], v.subversion) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.revision, 0) + self.assertGreaterEqual(v.subversion, 0) + + def test_zlib_version(self): + if support.verbose: + print(f'ZLIB_VERSION = {zlib.ZLIB_VERSION}', flush=True) + print(f'zlib_version = {zlib.zlib_version}', flush=True) + print(f'ZLIB_VERSION_INFO = {zlib.ZLIB_VERSION_INFO}', flush=True) + print(f'zlib_version_info = {zlib.zlib_version_info}', flush=True) + self._test_zlib_version(zlib.ZLIB_VERSION_INFO) + self.assertEqual(zlib.ZLIB_VERSION_INFO, _parse_version(zlib.ZLIB_VERSION)) + self._test_zlib_version(zlib.zlib_version_info) + self.assertEqual(zlib.zlib_version_info, _parse_version(zlib.zlib_version)) + self.assertEqual(zlib.ZLIB_RUNTIME_VERSION, zlib.zlib_version) + + def _test_zlibng_version(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 4) + 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.revision, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.revision) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.revision, 0) + + @unittest.skipUnless(hasattr(zlib, 'ZLIBNG_VERSION'), 'requires zlib-ng') + def test_zlibng_version(self): + if support.verbose: + print(f'ZLIBNG_VERSION = {zlib.ZLIBNG_VERSION}', flush=True) + print(f'zlibng_version = {zlib.zlibng_version}', flush=True) + print(f'ZLIBNG_VERSION_INFO = {zlib.ZLIBNG_VERSION_INFO}', flush=True) + print(f'zlibng_version_info = {zlib.zlibng_version_info}', flush=True) + self._test_zlibng_version(zlib.ZLIBNG_VERSION_INFO) + self.assertEqual(zlib.ZLIBNG_VERSION, '%d.%d.%d' % zlib.ZLIBNG_VERSION_INFO) + self._test_zlibng_version(zlib.zlibng_version_info) + self.assertEqual(zlib.zlibng_version, '%d.%d.%d' % zlib.zlibng_version_info) class ChecksumTestCase(unittest.TestCase): @@ -604,7 +674,7 @@ def test_flushes(self): 'Z_PARTIAL_FLUSH'] # Z_BLOCK has a known failure prior to 1.2.5.3 - if ZLIB_RUNTIME_VERSION_TUPLE >= (1, 2, 5, 3): + if zlib.zlib_version_info >= (1, 2, 5, 3): sync_opt.append('Z_BLOCK') sync_opt = [getattr(zlib, opt) for opt in sync_opt @@ -918,7 +988,7 @@ def test_large_unconsumed_tail(self, size): def test_wbits(self): # wbits=0 only supported since zlib v1.2.3.5 - supports_wbits_0 = ZLIB_RUNTIME_VERSION_TUPLE >= (1, 2, 3, 5) + supports_wbits_0 = zlib.zlib_version_info >= (1, 2, 3, 5) co = zlib.compressobj(level=1, wbits=15) zlib15 = co.compress(HAMLET_SCENE) + co.flush() diff --git a/Lib/test/test_zstd.py b/Lib/test/test_zstd.py index 6358cc78739cd9d..bac535ed4ca524f 100644 --- a/Lib/test/test_zstd.py +++ b/Lib/test/test_zstd.py @@ -9,6 +9,7 @@ import tempfile import threading +from test import support from test.support.import_helper import import_module from test.support import threading_helper from test.support import _1M @@ -24,8 +25,6 @@ ZstdDecompressor, ZstdDict, ZstdError, - zstd_version, - zstd_version_info, COMPRESSION_LEVEL_DEFAULT, get_frame_info, get_frame_size, @@ -133,9 +132,33 @@ def setUpModule(): class FunctionsTestCase(unittest.TestCase): - def test_version(self): - s = ".".join((str(i) for i in zstd_version_info)) - self.assertEqual(s, zstd_version) + def _test_zstd_version(self, v, string): + 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.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + + self.assertEqual(string, '%d.%d.%d' % v) + + def test_lzma_version(self): + if support.verbose: + print(f'ZSTD_VERSION = {zstd.ZSTD_VERSION}', flush=True) + print(f'zstd_version = {zstd.zstd_version}', flush=True) + print(f'ZSTD_VERSION_INFO = {zstd.ZSTD_VERSION_INFO}', flush=True) + print(f'zstd_version_info = {zstd.zstd_version_info}', flush=True) + self._test_zstd_version(zstd.ZSTD_VERSION_INFO, zstd.ZSTD_VERSION) + self._test_zstd_version(zstd.zstd_version_info, zstd.zstd_version) + self.assertEqual(zstd.ZSTD_VERSION_INFO[0], zstd.zstd_version_info[0]) def test_compressionLevel_values(self): min, max = CompressionParameter.compression_level.bounds() diff --git a/Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst b/Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst new file mode 100644 index 000000000000000..3f1942cf7e143c6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst @@ -0,0 +1,5 @@ +Add a number of constants in modules :mod:`zlib`, :mod:`bz2`, :mod:`lzma`, +and :mod:`compression.zstd`, which provide information about the version of +libraries that were used for building the module and that are actually +loaded. +Make :const:`compression.zstd.zstd_version_info` a named tuple. diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c index 4cf8beed9ee3eba..c60a5c54a55bd4e 100644 --- a/Modules/_bz2module.c +++ b/Modules/_bz2module.c @@ -735,6 +735,56 @@ static PyType_Spec bz2_decompressor_type_spec = { .slots = bz2_decompressor_type_slots, }; + +PyDoc_STRVAR(bzlib_version__doc__, +"_bz2.bzlib_version_info\n\ +\n\ +Bzlib version information as a named tuple."); + +static PyStructSequence_Field bzlib_version_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {0} +}; + +static PyStructSequence_Desc zlib_version_desc = { + "_bz2.bzlib_version_info", /* name */ + bzlib_version__doc__, /* doc */ + bzlib_version_fields, /* fields */ + 3 +}; + +static PyObject * +make_bzlib_version(PyTypeObject *type, const char *string) +{ + PyObject *version; + int pos = 0; + unsigned int major = 0, minor = 0, patch = 0; + + sscanf(string, "%u.%u.%u", &major, &minor, &patch); + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetIntItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, PyLong_FromUnsignedLong(VALUE)); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetIntItem(major) + SetIntItem(minor) + SetIntItem(patch) +#undef SetIntItem + + return version; +} + + /* Module initialization. */ static int @@ -759,6 +809,24 @@ _bz2_exec(PyObject *module) return -1; } + /* bzlib_version */ + if (PyModule_Add(module, "bzlib_version", + PyUnicode_FromString(BZ2_bzlibVersion())) < 0) + { + return -1; + } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&zlib_version_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(module, "bzlib_version_info", + make_bzlib_version(version_type, BZ2_bzlibVersion())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); return 0; } diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c index 4335a8bb162414d..82b6ec3a0a166fa 100644 --- a/Modules/_lzmamodule.c +++ b/Modules/_lzmamodule.c @@ -1509,6 +1509,62 @@ _lzma__decode_filter_properties_impl(PyObject *module, lzma_vli filter_id, return result; } + +PyDoc_STRVAR(lzma_version__doc__, +"_lzma.lzma_version_info\n\ +\n\ +Lzma version information as a named tuple."); + +static PyStructSequence_Field lzma_version_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {"stability", "'alpha', 'beta', or 'stable'"}, + {0} +}; + +static PyStructSequence_Desc lzma_version_desc = { + "_lzma.lzma_version_info", /* name */ + lzma_version__doc__, /* doc */ + lzma_version_fields, /* fields */ + 4 +}; + +static PyObject * +make_lzma_version(PyTypeObject *type, unsigned int number) +{ + PyObject *version; + int pos = 0; + unsigned int major = number / 10000000u; + unsigned int minor = (number % 10000000u) / 10000u; + unsigned int patch = (number % 10000u) / 10u; + unsigned int stability = number % 10u; + const char *stability_string = (stability == 0) ? "alpha" + : (stability == 1) ? "beta" + : "stable"; + + 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_FromUnsignedLong(major)) + SetItem(PyLong_FromUnsignedLong(minor)) + SetItem(PyLong_FromUnsignedLong(patch)) + SetItem(PyUnicode_FromString(stability_string)) +#undef SetItem + + return version; +} + + /* Some of our constants are more than 32 bits wide, so PyModule_AddIntConstant would not work correctly on platforms with 32-bit longs. */ static int @@ -1603,6 +1659,33 @@ lzma_exec(PyObject *module) return -1; } + /* lzma_version */ + if (PyModule_Add(module, "LZMA_VERSION", + PyUnicode_FromString(LZMA_VERSION_STRING)) < 0) { + return -1; + } + if (PyModule_Add(module, "lzma_version", + PyUnicode_FromString(lzma_version_string())) < 0) { + return -1; + } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&lzma_version_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(module, "LZMA_VERSION_INFO", + make_lzma_version(version_type, LZMA_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(module, "lzma_version_info", + make_lzma_version(version_type, lzma_version_number())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); return 0; } diff --git a/Modules/_zstd/_zstdmodule.c b/Modules/_zstd/_zstdmodule.c index 94246dd93b17de1..2f424aa1bb1f0d3 100644 --- a/Modules/_zstd/_zstdmodule.c +++ b/Modules/_zstd/_zstdmodule.c @@ -571,6 +571,55 @@ static PyMethodDef _zstd_methods[] = { {NULL, NULL} }; +PyDoc_STRVAR(zstd_version__doc__, + "_zstd.zstd_version_info\n\ +\n\ +Zstd version information as a named tuple."); + +static PyStructSequence_Field zstd_version_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {0} +}; + +static PyStructSequence_Desc zstd_version_desc = { + "_zstd.zstd_version_info", /* name */ + zstd_version__doc__, /* doc */ + zstd_version_fields, /* fields */ + 3 +}; + +static PyObject * +make_zstd_version(PyTypeObject *type, unsigned int number) +{ + PyObject *version; + int pos = 0; + unsigned int major = number / 10000u; + unsigned int minor = (number % 10000u) / 100u; + unsigned int patch = number % 100u; + + 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_FromUnsignedLong(major)) + SetItem(PyLong_FromUnsignedLong(minor)) + SetItem(PyLong_FromUnsignedLong(patch)) + #undef SetItem + + return version; +} + + static int _zstd_exec(PyObject *m) { @@ -623,15 +672,32 @@ do { \ } /* Add constants */ - if (PyModule_AddIntConstant(m, "zstd_version_number", - ZSTD_versionNumber()) < 0) { + if (PyModule_AddStringConstant(m, "ZSTD_VERSION", + ZSTD_VERSION_STRING) < 0) { return -1; } - if (PyModule_AddStringConstant(m, "zstd_version", ZSTD_versionString()) < 0) { return -1; } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&zstd_version_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(m, "ZSTD_VERSION_INFO", + make_zstd_version(version_type, ZSTD_VERSION_NUMBER)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(m, "zstd_version_info", + make_zstd_version(version_type, ZSTD_versionNumber())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); #if ZSTD_VERSION_NUMBER >= 10500 if (PyModule_AddIntConstant(m, "ZSTD_CLEVEL_DEFAULT", diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index 0a6732835eb51f5..b598051cecf3d60 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -2060,6 +2060,107 @@ zlib_getattr(PyObject *self, PyObject *args) return NULL; } +PyDoc_STRVAR(zlib_version__doc__, +"zlib.zlib_version_info\n\ +\n\ +Zlib version information as a named tuple."); + +static PyStructSequence_Field zlib_version_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"revision", "Revision release number"}, + {"subversion", "Subversion release number"}, + {0} +}; + +static PyStructSequence_Desc zlib_version_desc = { + "zlib.zlib_version_info", /* name */ + zlib_version__doc__, /* doc */ + zlib_version_fields, /* fields */ + 4 +}; + +static PyObject * +make_zlib_version(PyTypeObject *type, const char *string) +{ + PyObject *version; + int pos = 0; + unsigned int major = 0, minor = 0, revision = 0, subversion = 0; + + sscanf(string, "%u.%u.%u.%u", &major, &minor, &revision, &subversion); + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetIntItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, PyLong_FromUnsignedLong(VALUE)); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetIntItem(major) + SetIntItem(minor) + SetIntItem(revision) + SetIntItem(subversion) +#undef SetIntItem + + return version; +} + +#ifdef ZLIBNG_VERSION +PyDoc_STRVAR(zlibng_version__doc__, + "zlib.zlibng_version_info\n\ +\n\ +Zlib-ng version information as a named tuple."); + +static PyStructSequence_Field zlibng_version_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"revision", "Revision release number"}, + {0} +}; + +static PyStructSequence_Desc zlibng_version_desc = { + "zlib.zlibng_version_info", /* name */ + zlibng_version__doc__, /* doc */ + zlibng_version_fields, /* fields */ + 3 +}; + +static PyObject * +make_zlibng_version(PyTypeObject *type, const char *string) +{ + PyObject *version; + int pos = 0; + unsigned int major = 0, minor = 0, revision = 0; + + sscanf(string, "%u.%u.%u", &major, &minor, &revision); + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + + #define SetIntItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, PyLong_FromUnsignedLong(VALUE)); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetIntItem(major) + SetIntItem(minor) + SetIntItem(revision) + #undef SetIntItem + + return version; +} +#endif // ZLIBNG_VERSION + + static PyMethodDef zlib_methods[] = { ZLIB_ADLER32_METHODDEF @@ -2255,10 +2356,16 @@ zlib_exec(PyObject *mod) #ifdef Z_TREES // 1.2.3.4, only for inflate ZLIB_ADD_INT_MACRO(Z_TREES); #endif + + /* zlib_version */ if (PyModule_Add(mod, "ZLIB_VERSION", PyUnicode_FromString(ZLIB_VERSION)) < 0) { return -1; } + if (PyModule_Add(mod, "zlib_version", + PyUnicode_FromString(zlibVersion())) < 0) { + return -1; + } if (PyModule_Add(mod, "ZLIB_RUNTIME_VERSION", PyUnicode_FromString(zlibVersion())) < 0) { return -1; @@ -2268,6 +2375,47 @@ zlib_exec(PyObject *mod) PyUnicode_FromString(ZLIBNG_VERSION)) < 0) { return -1; } + if (PyModule_Add(mod, "zlibng_version", + PyUnicode_FromString(zlibng_version())) < 0) { + return -1; + } +#endif + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&zlib_version_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(mod, "ZLIB_VERSION_INFO", + make_zlib_version(version_type, ZLIB_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(mod, "zlib_version_info", + make_zlib_version(version_type, zlibVersion())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); +#ifdef ZLIBNG_VERSION + version_type = PyStructSequence_NewType(&zlibng_version_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(mod, "ZLIBNG_VERSION_INFO", + make_zlibng_version(version_type, ZLIBNG_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(mod, "zlibng_version_info", + make_zlibng_version(version_type, zlibng_version())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); #endif return 0; } From 117547156fc0de5d3cc7fe8f029c6a2ea9777e97 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 May 2026 12:32:48 +0300 Subject: [PATCH 2/3] Remove runtime zlib-ng versions which are not available in compat mode. --- Doc/library/zlib.rst | 16 ---------------- Doc/whatsnew/3.16.rst | 3 +-- Lib/test/pythoninfo.py | 2 +- Lib/test/test_zlib.py | 14 -------------- Modules/zlibmodule.c | 10 ---------- 5 files changed, 2 insertions(+), 43 deletions(-) diff --git a/Doc/library/zlib.rst b/Doc/library/zlib.rst index da6ef40558ca40e..6c68c77dab91e65 100644 --- a/Doc/library/zlib.rst +++ b/Doc/library/zlib.rst @@ -526,14 +526,6 @@ the module: .. versionadded:: 3.14 -.. data:: zlibng_version - - The version string of the zlib-ng library actually loaded - by the interpreter. - - .. versionadded:: next - - .. data:: ZLIBNG_VERSION_INFO A named tuple containing the version of the zlib-ng library that was @@ -542,14 +534,6 @@ the module: .. versionadded:: next -.. data:: zlibng_version_info - - A named tuple containing the zlib-ng library version actually loaded - by the interpreter. - - .. versionadded:: next - - .. seealso:: Module :mod:`gzip` diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 02ef6e9e22f4820..08ee3034ae88d1d 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -150,8 +150,7 @@ zlib ---- * Added constants :const:`~zlib.ZLIB_VERSION_INFO`, - :const:`~zlib.zlib_version_info`, :const:`~zlib.zlibng_version`, - :const:`~zlib.ZLIBNG_VERSION_INFO`, and :const:`~zlib.zlibng_version_info`, + :const:`~zlib.zlib_version_info`, and :const:`~zlib.ZLIBNG_VERSION_INFO`, which provide information about the version of the zlib and the zlib-ng libraries in use. Added :const:`~zlib.zlib_version` as an alias of diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index d4245a6be57ff84..749705afc54e05d 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -660,7 +660,7 @@ def collect_zlib(info_add): except ImportError: return - attributes = ('ZLIB_VERSION', 'zlib_version', 'ZLIBNG_VERSION', 'zlibng_version') + attributes = ('ZLIB_VERSION', 'zlib_version', 'ZLIBNG_VERSION') copy_attributes(info_add, zlib, 'zlib.%s', attributes) diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py index ac89c8795ff8036..6a5a86024b2bcef 100644 --- a/Lib/test/test_zlib.py +++ b/Lib/test/test_zlib.py @@ -72,16 +72,6 @@ def test_library_version(self): zlib.ZLIB_VERSION.split('.')[0]) self.assertEqual(zlib.zlib_version_info[0], zlib.ZLIB_VERSION_INFO[0]) - if hasattr(zlib, 'ZLIBNG_VERSION'): - self.assertEqual(zlib.zlibng_version.split('.')[0], - zlib.ZLIBNG_VERSION.split('.')[0]) - self.assertEqual(zlib.zlibng_version_info[0], - zlib.ZLIBNG_VERSION_INFO[0]) - else: - self.assertNotHasAttr(zlib, 'ZLIBNG_VERSION') - self.assertNotHasAttr(zlib, 'zlibng_version') - self.assertNotHasAttr(zlib, 'ZLIBNG_VERSION_INFO') - self.assertNotHasAttr(zlib, 'zlibng_version_info') def _test_zlib_version(self, v): self.assertIsInstance(v[:], tuple) @@ -135,13 +125,9 @@ def _test_zlibng_version(self, v): def test_zlibng_version(self): if support.verbose: print(f'ZLIBNG_VERSION = {zlib.ZLIBNG_VERSION}', flush=True) - print(f'zlibng_version = {zlib.zlibng_version}', flush=True) print(f'ZLIBNG_VERSION_INFO = {zlib.ZLIBNG_VERSION_INFO}', flush=True) - print(f'zlibng_version_info = {zlib.zlibng_version_info}', flush=True) self._test_zlibng_version(zlib.ZLIBNG_VERSION_INFO) self.assertEqual(zlib.ZLIBNG_VERSION, '%d.%d.%d' % zlib.ZLIBNG_VERSION_INFO) - self._test_zlibng_version(zlib.zlibng_version_info) - self.assertEqual(zlib.zlibng_version, '%d.%d.%d' % zlib.zlibng_version_info) class ChecksumTestCase(unittest.TestCase): diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index b598051cecf3d60..09b0d1ca0ad9cad 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -2375,10 +2375,6 @@ zlib_exec(PyObject *mod) PyUnicode_FromString(ZLIBNG_VERSION)) < 0) { return -1; } - if (PyModule_Add(mod, "zlibng_version", - PyUnicode_FromString(zlibng_version())) < 0) { - return -1; - } #endif PyTypeObject *version_type; version_type = PyStructSequence_NewType(&zlib_version_desc); @@ -2409,12 +2405,6 @@ zlib_exec(PyObject *mod) Py_DECREF(version_type); return -1; } - if (PyModule_Add(mod, "zlibng_version_info", - make_zlibng_version(version_type, zlibng_version())) < 0) - { - Py_DECREF(version_type); - return -1; - } Py_DECREF(version_type); #endif return 0; From 0c719679744bb6b9726a1a6d8503666cbbc66c33 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 May 2026 13:29:56 +0300 Subject: [PATCH 3/3] Fix test_zlibng_version. --- Lib/test/test_zlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py index 6a5a86024b2bcef..874b0ce4495bc9b 100644 --- a/Lib/test/test_zlib.py +++ b/Lib/test/test_zlib.py @@ -107,7 +107,7 @@ def test_zlib_version(self): def _test_zlibng_version(self, v): self.assertIsInstance(v[:], tuple) - self.assertEqual(len(v), 4) + self.assertEqual(len(v), 3) self.assertIsInstance(v[0], int) self.assertIsInstance(v[1], int) self.assertIsInstance(v[2], int)