diff -r 367f5e98ffbb Doc/library/zipfile.rst --- a/Doc/library/zipfile.rst Fri Feb 06 10:21:37 2015 +0200 +++ b/Doc/library/zipfile.rst Fri Feb 06 18:36:56 2015 +0200 @@ -134,8 +134,11 @@ ZipFile Objects 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, ``'w'`` to truncate and write a new file, or ``'a'`` to append to an - existing file. If *mode* is ``'a'`` and *file* refers to an existing ZIP + file, ``'w'`` to truncate and write a new file, ``'x'`` to exclusive create + and write a new file, or ``'a'`` to append to an existing file. + If *mode* is ``'x'`` and *file* refers to an existing file, + :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 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 @@ -151,7 +154,7 @@ ZipFile Objects 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. - 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 ` without adding any files to the archive, the appropriate ZIP structures for an empty archive will be written to the file. @@ -171,6 +174,9 @@ ZipFile Objects .. versionchanged:: 3.4 ZIP64 extensions are enabled by default. + .. versionchanged:: 3.5 + Added support for the ``'x'`` mode. + .. method:: ZipFile.close() @@ -299,7 +305,8 @@ ZipFile Objects *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* 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 :exc:`RuntimeError`. Calling :meth:`write` on a closed ZipFile will raise a :exc:`RuntimeError`. @@ -327,10 +334,11 @@ ZipFile Objects 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 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 - opened with mode ``'w'`` or ``'a'`` -- calling :meth:`writestr` on a ZipFile - created with mode ``'r'`` will raise a :exc:`RuntimeError`. Calling - :meth:`writestr` on a closed ZipFile will raise a :exc:`RuntimeError`. + name, the date and time is set to the current date and time. + The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'`` -- calling + :meth:`writestr` on a ZipFile created with mode ``'r'`` will raise a + :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* parameter to the constructor for the new entry, or in the *zinfo_or_arcname* @@ -358,7 +366,8 @@ The following data attributes are also a .. attribute:: ZipFile.comment 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 truncated in the written archive when :meth:`close` is called. diff -r 367f5e98ffbb Lib/test/test_zipfile.py --- a/Lib/test/test_zipfile.py Fri Feb 06 10:21:37 2015 +0200 +++ b/Lib/test/test_zipfile.py Fri Feb 06 18:36:56 2015 +0200 @@ -1094,6 +1094,19 @@ class OtherTests(unittest.TestCase): self.assertEqual(zf.filelist[0].filename, "foo.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): if os.path.exists(TESTFN): os.unlink(TESTFN) diff -r 367f5e98ffbb Lib/zipfile.py --- a/Lib/zipfile.py Fri Feb 06 10:21:37 2015 +0200 +++ b/Lib/zipfile.py Fri Feb 06 18:36:56 2015 +0200 @@ -942,7 +942,8 @@ class ZipFile: 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. - 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), ZIP_BZIP2 (requires bz2) or ZIP_LZMA (requires lzma). allowZip64: if True ZipFile will create files with ZIP64 extensions when @@ -955,9 +956,10 @@ class ZipFile: _windows_illegal_name_trans_table = None def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True): - """Open the ZIP file with mode read "r", write "w" or append "a".""" - if mode not in ("r", "w", "a"): - raise RuntimeError('ZipFile() requires mode "r", "w", or "a"') + """Open the ZIP file with mode read 'r', write 'w', exclusive create 'x', + or append 'a'.""" + if mode not in ('r', 'w', 'x', 'a'): + raise RuntimeError("ZipFile() requires mode 'r', 'w', 'x', or 'a'") _check_compression(compression) @@ -976,8 +978,8 @@ class ZipFile: # No, it's a filename self._filePassed = 0 self.filename = file - modeDict = {'r' : 'rb', 'w': 'w+b', 'a' : 'r+b', - 'r+b': 'w+b', 'w+b': 'wb'} + modeDict = {'r' : 'rb', 'w': 'w+b', 'x': 'x+b', 'a' : 'r+b', + 'r+b': 'w+b', 'w+b': 'wb', 'x+b': 'xb'} filemode = modeDict[mode] while True: try: @@ -998,7 +1000,7 @@ class ZipFile: try: if mode == 'r': self._RealGetContents() - elif mode == 'w': + elif mode in ('w', 'x'): # set the modified flag so central directory gets written # even if no files are added to the archive self._didModify = True @@ -1018,7 +1020,7 @@ class ZipFile: self._didModify = True self.start_dir = self.fp.tell() else: - raise RuntimeError('Mode must be "r", "w" or "a"') + raise RuntimeError("Mode must be 'r', 'w', 'x', or 'a'") except: fp = self.fp self.fp = None @@ -1368,8 +1370,8 @@ class ZipFile: if zinfo.filename in self.NameToInfo: import warnings warnings.warn('Duplicate name: %r' % zinfo.filename, stacklevel=3) - if self.mode not in ("w", "a"): - raise RuntimeError('write() requires mode "w" or "a"') + if self.mode not in ('w', 'x', 'a'): + raise RuntimeError("write() requires mode 'w', 'x', or 'a'") if not self.fp: raise RuntimeError( "Attempt to write ZIP archive that was already closed") @@ -1549,13 +1551,13 @@ class ZipFile: self.close() 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.""" if self.fp is None: return 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: try: self.fp.seek(self.start_dir)