Issue #21717: tarfile.open() now supports 'x' (exclusive creation) mode.

This commit is contained in:
Berker Peksag 2015-02-13 21:02:12 +02:00
parent 6767757589
commit 0fe6325acf
5 changed files with 142 additions and 22 deletions

View File

@ -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(...)

View File

@ -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
----

View File

@ -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)

View File

@ -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.

View File

@ -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