Kaydet (Commit) 0fe6325a authored tarafından Berker Peksag's avatar Berker Peksag

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

üst 67677575
...@@ -62,6 +62,23 @@ Some facts and figures: ...@@ -62,6 +62,23 @@ Some facts and figures:
+------------------+---------------------------------------------+ +------------------+---------------------------------------------+
| ``'r:xz'`` | Open for reading with lzma compression. | | ``'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 | | ``'a' or 'a:'`` | Open for appending with no compression. The |
| | file is created if it does not exist. | | | file is created if it does not exist. |
+------------------+---------------------------------------------+ +------------------+---------------------------------------------+
...@@ -82,9 +99,9 @@ Some facts and figures: ...@@ -82,9 +99,9 @@ Some facts and figures:
If *fileobj* is specified, it is used as an alternative to a :term:`file object` 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. 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` For modes ``'w:gz'``, ``'r:gz'``, ``'w:bz2'``, ``'r:bz2'``, ``'x:gz'``,
accepts the keyword argument *compresslevel* to specify the compression level of ``'x:bz2'``, :func:`tarfile.open` accepts the keyword argument
the file. *compresslevel* to specify the compression level of the file.
For special purposes, there is a second format for *mode*: For special purposes, there is a second format for *mode*:
``'filemode|[compression]'``. :func:`tarfile.open` will return a :class:`TarFile` ``'filemode|[compression]'``. :func:`tarfile.open` will return a :class:`TarFile`
...@@ -127,6 +144,8 @@ Some facts and figures: ...@@ -127,6 +144,8 @@ Some facts and figures:
| | writing. | | | writing. |
+-------------+--------------------------------------------+ +-------------+--------------------------------------------+
.. versionchanged:: 3.5
The ``'x'`` (exclusive creation) mode was added.
.. class:: TarFile .. class:: TarFile
...@@ -252,8 +271,8 @@ be finalized; only the internally used file object will be closed. See the ...@@ -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. 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 *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 data to an existing file, ``'w'`` to create a new file overwriting an existing
one. 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 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 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 ...@@ -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. to be handled. The default settings will work for most users.
See section :ref:`tar-unicode` for in-depth information. 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 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`. 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(...) .. classmethod:: TarFile.open(...)
......
...@@ -334,6 +334,12 @@ socket ...@@ -334,6 +334,12 @@ socket
:meth:`socket.socket.send`. :meth:`socket.socket.send`.
(Contributed by Giampaolo Rodola' in :issue:`17552`.) (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 time
---- ----
......
...@@ -1409,9 +1409,9 @@ class TarFile(object): ...@@ -1409,9 +1409,9 @@ class TarFile(object):
can be determined, `mode' is overridden by `fileobj's mode. can be determined, `mode' is overridden by `fileobj's mode.
`fileobj' is not closed, when TarFile is closed. `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: 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 = mode
self._mode = modes[mode] self._mode = modes[mode]
...@@ -1524,6 +1524,15 @@ class TarFile(object): ...@@ -1524,6 +1524,15 @@ class TarFile(object):
'w:bz2' open for writing with bzip2 compression 'w:bz2' open for writing with bzip2 compression
'w:xz' open for writing with lzma 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 a stream of tar blocks with transparent compression
'r|' open an uncompressed stream of tar blocks for reading 'r|' open an uncompressed stream of tar blocks for reading
'r|gz' open a gzip compressed stream of tar blocks 'r|gz' open a gzip compressed stream of tar blocks
...@@ -1582,7 +1591,7 @@ class TarFile(object): ...@@ -1582,7 +1591,7 @@ class TarFile(object):
t._extfileobj = False t._extfileobj = False
return t return t
elif mode in ("a", "w"): elif mode in ("a", "w", "x"):
return cls.taropen(name, mode, fileobj, **kwargs) return cls.taropen(name, mode, fileobj, **kwargs)
raise ValueError("undiscernible mode") raise ValueError("undiscernible mode")
...@@ -1591,8 +1600,8 @@ class TarFile(object): ...@@ -1591,8 +1600,8 @@ class TarFile(object):
def taropen(cls, name, mode="r", fileobj=None, **kwargs): def taropen(cls, name, mode="r", fileobj=None, **kwargs):
"""Open uncompressed tar archive name for reading or writing. """Open uncompressed tar archive name for reading or writing.
""" """
if mode not in ("r", "a", "w"): if mode not in ("r", "a", "w", "x"):
raise ValueError("mode must be 'r', 'a' or 'w'") raise ValueError("mode must be 'r', 'a', 'w' or 'x'")
return cls(name, mode, fileobj, **kwargs) return cls(name, mode, fileobj, **kwargs)
@classmethod @classmethod
...@@ -1600,8 +1609,8 @@ class TarFile(object): ...@@ -1600,8 +1609,8 @@ class TarFile(object):
"""Open gzip compressed tar archive name for reading or writing. """Open gzip compressed tar archive name for reading or writing.
Appending is not allowed. Appending is not allowed.
""" """
if mode not in ("r", "w"): if mode not in ("r", "w", "x"):
raise ValueError("mode must be 'r' or 'w'") raise ValueError("mode must be 'r', 'w' or 'x'")
try: try:
import gzip import gzip
...@@ -1634,8 +1643,8 @@ class TarFile(object): ...@@ -1634,8 +1643,8 @@ class TarFile(object):
"""Open bzip2 compressed tar archive name for reading or writing. """Open bzip2 compressed tar archive name for reading or writing.
Appending is not allowed. Appending is not allowed.
""" """
if mode not in ("r", "w"): if mode not in ("r", "w", "x"):
raise ValueError("mode must be 'r' or 'w'.") raise ValueError("mode must be 'r', 'w' or 'x'")
try: try:
import bz2 import bz2
...@@ -1663,8 +1672,8 @@ class TarFile(object): ...@@ -1663,8 +1672,8 @@ class TarFile(object):
"""Open lzma compressed tar archive name for reading or writing. """Open lzma compressed tar archive name for reading or writing.
Appending is not allowed. Appending is not allowed.
""" """
if mode not in ("r", "w"): if mode not in ("r", "w", "x"):
raise ValueError("mode must be 'r' or 'w'") raise ValueError("mode must be 'r', 'w' or 'x'")
try: try:
import lzma import lzma
...@@ -1751,7 +1760,7 @@ class TarFile(object): ...@@ -1751,7 +1760,7 @@ class TarFile(object):
addfile(). If given, `arcname' specifies an alternative name for the addfile(). If given, `arcname' specifies an alternative name for the
file in the archive. file in the archive.
""" """
self._check("aw") self._check("awx")
# When fileobj is given, replace name by # When fileobj is given, replace name by
# fileobj's real name. # fileobj's real name.
...@@ -1885,7 +1894,7 @@ class TarFile(object): ...@@ -1885,7 +1894,7 @@ class TarFile(object):
TarInfo object, if it returns None the TarInfo object will be TarInfo object, if it returns None the TarInfo object will be
excluded from the archive. excluded from the archive.
""" """
self._check("aw") self._check("awx")
if arcname is None: if arcname is None:
arcname = name arcname = name
...@@ -1942,7 +1951,7 @@ class TarFile(object): ...@@ -1942,7 +1951,7 @@ class TarFile(object):
On Windows platforms, `fileobj' should always be opened with mode On Windows platforms, `fileobj' should always be opened with mode
'rb' to avoid irritation about the file size. 'rb' to avoid irritation about the file size.
""" """
self._check("aw") self._check("awx")
tarinfo = copy.copy(tarinfo) tarinfo = copy.copy(tarinfo)
......
...@@ -1428,6 +1428,88 @@ class GNUWriteTest(unittest.TestCase): ...@@ -1428,6 +1428,88 @@ class GNUWriteTest(unittest.TestCase):
("longlnk/" * 127) + "longlink_") ("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") @unittest.skipUnless(hasattr(os, "link"), "Missing hardlink implementation")
class HardlinkTest(unittest.TestCase): class HardlinkTest(unittest.TestCase):
# Test the creation of LNKTYPE (hardlink) members in an archive. # Test the creation of LNKTYPE (hardlink) members in an archive.
......
...@@ -13,6 +13,8 @@ Core and Builtins ...@@ -13,6 +13,8 @@ Core and Builtins
Library Library
------- -------
- Issue #21717: tarfile.open() now supports 'x' (exclusive creation) mode.
- Issue #23344: marshal.dumps() is now 20-25% faster on average. - 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 - Issue #20416: marshal.dumps() with protocols 3 and 4 is now 40-50% faster on
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment