https://github.com/python/cpython/commit/025fe552c462796b32d57c34987355d04aa55ed5
commit: 025fe552c462796b32d57c34987355d04aa55ed5
branch: main
author: Emma Smith <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-10-30T10:35:44Z
summary:

gh-84027: Add `whl` as an unpack format in `shutil` (#140385)

Co-authored-by: Emma Smith <[email protected]>
Co-authored-by: Stan Ulbrych <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-10-20-13-08-04.gh-issue-84027.HjBO0B.rst
M Doc/library/shutil.rst
M Doc/whatsnew/3.16.rst
M Lib/shutil.py
M Lib/test/test_shutil.py

diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst
index 8f3c90719a9a15..2d3edd8e5aebfc 100644
--- a/Doc/library/shutil.rst
+++ b/Doc/library/shutil.rst
@@ -753,7 +753,7 @@ provided.  They rely on the :mod:`zipfile` and 
:mod:`tarfile` modules.
    unpacked. If not provided, the current working directory is used.
 
    *format* is the archive format: one of "zip", "tar", "gztar", "bztar",
-   "xztar", or "zstdtar".  Or any other format registered with
+   "xztar", "zstdtar", or "whl".  Or any other format registered with
    :func:`register_unpack_format`.  If not provided, :func:`unpack_archive`
    will use the archive file name extension and see if an unpacker was
    registered for that extension.  In case none is found,
@@ -786,6 +786,9 @@ provided.  They rely on the :mod:`zipfile` and 
:mod:`tarfile` modules.
    .. versionchanged:: 3.12
       Added the *filter* argument.
 
+   .. versionchanged:: next
+      Added the "whl" format.
+
 .. function:: register_unpack_format(name, extensions, function[, extra_args[, 
description]])
 
    Registers an unpack format. *name* is the name of the format and
@@ -827,10 +830,16 @@ provided.  They rely on the :mod:`zipfile` and 
:mod:`tarfile` modules.
    - *xztar*: xz'ed tar-file (if the :mod:`lzma` module is available).
    - *zstdtar*: Zstandard compressed tar-file (if the :mod:`compression.zstd`
      module is available).
+   - *whl*: `wheel file 
<https://packaging.python.org/en/latest/discussions/package-formats/#what-is-a-wheel>`__
+     (unpacking compressed files works only if the corresponding module is
+     available).
 
    You can register new formats or provide your own unpacker for any existing
    formats, by using :func:`register_unpack_format`.
 
+   .. versionchanged:: next
+      Added the *whl* format.
+
 
 .. _shutil-archiving-example:
 
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index ce001d07731bcc..99fd00938ab8bc 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -681,6 +681,9 @@ shutil
   :func:`shutil.copystat`, e.g. when copying a read-only tree.
   (Contributed by Ɓukasz Langa in :gh:`89721`.)
 
+* :func:`shutil.unpack_archive` can now unpack wheel files using the ``whl`` 
format.
+  (Contributed by Emma Harper Smith and Stan Ulbrych in :gh:`84027`.)
+
 
 socket
 ------
diff --git a/Lib/shutil.py b/Lib/shutil.py
index f678601e6758eb..f0057164c7ce35 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -1376,6 +1376,7 @@ def _unpack_tarfile(filename, extract_dir, *, 
filter=None):
 _UNPACK_FORMATS = {
     'tar':   (['.tar'], _unpack_tarfile, [], "uncompressed tar file"),
     'zip':   (['.zip'], _unpack_zipfile, [], "ZIP file"),
+    'whl':   (['.whl'], _unpack_zipfile, [], "Wheel file"),
 }
 
 if _ZLIB_SUPPORTED:
@@ -1410,9 +1411,9 @@ def unpack_archive(filename, extract_dir=None, 
format=None, *, filter=None):
     is unpacked. If not provided, the current working directory is used.
 
     `format` is the archive format: one of "zip", "tar", "gztar", "bztar",
-    "xztar", or "zstdtar".  Or any other registered format.  If not provided,
-    unpack_archive will use the filename extension and see if an unpacker
-    was registered for that extension.
+    "xztar", "zstdtar", or "whl".  Or any other registered format.  If not
+    provided, unpack_archive will use the filename extension and see if an
+    unpacker was registered for that extension.
 
     In case none is found, a ValueError is raised.
 
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index 1a1a77ecdaeb76..67f45167e680a9 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -2287,12 +2287,18 @@ def check_unpack_archive(self, format, **kwargs):
         self.check_unpack_archive_with_converter(format, FakePath, **kwargs)
 
     def check_unpack_archive_with_converter(self, format, converter, **kwargs):
+        make_format = kwargs.pop("make_format", format)
+        extension = kwargs.pop("extension", None)
         root_dir, base_dir = self._create_files()
         expected = rlistdir(root_dir)
         expected.remove('outer')
 
         base_name = os.path.join(self.mkdtemp(), 'archive')
-        filename = make_archive(base_name, format, root_dir, base_dir)
+        filename = make_archive(base_name, make_format, root_dir, base_dir)
+        if extension is not None:
+            new_filename = os.path.splitext(filename)[0] + extension
+            os.rename(filename, new_filename)
+            filename = new_filename
 
         # let's try to unpack it now
         tmpdir2 = self.mkdtemp()
@@ -2340,6 +2346,13 @@ def test_unpack_archive_zip(self):
         with self.assertRaises(TypeError):
             self.check_unpack_archive('zip', filter='data')
 
+    @support.requires_zlib()
+    def test_unpack_archive_whl(self):
+        self.check_unpack_archive('whl', make_format='zip', extension='.whl')
+        with self.assertRaises(TypeError):
+            self.check_unpack_archive('whl', filter='data', make_format='zip',
+                                      extension='.whl')
+
     def test_unpack_archive_zip_badpaths(self):
         srcdir = self.mkdtemp()
         zipname = os.path.join(srcdir, 'test.zip')
diff --git 
a/Misc/NEWS.d/next/Library/2025-10-20-13-08-04.gh-issue-84027.HjBO0B.rst 
b/Misc/NEWS.d/next/Library/2025-10-20-13-08-04.gh-issue-84027.HjBO0B.rst
new file mode 100644
index 00000000000000..060eeabd903bbb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-20-13-08-04.gh-issue-84027.HjBO0B.rst
@@ -0,0 +1 @@
+:mod:`shutil` now can unpack wheel packages using the ``whl`` format.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to