diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index fe2ed99189c..4fd94fd90d9 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -62,6 +62,23 @@ Some facts and figures: +------------------+---------------------------------------------+ | ``'r:xz'`` | Open for reading with lzma compression. | +------------------+---------------------------------------------+ + | ``'x'`` or | Create a tarfile exclusively without | + | ``'x:'`` | 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. | +------------------+---------------------------------------------+ @@ -82,9 +99,9 @@ Some facts and figures: If *fileobj* is specified, it is used as an alternative to a :term:`file object` opened in binary mode for *name*. It is supposed to be at position 0. - For modes ``'w:gz'``, ``'r:gz'``, ``'w:bz2'``, ``'r:bz2'``, :func:`tarfile.open` - accepts the keyword argument *compresslevel* to specify the compression level of - the file. + For modes ``'w:gz'``, ``'r:gz'``, ``'w:bz2'``, ``'r:bz2'``, ``'x:gz'``, + ``'x:bz2'``, :func:`tarfile.open` accepts the keyword argument + *compresslevel* to specify the compression level of the file. For special purposes, there is a second format for *mode*: ``'filemode|[compression]'``. :func:`tarfile.open` will return a :class:`TarFile` @@ -127,6 +144,8 @@ Some facts and figures: | | writing. | +-------------+--------------------------------------------+ + .. versionchanged:: 3.5 + The ``'x'`` (exclusive creation) mode was added. .. class:: TarFile @@ -252,8 +271,8 @@ be finalized; only the internally used file object will be closed. See the 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 @@ -292,12 +311,14 @@ be finalized; only the internally used file object will be closed. See the to be handled. The default settings will work for most users. See section :ref:`tar-unicode` for in-depth information. - .. 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.2 + Use ``'surrogateescape'`` as the default for the *errors* argument. + + .. versionchanged:: 3.5 + The ``'x'`` (exclusive creation) mode was added. .. classmethod:: TarFile.open(...) diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst index b475007e2e9..1de9e8fa5ea 100644 --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -334,6 +334,12 @@ socket :meth:`socket.socket.send`. (Contributed by Giampaolo Rodola' in :issue:`17552`.) +tarfile +------- + +* The :func:`tarfile.open` function now supports ``'x'`` (exclusive creation) + mode. (Contributed by Berker Peksag in :issue:`21717`.) + time ---- diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 06436ebc3dc..ea7a89a8d3c 100755 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1409,9 +1409,9 @@ class TarFile(object): 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] @@ -1524,6 +1524,15 @@ class TarFile(object): 'w:bz2' open for writing with bzip2 compression 'w:xz' open for writing with lzma compression + 'x' or '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 @@ -1582,7 +1591,7 @@ class TarFile(object): 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") @@ -1591,8 +1600,8 @@ class TarFile(object): 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 @@ -1600,8 +1609,8 @@ class TarFile(object): """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 @@ -1634,8 +1643,8 @@ class TarFile(object): """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 @@ -1663,8 +1672,8 @@ class TarFile(object): """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 @@ -1751,7 +1760,7 @@ class TarFile(object): addfile(). If given, `arcname' specifies an alternative name for the file in the archive. """ - self._check("aw") + self._check("awx") # When fileobj is given, replace name by # fileobj's real name. @@ -1885,7 +1894,7 @@ class TarFile(object): TarInfo object, if it returns None the TarInfo object will be excluded from the archive. """ - self._check("aw") + self._check("awx") if arcname is None: arcname = name @@ -1942,7 +1951,7 @@ class TarFile(object): On Windows platforms, `fileobj' should always be opened with mode 'rb' to avoid irritation about the file size. """ - self._check("aw") + self._check("awx") tarinfo = copy.copy(tarinfo) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index ef71f5ee263..01d1a922ee6 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1428,6 +1428,88 @@ class GNUWriteTest(unittest.TestCase): ("longlnk/" * 127) + "longlink_") +class CreateTest(TarTest, unittest.TestCase): + + prefix = "x:" + + file_path = os.path.join(TEMPDIR, "spameggs42") + + def setUp(self): + support.unlink(tmpname) + + @classmethod + def setUpClass(cls): + with open(cls.file_path, "wb") as fobj: + fobj.write(b"aaa") + + @classmethod + def tearDownClass(cls): + support.unlink(cls.file_path) + + def test_create(self): + with tarfile.open(tmpname, self.mode) as tobj: + tobj.add(self.file_path) + + with self.taropen(tmpname) as tobj: + names = tobj.getnames() + self.assertEqual(len(names), 1) + self.assertIn('spameggs42', names[0]) + + def test_create_existing(self): + with tarfile.open(tmpname, self.mode) as tobj: + tobj.add(self.file_path) + + with self.assertRaises(FileExistsError): + tobj = tarfile.open(tmpname, self.mode) + + with self.taropen(tmpname) as tobj: + names = tobj.getnames() + self.assertEqual(len(names), 1) + self.assertIn('spameggs42', names[0]) + + def test_create_taropen(self): + with self.taropen(tmpname, "x") as tobj: + tobj.add(self.file_path) + + with self.taropen(tmpname) as tobj: + names = tobj.getnames() + self.assertEqual(len(names), 1) + self.assertIn('spameggs42', names[0]) + + def test_create_existing_taropen(self): + with self.taropen(tmpname, "x") as tobj: + tobj.add(self.file_path) + + with self.assertRaises(FileExistsError): + with self.taropen(tmpname, "x"): + pass + + with self.taropen(tmpname) as tobj: + names = tobj.getnames() + self.assertEqual(len(names), 1) + self.assertIn("spameggs42", names[0]) + + +class GzipCreateTest(GzipTest, CreateTest): + pass + + +class Bz2CreateTest(Bz2Test, CreateTest): + pass + + +class LzmaCreateTest(LzmaTest, CreateTest): + pass + + +class CreateWithXModeTest(CreateTest): + + prefix = "x" + + test_create_taropen = None + test_create_existing_taropen = None + + @unittest.skipUnless(hasattr(os, "link"), "Missing hardlink implementation") class HardlinkTest(unittest.TestCase): # Test the creation of LNKTYPE (hardlink) members in an archive. diff --git a/Misc/NEWS b/Misc/NEWS index b497bd5bfe2..cae74df68b7 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -13,6 +13,8 @@ Core and Builtins Library ------- +- Issue #21717: tarfile.open() now supports 'x' (exclusive creation) mode. + - Issue #23344: marshal.dumps() is now 20-25% faster on average. - Issue #20416: marshal.dumps() with protocols 3 and 4 is now 40-50% faster on