[Python-checkins] gh-74696: Pass root_dir to custom archivers which support it (GH-94251)

serhiy-storchaka webhook-mailer at python.org
Wed Oct 5 05:49:21 EDT 2022


https://github.com/python/cpython/commit/e3ef400be74e027eaa19f7677af986fb05dd3334
commit: e3ef400be74e027eaa19f7677af986fb05dd3334
branch: main
author: Serhiy Storchaka <storchaka at gmail.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2022-10-05T12:48:59+03:00
summary:

gh-74696: Pass root_dir to custom archivers which support it (GH-94251)

Co-authored-by: Éric <merwok at netwok.org>

files:
A Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst
M Doc/library/shutil.rst
M Doc/whatsnew/3.12.rst
M Lib/shutil.py
M Lib/test/test_shutil.py

diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst
index 8f1668f76b90..b33dbe21b1fa 100644
--- a/Doc/library/shutil.rst
+++ b/Doc/library/shutil.rst
@@ -575,9 +575,10 @@ provided.  They rely on the :mod:`zipfile` and :mod:`tarfile` modules.
    .. note::
 
       This function is not thread-safe when custom archivers registered
-      with :func:`register_archive_format` are used.  In this case it
+      with :func:`register_archive_format` do not support the *root_dir*
+      argument.  In this case it
       temporarily changes the current working directory of the process
-      to perform archiving.
+      to *root_dir* to perform archiving.
 
    .. versionchanged:: 3.8
       The modern pax (POSIX.1-2001) format is now used instead of
@@ -614,12 +615,21 @@ provided.  They rely on the :mod:`zipfile` and :mod:`tarfile` modules.
    Further arguments are passed as keyword arguments: *owner*, *group*,
    *dry_run* and *logger* (as passed in :func:`make_archive`).
 
+   If *function* has the custom attribute ``function.supports_root_dir`` set to ``True``,
+   the *root_dir* argument is passed as a keyword argument.
+   Otherwise the current working directory of the process is temporarily
+   changed to *root_dir* before calling *function*.
+   In this case :func:`make_archive` is not thread-safe.
+
    If given, *extra_args* is a sequence of ``(name, value)`` pairs that will be
    used as extra keywords arguments when the archiver callable is used.
 
    *description* is used by :func:`get_archive_formats` which returns the
    list of archivers.  Defaults to an empty string.
 
+   .. versionchanged:: 3.12
+      Added support for functions supporting the *root_dir* argument.
+
 
 .. function:: unregister_archive_format(name)
 
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 052507a4873f..62ec2de2e78c 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -127,6 +127,15 @@ os
   for a process with :func:`os.pidfd_open` in non-blocking mode.
   (Contributed by Kumar Aditya in :gh:`93312`.)
 
+shutil
+------
+
+* :func:`shutil.make_archive` now passes the *root_dir* argument to custom
+  archivers which support it.
+  In this case it no longer temporarily changes the current working directory
+  of the process to *root_dir* to perform archiving.
+  (Contributed by Serhiy Storchaka in :gh:`74696`.)
+
 
 sqlite3
 -------
diff --git a/Lib/shutil.py b/Lib/shutil.py
index b49437cd1f3e..ac1dd530528c 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -1023,28 +1023,30 @@ def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0,
         zip_filename = os.path.abspath(zip_filename)
     return zip_filename
 
+_make_tarball.supports_root_dir = True
+_make_zipfile.supports_root_dir = True
+
 # Maps the name of the archive format to a tuple containing:
 # * the archiving function
 # * extra keyword arguments
 # * description
-# * does it support the root_dir argument?
 _ARCHIVE_FORMATS = {
     'tar':   (_make_tarball, [('compress', None)],
-              "uncompressed tar file", True),
+              "uncompressed tar file"),
 }
 
 if _ZLIB_SUPPORTED:
     _ARCHIVE_FORMATS['gztar'] = (_make_tarball, [('compress', 'gzip')],
-                                "gzip'ed tar-file", True)
-    _ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file", True)
+                                "gzip'ed tar-file")
+    _ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file")
 
 if _BZ2_SUPPORTED:
     _ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')],
-                                "bzip2'ed tar-file", True)
+                                "bzip2'ed tar-file")
 
 if _LZMA_SUPPORTED:
     _ARCHIVE_FORMATS['xztar'] = (_make_tarball, [('compress', 'xz')],
-                                "xz'ed tar-file", True)
+                                "xz'ed tar-file")
 
 def get_archive_formats():
     """Returns a list of supported formats for archiving and unarchiving.
@@ -1075,7 +1077,7 @@ def register_archive_format(name, function, extra_args=None, description=''):
         if not isinstance(element, (tuple, list)) or len(element) !=2:
             raise TypeError('extra_args elements are : (arg_name, value)')
 
-    _ARCHIVE_FORMATS[name] = (function, extra_args, description, False)
+    _ARCHIVE_FORMATS[name] = (function, extra_args, description)
 
 def unregister_archive_format(name):
     del _ARCHIVE_FORMATS[name]
@@ -1114,10 +1116,10 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
     if base_dir is None:
         base_dir = os.curdir
 
-    support_root_dir = format_info[3]
+    supports_root_dir = getattr(func, 'supports_root_dir', False)
     save_cwd = None
     if root_dir is not None:
-        if support_root_dir:
+        if supports_root_dir:
             # Support path-like base_name here for backwards-compatibility.
             base_name = os.fspath(base_name)
             kwargs['root_dir'] = root_dir
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index a2c4ab508195..6789fe4cc72e 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -1568,28 +1568,65 @@ def test_tarfile_root_owner(self):
         finally:
             archive.close()
 
+    def test_make_archive_cwd_default(self):
+        current_dir = os.getcwd()
+        def archiver(base_name, base_dir, **kw):
+            self.assertNotIn('root_dir', kw)
+            self.assertEqual(base_name, 'basename')
+            self.assertEqual(os.getcwd(), current_dir)
+            raise RuntimeError()
+
+        register_archive_format('xxx', archiver, [], 'xxx file')
+        try:
+            with no_chdir:
+                with self.assertRaises(RuntimeError):
+                    make_archive('basename', 'xxx')
+            self.assertEqual(os.getcwd(), current_dir)
+        finally:
+            unregister_archive_format('xxx')
+
     def test_make_archive_cwd(self):
         current_dir = os.getcwd()
         root_dir = self.mkdtemp()
-        def _breaks(*args, **kw):
+        def archiver(base_name, base_dir, **kw):
+            self.assertNotIn('root_dir', kw)
+            self.assertEqual(base_name, os.path.join(current_dir, 'basename'))
+            self.assertEqual(os.getcwd(), root_dir)
             raise RuntimeError()
         dirs = []
         def _chdir(path):
             dirs.append(path)
             orig_chdir(path)
 
-        register_archive_format('xxx', _breaks, [], 'xxx file')
+        register_archive_format('xxx', archiver, [], 'xxx file')
         try:
             with support.swap_attr(os, 'chdir', _chdir) as orig_chdir:
-                try:
-                    make_archive('xxx', 'xxx', root_dir=root_dir)
-                except Exception:
-                    pass
+                with self.assertRaises(RuntimeError):
+                    make_archive('basename', 'xxx', root_dir=root_dir)
             self.assertEqual(os.getcwd(), current_dir)
             self.assertEqual(dirs, [root_dir, current_dir])
         finally:
             unregister_archive_format('xxx')
 
+    def test_make_archive_cwd_supports_root_dir(self):
+        current_dir = os.getcwd()
+        root_dir = self.mkdtemp()
+        def archiver(base_name, base_dir, **kw):
+            self.assertEqual(base_name, 'basename')
+            self.assertEqual(kw['root_dir'], root_dir)
+            self.assertEqual(os.getcwd(), current_dir)
+            raise RuntimeError()
+        archiver.supports_root_dir = True
+
+        register_archive_format('xxx', archiver, [], 'xxx file')
+        try:
+            with no_chdir:
+                with self.assertRaises(RuntimeError):
+                    make_archive('basename', 'xxx', root_dir=root_dir)
+            self.assertEqual(os.getcwd(), current_dir)
+        finally:
+            unregister_archive_format('xxx')
+
     def test_make_tarfile_in_curdir(self):
         # Issue #21280
         root_dir = self.mkdtemp()
diff --git a/Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst b/Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst
new file mode 100644
index 000000000000..48beaff59a16
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst
@@ -0,0 +1,2 @@
+:func:`shutil.make_archive` now passes the *root_dir* argument to custom
+archivers which support it.



More information about the Python-checkins mailing list