Skip to content

Commit

Permalink
Issue #14061: Misc fixes and cleanups in archiving code in shutil.
Browse files Browse the repository at this point in the history
Imporoved the documentation and tests for make_archive() and unpack_archive().
Improved error handling when corresponding compress module is not available.
Brake circular dependency between shutil and tarfile modules.
  • Loading branch information
serhiy-storchaka committed Dec 16, 2016
2 parents cb5fe9c + 20cdffd commit 9bb6fe5
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 109 deletions.
44 changes: 24 additions & 20 deletions Doc/library/shutil.rst
Expand Up @@ -458,6 +458,10 @@ Archiving operations

.. versionadded:: 3.2

.. versionchanged:: 3.5
Added support for the *xztar* format.


High-level utilities to create and read compressed and archived files are also
provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.

Expand All @@ -467,8 +471,9 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.

*base_name* is the name of the file to create, including the path, minus
any format-specific extension. *format* is the archive format: one of
"zip", "tar", "bztar" (if the :mod:`bz2` module is available), "xztar"
(if the :mod:`lzma` module is available) or "gztar".
"zip" (if the :mod:`zlib` module is available), "tar", "gztar" (if the
:mod:`zlib` module is available), "bztar" (if the :mod:`bz2` module is
available), or "xztar" (if the :mod:`lzma` module is available).

*root_dir* is a directory that will be the root directory of the
archive; for example, we typically chdir into *root_dir* before creating the
Expand All @@ -491,9 +496,6 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.

The *verbose* argument is unused and deprecated.

.. versionchanged:: 3.5
Added support for the *xztar* format.


.. function:: get_archive_formats()

Expand All @@ -502,11 +504,11 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.

By default :mod:`shutil` provides these formats:

- *gztar*: gzip'ed tar-file
- *bztar*: bzip2'ed tar-file (if the :mod:`bz2` module is available.)
- *xztar*: xz'ed tar-file (if the :mod:`lzma` module is available.)
- *tar*: uncompressed tar file
- *zip*: ZIP file
- *zip*: ZIP file (if the :mod:`zlib` module is available).
- *tar*: uncompressed tar file.
- *gztar*: gzip'ed tar-file (if the :mod:`zlib` module is available).
- *bztar*: bzip2'ed tar-file (if the :mod:`bz2` module is available).
- *xztar*: xz'ed tar-file (if the :mod:`lzma` module is available).

You can register new formats or provide your own archiver for any existing
formats, by using :func:`register_archive_format`.
Expand Down Expand Up @@ -541,11 +543,12 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.
*extract_dir* is the name of the target directory where the archive is
unpacked. If not provided, the current working directory is used.

*format* is the archive format: one of "zip", "tar", or "gztar". 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, a :exc:`ValueError` is raised.
*format* is the archive format: one of "zip", "tar", "gztar", "bztar", or
"xztar". 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,
a :exc:`ValueError` is raised.


.. function:: register_unpack_format(name, extensions, function[, extra_args[, description]])
Expand Down Expand Up @@ -578,11 +581,12 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules.

By default :mod:`shutil` provides these formats:

- *gztar*: gzip'ed tar-file
- *bztar*: bzip2'ed tar-file (if the :mod:`bz2` module is available.)
- *xztar*: xz'ed tar-file (if the :mod:`lzma` module is available.)
- *tar*: uncompressed tar file
- *zip*: ZIP file
- *zip*: ZIP file (unpacking compressed files works only if corresponding
module is available).
- *tar*: uncompressed tar file.
- *gztar*: gzip'ed tar-file (if the :mod:`zlib` module is available).
- *bztar*: bzip2'ed tar-file (if the :mod:`bz2` module is available).
- *xztar*: xz'ed tar-file (if the :mod:`lzma` module is available).

You can register new formats or provide your own unpacker for any existing
formats, by using :func:`register_unpack_format`.
Expand Down
82 changes: 44 additions & 38 deletions Lib/shutil.py
Expand Up @@ -10,7 +10,13 @@
import fnmatch
import collections
import errno
import tarfile

try:
import zlib
del zlib
_ZLIB_SUPPORTED = True
except ImportError:
_ZLIB_SUPPORTED = False

try:
import bz2
Expand Down Expand Up @@ -602,23 +608,22 @@ def _make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0,
Returns the output filename.
"""
tar_compression = {'gzip': 'gz', None: ''}
compress_ext = {'gzip': '.gz'}

if _BZ2_SUPPORTED:
tar_compression['bzip2'] = 'bz2'
compress_ext['bzip2'] = '.bz2'

if _LZMA_SUPPORTED:
tar_compression['xz'] = 'xz'
compress_ext['xz'] = '.xz'

# flags for compression program, each element of list will be an argument
if compress is not None and compress not in compress_ext:
if compress is None:
tar_compression = ''
elif _ZLIB_SUPPORTED and compress == 'gzip':
tar_compression = 'gz'
elif _BZ2_SUPPORTED and compress == 'bzip2':
tar_compression = 'bz2'
elif _LZMA_SUPPORTED and compress == 'xz':
tar_compression = 'xz'
else:
raise ValueError("bad value for 'compress', or compression format not "
"supported : {0}".format(compress))

archive_name = base_name + '.tar' + compress_ext.get(compress, '')
import tarfile # late import for breaking circular dependency

compress_ext = '.' + tar_compression if compress else ''
archive_name = base_name + '.tar' + compress_ext
archive_dir = os.path.dirname(archive_name)

if archive_dir and not os.path.exists(archive_dir):
Expand All @@ -644,7 +649,7 @@ def _set_uid_gid(tarinfo):
return tarinfo

if not dry_run:
tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress])
tar = tarfile.open(archive_name, 'w|%s' % tar_compression)
try:
tar.add(base_dir, filter=_set_uid_gid)
finally:
Expand All @@ -655,13 +660,10 @@ def _set_uid_gid(tarinfo):
def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0, logger=None):
"""Create a zip file from all the files under 'base_dir'.
The output zip file will be named 'base_name' + ".zip". Uses either the
"zipfile" Python module (if available) or the InfoZIP "zip" utility
(if installed and found on the default search path). If neither tool is
available, raises ExecError. Returns the name of the output zip
file.
The output zip file will be named 'base_name' + ".zip". Returns the
name of the output zip file.
"""
import zipfile
import zipfile # late import for breaking circular dependency

zip_filename = base_name + ".zip"
archive_dir = os.path.dirname(base_name)
Expand Down Expand Up @@ -700,10 +702,13 @@ def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0, logger=None):
return zip_filename

_ARCHIVE_FORMATS = {
'gztar': (_make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"),
'tar': (_make_tarball, [('compress', None)], "uncompressed tar file"),
'zip': (_make_zipfile, [], "ZIP file")
}
}

if _ZLIB_SUPPORTED:
_ARCHIVE_FORMATS['gztar'] = (_make_tarball, [('compress', 'gzip')],
"gzip'ed tar-file")
_ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file")

if _BZ2_SUPPORTED:
_ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')],
Expand Down Expand Up @@ -752,8 +757,8 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
"""Create an archive file (eg. zip or tar).
'base_name' is the name of the file to create, minus any format-specific
extension; 'format' is the archive format: one of "zip", "tar", "bztar"
or "gztar".
extension; 'format' is the archive format: one of "zip", "tar", "gztar",
"bztar", or "xztar". Or any other registered format.
'root_dir' is a directory that will be the root directory of the
archive; ie. we typically chdir into 'root_dir' before creating the
Expand Down Expand Up @@ -866,10 +871,7 @@ def _ensure_directory(path):
def _unpack_zipfile(filename, extract_dir):
"""Unpack zip `filename` to `extract_dir`
"""
try:
import zipfile
except ImportError:
raise ReadError('zlib not supported, cannot unpack this archive.')
import zipfile # late import for breaking circular dependency

if not zipfile.is_zipfile(filename):
raise ReadError("%s is not a zip file" % filename)
Expand Down Expand Up @@ -903,6 +905,7 @@ def _unpack_zipfile(filename, extract_dir):
def _unpack_tarfile(filename, extract_dir):
"""Unpack tar/tar.gz/tar.bz2/tar.xz `filename` to `extract_dir`
"""
import tarfile # late import for breaking circular dependency
try:
tarobj = tarfile.open(filename)
except tarfile.TarError:
Expand All @@ -914,10 +917,13 @@ def _unpack_tarfile(filename, extract_dir):
tarobj.close()

_UNPACK_FORMATS = {
'gztar': (['.tar.gz', '.tgz'], _unpack_tarfile, [], "gzip'ed tar-file"),
'tar': (['.tar'], _unpack_tarfile, [], "uncompressed tar file"),
'zip': (['.zip'], _unpack_zipfile, [], "ZIP file")
}
'zip': (['.zip'], _unpack_zipfile, [], "ZIP file"),
}

if _ZLIB_SUPPORTED:
_UNPACK_FORMATS['gztar'] = (['.tar.gz', '.tgz'], _unpack_tarfile, [],
"gzip'ed tar-file")

if _BZ2_SUPPORTED:
_UNPACK_FORMATS['bztar'] = (['.tar.bz2', '.tbz2'], _unpack_tarfile, [],
Expand All @@ -942,10 +948,10 @@ def unpack_archive(filename, extract_dir=None, format=None):
`extract_dir` is the name of the target directory, where the archive
is unpacked. If not provided, the current working directory is used.
`format` is the archive format: one of "zip", "tar", or "gztar". 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.
`format` is the archive format: one of "zip", "tar", "gztar", "bztar",
or "xztar". 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.
"""
Expand Down

0 comments on commit 9bb6fe5

Please sign in to comment.