]> granicus.if.org Git - python/commitdiff
Issue #21717: The zipfile.ZipFile.open function now supports 'x' (exclusive
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 25 Mar 2015 08:09:41 +0000 (10:09 +0200)
committerSerhiy Storchaka <storchaka@gmail.com>
Wed, 25 Mar 2015 08:09:41 +0000 (10:09 +0200)
creation) mode.

Doc/library/zipfile.rst
Doc/whatsnew/3.5.rst
Lib/test/test_zipfile.py
Lib/zipfile.py
Misc/NEWS

index c1dda25fbaf8f4a49f14abad5a22fcfe24495040..a15c461d9e8e1e698462cd1cd24c73a659540bc3 100644 (file)
@@ -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,
+   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 @@ 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 <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 @@ ZipFile Objects
 
    .. versionchanged:: 3.5
       Added support for writing to unseekable streams.
+      Added support for the ``'x'`` mode.
 
 
 .. method:: ZipFile.close()
@@ -310,7 +314,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`.
@@ -337,10 +342,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*
@@ -368,7 +374,8 @@ The following data attributes are also available:
 .. 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.
 
index aa5af04ff74b0783217d84c3cb60089be01f011b..0996350c92655e68181619479fd6b5134d4c9544 100644 (file)
@@ -454,6 +454,9 @@ zipfile
 * 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
 =============
index 4cd5fe3d83f655cb3bb4282ea06218b99bf2357e..1b2dc85d3bd6203e6456514015796d6c63cc378b 100644 (file)
@@ -1104,6 +1104,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)
index 55afa0850536b1afee9ee19c9654745386d63096..d545c5566fa6a68bdbacc2b91fedbcefb6e89606 100644 (file)
@@ -962,7 +962,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
@@ -975,9 +976,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)
 
@@ -996,8 +998,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:
@@ -1019,7 +1021,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
@@ -1050,7 +1052,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
@@ -1400,8 +1402,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")
@@ -1588,13 +1590,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:
                     if self._seekable:
                         self.fp.seek(self.start_dir)
index b85eeafe880064de080b3557853608e68b0ace1a..6534443f07b10081f8310e3aad9be18b39e91283 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -30,6 +30,9 @@ Core and Builtins
 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().