Skip to content

Commit c34bef6

Browse files
serhiy-storchakamerwok
authored andcommitted
pythongh-74696: Pass root_dir to custom archivers which support it (pythonGH-94251)
Co-authored-by: Éric <[email protected]>
1 parent 45b9d05 commit c34bef6

File tree

5 files changed

+77
-17
lines changed

5 files changed

+77
-17
lines changed

Doc/library/shutil.rst

+12-2
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,10 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.
575575
.. note::
576576

577577
This function is not thread-safe when custom archivers registered
578-
with :func:`register_archive_format` are used. In this case it
578+
with :func:`register_archive_format` do not support the *root_dir*
579+
argument. In this case it
579580
temporarily changes the current working directory of the process
580-
to perform archiving.
581+
to *root_dir* to perform archiving.
581582

582583
.. versionchanged:: 3.8
583584
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.
614615
Further arguments are passed as keyword arguments: *owner*, *group*,
615616
*dry_run* and *logger* (as passed in :func:`make_archive`).
616617

618+
If *function* has the custom attribute ``function.supports_root_dir`` set to ``True``,
619+
the *root_dir* argument is passed as a keyword argument.
620+
Otherwise the current working directory of the process is temporarily
621+
changed to *root_dir* before calling *function*.
622+
In this case :func:`make_archive` is not thread-safe.
623+
617624
If given, *extra_args* is a sequence of ``(name, value)`` pairs that will be
618625
used as extra keywords arguments when the archiver callable is used.
619626

620627
*description* is used by :func:`get_archive_formats` which returns the
621628
list of archivers. Defaults to an empty string.
622629

630+
.. versionchanged:: 3.12
631+
Added support for functions supporting the *root_dir* argument.
632+
623633

624634
.. function:: unregister_archive_format(name)
625635

Doc/whatsnew/3.12.rst

+9
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ os
127127
for a process with :func:`os.pidfd_open` in non-blocking mode.
128128
(Contributed by Kumar Aditya in :gh:`93312`.)
129129

130+
shutil
131+
------
132+
133+
* :func:`shutil.make_archive` now passes the *root_dir* argument to custom
134+
archivers which support it.
135+
In this case it no longer temporarily changes the current working directory
136+
of the process to *root_dir* to perform archiving.
137+
(Contributed by Serhiy Storchaka in :gh:`74696`.)
138+
130139

131140
sqlite3
132141
-------

Lib/shutil.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -1023,28 +1023,30 @@ def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0,
10231023
zip_filename = os.path.abspath(zip_filename)
10241024
return zip_filename
10251025

1026+
_make_tarball.supports_root_dir = True
1027+
_make_zipfile.supports_root_dir = True
1028+
10261029
# Maps the name of the archive format to a tuple containing:
10271030
# * the archiving function
10281031
# * extra keyword arguments
10291032
# * description
1030-
# * does it support the root_dir argument?
10311033
_ARCHIVE_FORMATS = {
10321034
'tar': (_make_tarball, [('compress', None)],
1033-
"uncompressed tar file", True),
1035+
"uncompressed tar file"),
10341036
}
10351037

10361038
if _ZLIB_SUPPORTED:
10371039
_ARCHIVE_FORMATS['gztar'] = (_make_tarball, [('compress', 'gzip')],
1038-
"gzip'ed tar-file", True)
1039-
_ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file", True)
1040+
"gzip'ed tar-file")
1041+
_ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file")
10401042

10411043
if _BZ2_SUPPORTED:
10421044
_ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')],
1043-
"bzip2'ed tar-file", True)
1045+
"bzip2'ed tar-file")
10441046

10451047
if _LZMA_SUPPORTED:
10461048
_ARCHIVE_FORMATS['xztar'] = (_make_tarball, [('compress', 'xz')],
1047-
"xz'ed tar-file", True)
1049+
"xz'ed tar-file")
10481050

10491051
def get_archive_formats():
10501052
"""Returns a list of supported formats for archiving and unarchiving.
@@ -1075,7 +1077,7 @@ def register_archive_format(name, function, extra_args=None, description=''):
10751077
if not isinstance(element, (tuple, list)) or len(element) !=2:
10761078
raise TypeError('extra_args elements are : (arg_name, value)')
10771079

1078-
_ARCHIVE_FORMATS[name] = (function, extra_args, description, False)
1080+
_ARCHIVE_FORMATS[name] = (function, extra_args, description)
10791081

10801082
def unregister_archive_format(name):
10811083
del _ARCHIVE_FORMATS[name]
@@ -1114,10 +1116,10 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
11141116
if base_dir is None:
11151117
base_dir = os.curdir
11161118

1117-
support_root_dir = format_info[3]
1119+
supports_root_dir = getattr(func, 'supports_root_dir', False)
11181120
save_cwd = None
11191121
if root_dir is not None:
1120-
if support_root_dir:
1122+
if supports_root_dir:
11211123
# Support path-like base_name here for backwards-compatibility.
11221124
base_name = os.fspath(base_name)
11231125
kwargs['root_dir'] = root_dir

Lib/test/test_shutil.py

+43-6
Original file line numberDiff line numberDiff line change
@@ -1568,28 +1568,65 @@ def test_tarfile_root_owner(self):
15681568
finally:
15691569
archive.close()
15701570

1571+
def test_make_archive_cwd_default(self):
1572+
current_dir = os.getcwd()
1573+
def archiver(base_name, base_dir, **kw):
1574+
self.assertNotIn('root_dir', kw)
1575+
self.assertEqual(base_name, 'basename')
1576+
self.assertEqual(os.getcwd(), current_dir)
1577+
raise RuntimeError()
1578+
1579+
register_archive_format('xxx', archiver, [], 'xxx file')
1580+
try:
1581+
with no_chdir:
1582+
with self.assertRaises(RuntimeError):
1583+
make_archive('basename', 'xxx')
1584+
self.assertEqual(os.getcwd(), current_dir)
1585+
finally:
1586+
unregister_archive_format('xxx')
1587+
15711588
def test_make_archive_cwd(self):
15721589
current_dir = os.getcwd()
15731590
root_dir = self.mkdtemp()
1574-
def _breaks(*args, **kw):
1591+
def archiver(base_name, base_dir, **kw):
1592+
self.assertNotIn('root_dir', kw)
1593+
self.assertEqual(base_name, os.path.join(current_dir, 'basename'))
1594+
self.assertEqual(os.getcwd(), root_dir)
15751595
raise RuntimeError()
15761596
dirs = []
15771597
def _chdir(path):
15781598
dirs.append(path)
15791599
orig_chdir(path)
15801600

1581-
register_archive_format('xxx', _breaks, [], 'xxx file')
1601+
register_archive_format('xxx', archiver, [], 'xxx file')
15821602
try:
15831603
with support.swap_attr(os, 'chdir', _chdir) as orig_chdir:
1584-
try:
1585-
make_archive('xxx', 'xxx', root_dir=root_dir)
1586-
except Exception:
1587-
pass
1604+
with self.assertRaises(RuntimeError):
1605+
make_archive('basename', 'xxx', root_dir=root_dir)
15881606
self.assertEqual(os.getcwd(), current_dir)
15891607
self.assertEqual(dirs, [root_dir, current_dir])
15901608
finally:
15911609
unregister_archive_format('xxx')
15921610

1611+
def test_make_archive_cwd_supports_root_dir(self):
1612+
current_dir = os.getcwd()
1613+
root_dir = self.mkdtemp()
1614+
def archiver(base_name, base_dir, **kw):
1615+
self.assertEqual(base_name, 'basename')
1616+
self.assertEqual(kw['root_dir'], root_dir)
1617+
self.assertEqual(os.getcwd(), current_dir)
1618+
raise RuntimeError()
1619+
archiver.supports_root_dir = True
1620+
1621+
register_archive_format('xxx', archiver, [], 'xxx file')
1622+
try:
1623+
with no_chdir:
1624+
with self.assertRaises(RuntimeError):
1625+
make_archive('basename', 'xxx', root_dir=root_dir)
1626+
self.assertEqual(os.getcwd(), current_dir)
1627+
finally:
1628+
unregister_archive_format('xxx')
1629+
15931630
def test_make_tarfile_in_curdir(self):
15941631
# Issue #21280
15951632
root_dir = self.mkdtemp()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`shutil.make_archive` now passes the *root_dir* argument to custom
2+
archivers which support it.

0 commit comments

Comments
 (0)