[Python-checkins] cpython: Issue #21717: The zipfile.ZipFile.open function now supports 'x' (exclusive

serhiy.storchaka python-checkins at python.org
Wed Mar 25 09:10:24 CET 2015


https://hg.python.org/cpython/rev/b2a8c30d8ddb
changeset:   95200:b2a8c30d8ddb
user:        Serhiy Storchaka <storchaka at gmail.com>
date:        Wed Mar 25 10:09:41 2015 +0200
summary:
  Issue #21717: The zipfile.ZipFile.open function now supports 'x' (exclusive
creation) mode.

files:
  Doc/library/zipfile.rst  |  25 ++++++++++++++++---------
  Doc/whatsnew/3.5.rst     |   3 +++
  Lib/test/test_zipfile.py |  13 +++++++++++++
  Lib/zipfile.py           |  26 ++++++++++++++------------
  Misc/NEWS                |   3 +++
  5 files changed, 49 insertions(+), 21 deletions(-)


diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst
--- a/Doc/library/zipfile.rst
+++ b/Doc/library/zipfile.rst
@@ -134,8 +134,11 @@
 
    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,
+   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
    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
@@ -152,7 +155,7 @@
    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 <close>` without adding any files to the archive, the appropriate
    ZIP structures for an empty archive will be written to the file.
 
@@ -174,6 +177,7 @@
 
    .. versionchanged:: 3.5
       Added support for writing to unseekable streams.
+      Added support for the ``'x'`` mode.
 
 
 .. method:: ZipFile.close()
@@ -310,7 +314,8 @@
    *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`.
@@ -337,10 +342,11 @@
    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*
@@ -368,7 +374,8 @@
 .. 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 --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst
--- a/Doc/whatsnew/3.5.rst
+++ b/Doc/whatsnew/3.5.rst
@@ -454,6 +454,9 @@
 * Added support for writing ZIP files to unseekable streams.
   (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
 =============
diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -1104,6 +1104,19 @@
             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 --git a/Lib/zipfile.py b/Lib/zipfile.py
--- a/Lib/zipfile.py
+++ b/Lib/zipfile.py
@@ -962,7 +962,8 @@
 
     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
@@ -975,9 +976,10 @@
     _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)
 
@@ -996,8 +998,8 @@
             # 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:
@@ -1019,7 +1021,7 @@
         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
@@ -1050,7 +1052,7 @@
                     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
@@ -1400,8 +1402,8 @@
         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")
@@ -1588,13 +1590,13 @@
         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:
                     if self._seekable:
                         self.fp.seek(self.start_dir)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -30,6 +30,9 @@
 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
   writer failed in BufferedRWPair.close().
 

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list