diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -56,16 +56,38 @@ Some facts and figures: | | compression. | +------------------+---------------------------------------------+ | ``'r:gz'`` | Open for reading with gzip compression. | +------------------+---------------------------------------------+ | ``'r:bz2'`` | Open for reading with bzip2 compression. | +------------------+---------------------------------------------+ | ``'r:xz'`` | Open for reading with lzma compression. | +------------------+---------------------------------------------+ + | ``'x'`` or | Create a tarfile with transparent | + | ``'x:*'`` | compression. | + | | Raise an :exc:`FileExistsError` exception | + | | if it is already exists. | + +------------------+---------------------------------------------+ + | ``'x:'`` | Create a tarfile exclusively without | + | | compression. | + | | Raise an :exc:`FileExistsError` exception | + | | if it is already exists. | + +------------------+---------------------------------------------+ + | ``'x:gz'`` | Create a tarfile with gzip compression. | + | | Raise an :exc:`FileExistsError` exception | + | | if it is already exists. | + +------------------+---------------------------------------------+ + | ``'x:bz2'`` | Create a tarfile with bzip2 compression. | + | | Raise an :exc:`FileExistsError` exception | + | | if it is already exists. | + +------------------+---------------------------------------------+ + | ``'x:xz'`` | Create a tarfile with lzma compression. | + | | Raise an :exc:`FileExistsError` exception | + | | if it is already exists. | + +------------------+---------------------------------------------+ | ``'a' or 'a:'`` | Open for appending with no compression. The | | | file is created if it does not exist. | +------------------+---------------------------------------------+ | ``'w' or 'w:'`` | Open for uncompressed writing. | +------------------+---------------------------------------------+ | ``'w:gz'`` | Open for gzip compressed writing. | +------------------+---------------------------------------------+ | ``'w:bz2'`` | Open for bzip2 compressed writing. | @@ -121,16 +143,18 @@ Some facts and figures: +-------------+--------------------------------------------+ | ``'w|bz2'`` | Open a bzip2 compressed *stream* for | | | writing. | +-------------+--------------------------------------------+ | ``'w|xz'`` | Open an lzma compressed *stream* for | | | writing. | +-------------+--------------------------------------------+ + .. versionchanged:: 3.5 + Exclusive mode support was added. .. class:: TarFile Class for reading and writing tar archives. Do not use this class directly, better use :func:`tarfile.open` instead. See :ref:`tarfile-objects`. .. function:: is_tarfile(name) @@ -244,18 +268,18 @@ be finalized; only the internally used f All following arguments are optional and can be accessed as instance attributes as well. *name* is the pathname of the archive. It can be omitted if *fileobj* is given. In this case, the file object's :attr:`name` attribute is used if it exists. *mode* is either ``'r'`` to read from an existing archive, ``'a'`` to append - data to an existing file or ``'w'`` to create a new file overwriting an existing - one. + data to an existing file, ``'w'`` to create a new file overwriting an existing + one or ``'x'`` to create a new file only if it's not exists. If *fileobj* is given, it is used for reading or writing data. If it can be determined, *mode* is overridden by *fileobj*'s mode. *fileobj* will be used from position 0. .. note:: *fileobj* is not closed, when :class:`TarFile` is closed. @@ -284,22 +308,24 @@ be finalized; only the internally used f exceptions. If ``2``, all *non-fatal* errors are raised as :exc:`TarError` exceptions as well. The *encoding* and *errors* arguments define the character encoding to be used for reading or writing the archive and how conversion errors are going to be handled. The default settings will work for most users. See section :ref:`tar-unicode` for in-depth information. + The *pax_headers* argument is an optional dictionary of strings which + will be added as a pax global header if *format* is :const:`PAX_FORMAT`. + .. versionchanged:: 3.2 Use ``'surrogateescape'`` as the default for the *errors* argument. - The *pax_headers* argument is an optional dictionary of strings which - will be added as a pax global header if *format* is :const:`PAX_FORMAT`. - + .. versionchanged:: 3.5 + Exclusive mode support was added. .. classmethod:: TarFile.open(...) Alternative constructor. The :func:`tarfile.open` function is actually a shortcut to this classmethod. .. method:: TarFile.getmember(name) diff --git a/Lib/tarfile.py b/Lib/tarfile.py --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1404,19 +1404,19 @@ class TarFile(object): """Open an (uncompressed) tar archive `name'. `mode' is either 'r' to read from an existing archive, 'a' to append data to an existing file or 'w' to create a new file overwriting an existing one. `mode' defaults to 'r'. If `fileobj' is given, it is used for reading or writing data. If it can be determined, `mode' is overridden by `fileobj's mode. `fileobj' is not closed, when TarFile is closed. """ - modes = {"r": "rb", "a": "r+b", "w": "wb"} + modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"} if mode not in modes: - raise ValueError("mode must be 'r', 'a' or 'w'") + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") self.mode = mode self._mode = modes[mode] if not fileobj: if self.mode == "a" and not os.path.exists(name): # Create nonexistent files in append mode. self.mode = "w" self._mode = "wb" @@ -1518,16 +1518,27 @@ class TarFile(object): 'r:bz2' open for reading with bzip2 compression 'r:xz' open for reading with lzma compression 'a' or 'a:' open for appending, creating the file if necessary 'w' or 'w:' open for writing without compression 'w:gz' open for writing with gzip compression 'w:bz2' open for writing with bzip2 compression 'w:xz' open for writing with lzma compression + 'x' or 'x:*' create a tarfile with transparent compression, raise an + exception if the file is already created + 'x:' create a tarfile exclusively without compression, raise + an exception if the file is already created + 'x:gz' create an gzip compressed tarfile, raise an exception + if the file is already created + 'x:bz2' create an bzip2 compressed tarfile, raise an exception + if the file is already created + 'x:xz' create an lzma compressed tarfile, raise an exception + if the file is already created + 'r|*' open a stream of tar blocks with transparent compression 'r|' open an uncompressed stream of tar blocks for reading 'r|gz' open a gzip compressed stream of tar blocks 'r|bz2' open a bzip2 compressed stream of tar blocks 'r|xz' open an lzma compressed stream of tar blocks 'w|' open an uncompressed stream for writing 'w|gz' open a gzip compressed stream for writing 'w|bz2' open a bzip2 compressed stream for writing @@ -1576,36 +1587,36 @@ class TarFile(object): try: t = cls(name, filemode, stream, **kwargs) except: stream.close() raise t._extfileobj = False return t - elif mode in ("a", "w"): + elif mode in ("a", "w", "x"): return cls.taropen(name, mode, fileobj, **kwargs) raise ValueError("undiscernible mode") @classmethod def taropen(cls, name, mode="r", fileobj=None, **kwargs): """Open uncompressed tar archive name for reading or writing. """ - if mode not in ("r", "a", "w"): - raise ValueError("mode must be 'r', 'a' or 'w'") + if mode not in ("r", "a", "w", "x"): + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") return cls(name, mode, fileobj, **kwargs) @classmethod def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): """Open gzip compressed tar archive name for reading or writing. Appending is not allowed. """ - if mode not in ("r", "w"): - raise ValueError("mode must be 'r' or 'w'") + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") try: import gzip gzip.GzipFile except (ImportError, AttributeError): raise CompressionError("gzip module is not available") try: @@ -1628,18 +1639,18 @@ class TarFile(object): t._extfileobj = False return t @classmethod def bz2open(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): """Open bzip2 compressed tar archive name for reading or writing. Appending is not allowed. """ - if mode not in ("r", "w"): - raise ValueError("mode must be 'r' or 'w'.") + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") try: import bz2 except ImportError: raise CompressionError("bz2 module is not available") fileobj = bz2.BZ2File(fileobj or name, mode, compresslevel=compresslevel) @@ -1657,18 +1668,18 @@ class TarFile(object): t._extfileobj = False return t @classmethod def xzopen(cls, name, mode="r", fileobj=None, preset=None, **kwargs): """Open lzma compressed tar archive name for reading or writing. Appending is not allowed. """ - if mode not in ("r", "w"): - raise ValueError("mode must be 'r' or 'w'") + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") try: import lzma except ImportError: raise CompressionError("lzma module is not available") fileobj = lzma.LZMAFile(fileobj or name, mode, preset=preset) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -77,16 +77,49 @@ class ReadTest(TarTest): def setUp(self): self.tar = tarfile.open(self.tarname, mode=self.mode, encoding="iso8859-1") def tearDown(self): self.tar.close() +class CreateTest(TarTest): + + prefix = "x:" + + +class UstarCreateTest(CreateTest, unittest.TestCase): + + tarname = os.path.join(TEMPDIR, "testtar_x.tar") + + def test_create(self): + with tarfile.open(self.tarname, self.mode): + pass + + with self.assertRaises(FileExistsError): + with tarfile.open(self.tarname, self.mode): + pass + + +class GzipUstarCreateTest(GzipTest, UstarCreateTest): + + tarname = os.path.join(TEMPDIR, "testtar_x.tar.gz") + + +class Bz2UstarCreateTest(Bz2Test, UstarCreateTest): + + tarname = os.path.join(TEMPDIR, "testtar_x.tar.bz2") + + +class LzmaUstarCreateTest(LzmaTest, UstarCreateTest): + + tarname = os.path.join(TEMPDIR, "testtar_x.tar.xz") + + class UstarReadTest(ReadTest, unittest.TestCase): def test_fileobj_regular_file(self): tarinfo = self.tar.getmember("ustar/regtype") with self.tar.extractfile(tarinfo) as fobj: data = fobj.read() self.assertEqual(len(data), tarinfo.size, "regular file extraction failed")