Commit 764fc9bf authored by Serhiy Storchaka's avatar Serhiy Storchaka

Issue #21717: The zipfile.ZipFile.open function now supports 'x' (exclusive

creation) mode.
parent 48919976
...@@ -134,8 +134,11 @@ ZipFile Objects ...@@ -134,8 +134,11 @@ ZipFile Objects
Open a ZIP file, where *file* can be either a path to a file (a string) or a Open a ZIP file, where *file* can be either a path to a file (a string) or a
file-like object. The *mode* parameter should be ``'r'`` to read an existing file-like object. The *mode* parameter should be ``'r'`` to read an existing
file, ``'w'`` to truncate and write a new file, or ``'a'`` to append to an file, ``'w'`` to truncate and write a new file, ``'x'`` to exclusive create
existing file. If *mode* is ``'a'`` and *file* refers to an existing ZIP and write a new file, or ``'a'`` to append to an existing file.
If *mode* is ``'x'`` and *file* refers to an existing file,
a :exc:`FileExistsError` will be raised.
If *mode* is ``'a'`` and *file* refers to an existing ZIP
file, then additional files are added to it. If *file* does not refer to a file, then additional files are added to it. If *file* does not refer to a
ZIP file, then a new ZIP archive is appended to the file. This is meant for ZIP file, then a new ZIP archive is appended to the file. This is meant for
adding a ZIP archive to another file (such as :file:`python.exe`). If adding a ZIP archive to another file (such as :file:`python.exe`). If
...@@ -152,7 +155,7 @@ ZipFile Objects ...@@ -152,7 +155,7 @@ ZipFile Objects
extensions when the zipfile is larger than 2 GiB. If it is false :mod:`zipfile` extensions when the zipfile is larger than 2 GiB. If it is false :mod:`zipfile`
will raise an exception when the ZIP file would require ZIP64 extensions. will raise an exception when the ZIP file would require ZIP64 extensions.
If the file is created with mode ``'a'`` or ``'w'`` and then If the file is created with mode ``'w'``, ``'x'`` or ``'a'`` and then
:meth:`closed <close>` without adding any files to the archive, the appropriate :meth:`closed <close>` without adding any files to the archive, the appropriate
ZIP structures for an empty archive will be written to the file. ZIP structures for an empty archive will be written to the file.
...@@ -174,6 +177,7 @@ ZipFile Objects ...@@ -174,6 +177,7 @@ ZipFile Objects
.. versionchanged:: 3.5 .. versionchanged:: 3.5
Added support for writing to unseekable streams. Added support for writing to unseekable streams.
Added support for the ``'x'`` mode.
.. method:: ZipFile.close() .. method:: ZipFile.close()
...@@ -310,7 +314,8 @@ ZipFile Objects ...@@ -310,7 +314,8 @@ ZipFile Objects
*arcname* (by default, this will be the same as *filename*, but without a drive *arcname* (by default, this will be the same as *filename*, but without a drive
letter and with leading path separators removed). If given, *compress_type* letter and with leading path separators removed). If given, *compress_type*
overrides the value given for the *compression* parameter to the constructor for overrides the value given for the *compression* parameter to the constructor for
the new entry. The archive must be open with mode ``'w'`` or ``'a'`` -- calling the new entry.
The archive must be open with mode ``'w'``, ``'x'`` or ``'a'`` -- calling
:meth:`write` on a ZipFile created with mode ``'r'`` will raise a :meth:`write` on a ZipFile created with mode ``'r'`` will raise a
:exc:`RuntimeError`. Calling :meth:`write` on a closed ZipFile will raise a :exc:`RuntimeError`. Calling :meth:`write` on a closed ZipFile will raise a
:exc:`RuntimeError`. :exc:`RuntimeError`.
...@@ -337,10 +342,11 @@ ZipFile Objects ...@@ -337,10 +342,11 @@ ZipFile Objects
Write the string *bytes* to the archive; *zinfo_or_arcname* is either the file Write the string *bytes* to the archive; *zinfo_or_arcname* is either the file
name it will be given in the archive, or a :class:`ZipInfo` instance. If it's name it will be given in the archive, or a :class:`ZipInfo` instance. If it's
an instance, at least the filename, date, and time must be given. If it's a an instance, at least the filename, date, and time must be given. If it's a
name, the date and time is set to the current date and time. The archive must be name, the date and time is set to the current date and time.
opened with mode ``'w'`` or ``'a'`` -- calling :meth:`writestr` on a ZipFile The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'`` -- calling
created with mode ``'r'`` will raise a :exc:`RuntimeError`. Calling :meth:`writestr` on a ZipFile created with mode ``'r'`` will raise a
:meth:`writestr` on a closed ZipFile will raise a :exc:`RuntimeError`. :exc:`RuntimeError`. Calling :meth:`writestr` on a closed ZipFile will
raise a :exc:`RuntimeError`.
If given, *compress_type* overrides the value given for the *compression* If given, *compress_type* overrides the value given for the *compression*
parameter to the constructor for the new entry, or in the *zinfo_or_arcname* parameter to the constructor for the new entry, or in the *zinfo_or_arcname*
...@@ -368,7 +374,8 @@ The following data attributes are also available: ...@@ -368,7 +374,8 @@ The following data attributes are also available:
.. attribute:: ZipFile.comment .. attribute:: ZipFile.comment
The comment text associated with the ZIP file. If assigning a comment to a The comment text associated with the ZIP file. If assigning a comment to a
:class:`ZipFile` instance created with mode 'a' or 'w', this should be a :class:`ZipFile` instance created with mode ``'w'``, ``'x'`` or ``'a'``,
this should be a
string no longer than 65535 bytes. Comments longer than this will be string no longer than 65535 bytes. Comments longer than this will be
truncated in the written archive when :meth:`close` is called. truncated in the written archive when :meth:`close` is called.
......
...@@ -454,6 +454,9 @@ zipfile ...@@ -454,6 +454,9 @@ zipfile
* Added support for writing ZIP files to unseekable streams. * Added support for writing ZIP files to unseekable streams.
(Contributed by Serhiy Storchaka in :issue:`23252`.) (Contributed by Serhiy Storchaka in :issue:`23252`.)
* The :func:`zipfile.ZipFile.open` function now supports ``'x'`` (exclusive
creation) mode. (Contributed by Serhiy Storchaka in :issue:`21717`.)
Optimizations Optimizations
============= =============
......
...@@ -1104,6 +1104,19 @@ class OtherTests(unittest.TestCase): ...@@ -1104,6 +1104,19 @@ class OtherTests(unittest.TestCase):
self.assertEqual(zf.filelist[0].filename, "foo.txt") self.assertEqual(zf.filelist[0].filename, "foo.txt")
self.assertEqual(zf.filelist[1].filename, "\xf6.txt") self.assertEqual(zf.filelist[1].filename, "\xf6.txt")
def test_exclusive_create_zip_file(self):
"""Test exclusive creating a new zipfile."""
unlink(TESTFN2)
filename = 'testfile.txt'
content = b'hello, world. this is some content.'
with zipfile.ZipFile(TESTFN2, "x", zipfile.ZIP_STORED) as zipfp:
zipfp.writestr(filename, content)
with self.assertRaises(FileExistsError):
zipfile.ZipFile(TESTFN2, "x", zipfile.ZIP_STORED)
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
self.assertEqual(zipfp.namelist(), [filename])
self.assertEqual(zipfp.read(filename), content)
def test_create_non_existent_file_for_append(self): def test_create_non_existent_file_for_append(self):
if os.path.exists(TESTFN): if os.path.exists(TESTFN):
os.unlink(TESTFN) os.unlink(TESTFN)
......
...@@ -962,7 +962,8 @@ class ZipFile: ...@@ -962,7 +962,8 @@ class ZipFile:
file: Either the path to the file, or a file-like object. file: Either the path to the file, or a file-like object.
If it is a path, the file will be opened and closed by ZipFile. If it is a path, the file will be opened and closed by ZipFile.
mode: The mode can be either read "r", write "w" or append "a". mode: The mode can be either read 'r', write 'w', exclusive create 'x',
or append 'a'.
compression: ZIP_STORED (no compression), ZIP_DEFLATED (requires zlib), compression: ZIP_STORED (no compression), ZIP_DEFLATED (requires zlib),
ZIP_BZIP2 (requires bz2) or ZIP_LZMA (requires lzma). ZIP_BZIP2 (requires bz2) or ZIP_LZMA (requires lzma).
allowZip64: if True ZipFile will create files with ZIP64 extensions when allowZip64: if True ZipFile will create files with ZIP64 extensions when
...@@ -975,9 +976,10 @@ class ZipFile: ...@@ -975,9 +976,10 @@ class ZipFile:
_windows_illegal_name_trans_table = None _windows_illegal_name_trans_table = None
def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True): def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True):
"""Open the ZIP file with mode read "r", write "w" or append "a".""" """Open the ZIP file with mode read 'r', write 'w', exclusive create 'x',
if mode not in ("r", "w", "a"): or append 'a'."""
raise RuntimeError('ZipFile() requires mode "r", "w", or "a"') if mode not in ('r', 'w', 'x', 'a'):
raise RuntimeError("ZipFile requires mode 'r', 'w', 'x', or 'a'")
_check_compression(compression) _check_compression(compression)
...@@ -996,8 +998,8 @@ class ZipFile: ...@@ -996,8 +998,8 @@ class ZipFile:
# No, it's a filename # No, it's a filename
self._filePassed = 0 self._filePassed = 0
self.filename = file self.filename = file
modeDict = {'r' : 'rb', 'w': 'w+b', 'a' : 'r+b', modeDict = {'r' : 'rb', 'w': 'w+b', 'x': 'x+b', 'a' : 'r+b',
'r+b': 'w+b', 'w+b': 'wb'} 'r+b': 'w+b', 'w+b': 'wb', 'x+b': 'xb'}
filemode = modeDict[mode] filemode = modeDict[mode]
while True: while True:
try: try:
...@@ -1019,7 +1021,7 @@ class ZipFile: ...@@ -1019,7 +1021,7 @@ class ZipFile:
try: try:
if mode == 'r': if mode == 'r':
self._RealGetContents() self._RealGetContents()
elif mode == 'w': elif mode in ('w', 'x'):
# set the modified flag so central directory gets written # set the modified flag so central directory gets written
# even if no files are added to the archive # even if no files are added to the archive
self._didModify = True self._didModify = True
...@@ -1050,7 +1052,7 @@ class ZipFile: ...@@ -1050,7 +1052,7 @@ class ZipFile:
self._didModify = True self._didModify = True
self.start_dir = self.fp.tell() self.start_dir = self.fp.tell()
else: else:
raise RuntimeError('Mode must be "r", "w" or "a"') raise RuntimeError("Mode must be 'r', 'w', 'x', or 'a'")
except: except:
fp = self.fp fp = self.fp
self.fp = None self.fp = None
...@@ -1400,8 +1402,8 @@ class ZipFile: ...@@ -1400,8 +1402,8 @@ class ZipFile:
if zinfo.filename in self.NameToInfo: if zinfo.filename in self.NameToInfo:
import warnings import warnings
warnings.warn('Duplicate name: %r' % zinfo.filename, stacklevel=3) warnings.warn('Duplicate name: %r' % zinfo.filename, stacklevel=3)
if self.mode not in ("w", "a"): if self.mode not in ('w', 'x', 'a'):
raise RuntimeError('write() requires mode "w" or "a"') raise RuntimeError("write() requires mode 'w', 'x', or 'a'")
if not self.fp: if not self.fp:
raise RuntimeError( raise RuntimeError(
"Attempt to write ZIP archive that was already closed") "Attempt to write ZIP archive that was already closed")
...@@ -1588,13 +1590,13 @@ class ZipFile: ...@@ -1588,13 +1590,13 @@ class ZipFile:
self.close() self.close()
def close(self): def close(self):
"""Close the file, and for mode "w" and "a" write the ending """Close the file, and for mode 'w', 'x' and 'a' write the ending
records.""" records."""
if self.fp is None: if self.fp is None:
return return
try: try:
if self.mode in ("w", "a") and self._didModify: # write ending records if self.mode in ('w', 'x', 'a') and self._didModify: # write ending records
with self._lock: with self._lock:
if self._seekable: if self._seekable:
self.fp.seek(self.start_dir) self.fp.seek(self.start_dir)
......
...@@ -30,6 +30,9 @@ Core and Builtins ...@@ -30,6 +30,9 @@ Core and Builtins
Library Library
------- -------
- Issue #21717: The zipfile.ZipFile.open function now supports 'x' (exclusive
creation) mode.
- Issue #21802: The reader in BufferedRWPair now is closed even when closing - Issue #21802: The reader in BufferedRWPair now is closed even when closing
writer failed in BufferedRWPair.close(). writer failed in BufferedRWPair.close().
......
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