]> granicus.if.org Git - python/commitdiff
bpo-28231: The zipfile module now accepts path-like objects for external paths. ...
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 8 Mar 2017 12:37:51 +0000 (14:37 +0200)
committerGitHub <noreply@github.com>
Wed, 8 Mar 2017 12:37:51 +0000 (14:37 +0200)
Doc/library/zipfile.rst
Lib/test/test_zipfile.py
Lib/zipfile.py
Misc/NEWS

index a0de10cae3dbeb54ca9d5247673f1b59338746c9..4cde1dd76a297cf661a428c8f9b59dacb68b94eb 100644 (file)
@@ -132,8 +132,9 @@ ZipFile Objects
 
 .. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True)
 
-   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
+   Open a ZIP file, where *file* can be a path to a file (a string), a
+   file-like object or a :term:`path-like object`.
+   The *mode* parameter should be ``'r'`` to read an existing
    file, ``'w'`` to truncate and write a new file, ``'a'`` to append to an
    existing file, or ``'x'`` to exclusively create and write a new file.
    If *mode* is ``'x'`` and *file* refers to an existing file,
@@ -183,6 +184,9 @@ ZipFile Objects
       Previously, a plain :exc:`RuntimeError` was raised for unrecognized
       compression values.
 
+   .. versionchanged:: 3.6.2
+      The *file* parameter accepts a :term:`path-like object`.
+
 
 .. method:: ZipFile.close()
 
@@ -284,6 +288,9 @@ ZipFile Objects
       Calling :meth:`extract` on a closed ZipFile will raise a
       :exc:`ValueError`.  Previously, a :exc:`RuntimeError` was raised.
 
+   .. versionchanged:: 3.6.2
+      The *path* parameter accepts a :term:`path-like object`.
+
 
 .. method:: ZipFile.extractall(path=None, members=None, pwd=None)
 
@@ -304,6 +311,9 @@ ZipFile Objects
       Calling :meth:`extractall` on a closed ZipFile will raise a
       :exc:`ValueError`.  Previously, a :exc:`RuntimeError` was raised.
 
+   .. versionchanged:: 3.6.2
+      The *path* parameter accepts a :term:`path-like object`.
+
 
 .. method:: ZipFile.printdir()
 
@@ -403,6 +413,9 @@ ZipFile Objects
 
 The following data attributes are also available:
 
+.. attribute:: ZipFile.filename
+
+   Name of the ZIP file.
 
 .. attribute:: ZipFile.debug
 
@@ -488,6 +501,9 @@ The :class:`PyZipFile` constructor takes the same parameters as the
       .. versionadded:: 3.4
          The *filterfunc* parameter.
 
+      .. versionchanged:: 3.6.2
+         The *pathname* parameter accepts a :term:`path-like object`.
+
 
 .. _zipinfo-objects:
 
@@ -514,6 +530,10 @@ file:
 
    .. versionadded:: 3.6
 
+   .. versionchanged:: 3.6.2
+      The *filename* parameter accepts a :term:`path-like object`.
+
+
 Instances have the following methods and attributes:
 
 .. method:: ZipInfo.is_dir()
index 0a19d76f429b3997e9931895fadf4d490d39e26c..f3d993608f62e28768f3eae312df0de60ca1d8cb 100644 (file)
@@ -2,6 +2,7 @@ import contextlib
 import io
 import os
 import importlib.util
+import pathlib
 import posixpath
 import time
 import struct
@@ -13,7 +14,7 @@ from tempfile import TemporaryFile
 from random import randint, random, getrandbits
 
 from test.support import script_helper
-from test.support import (TESTFN, findfile, unlink, rmtree, temp_dir,
+from test.support import (TESTFN, findfile, unlink, rmtree, temp_dir, temp_cwd,
                           requires_zlib, requires_bz2, requires_lzma,
                           captured_stdout, check_warnings)
 
@@ -148,6 +149,12 @@ class AbstractTestsWithSourceFile:
         for f in get_files(self):
             self.zip_open_test(f, self.compression)
 
+    def test_open_with_pathlike(self):
+        path = pathlib.Path(TESTFN2)
+        self.zip_open_test(path, self.compression)
+        with zipfile.ZipFile(path, "r", self.compression) as zipfp:
+            self.assertIsInstance(zipfp.filename, str)
+
     def zip_random_open_test(self, f, compression):
         self.make_test_archive(f, compression)
 
@@ -906,22 +913,56 @@ class PyZipFileTests(unittest.TestCase):
         finally:
             rmtree(TESTFN2)
 
+    def test_write_pathlike(self):
+        os.mkdir(TESTFN2)
+        try:
+            with open(os.path.join(TESTFN2, "mod1.py"), "w") as fp:
+                fp.write("print(42)\n")
+
+            with TemporaryFile() as t, zipfile.PyZipFile(t, "w") as zipfp:
+                zipfp.writepy(pathlib.Path(TESTFN2) / "mod1.py")
+                names = zipfp.namelist()
+                self.assertCompiledIn('mod1.py', names)
+        finally:
+            rmtree(TESTFN2)
+
 
 class ExtractTests(unittest.TestCase):
-    def test_extract(self):
+
+    def make_test_file(self):
         with zipfile.ZipFile(TESTFN2, "w", zipfile.ZIP_STORED) as zipfp:
             for fpath, fdata in SMALL_TEST_DATA:
                 zipfp.writestr(fpath, fdata)
 
+    def test_extract(self):
+        with temp_cwd():
+            self.make_test_file()
+            with zipfile.ZipFile(TESTFN2, "r") as zipfp:
+                for fpath, fdata in SMALL_TEST_DATA:
+                    writtenfile = zipfp.extract(fpath)
+
+                    # make sure it was written to the right place
+                    correctfile = os.path.join(os.getcwd(), fpath)
+                    correctfile = os.path.normpath(correctfile)
+
+                    self.assertEqual(writtenfile, correctfile)
+
+                    # make sure correct data is in correct file
+                    with open(writtenfile, "rb") as f:
+                        self.assertEqual(fdata.encode(), f.read())
+
+                    unlink(writtenfile)
+
+    def _test_extract_with_target(self, target):
+        self.make_test_file()
         with zipfile.ZipFile(TESTFN2, "r") as zipfp:
             for fpath, fdata in SMALL_TEST_DATA:
-                writtenfile = zipfp.extract(fpath)
+                writtenfile = zipfp.extract(fpath, target)
 
                 # make sure it was written to the right place
-                correctfile = os.path.join(os.getcwd(), fpath)
+                correctfile = os.path.join(target, fpath)
                 correctfile = os.path.normpath(correctfile)
-
-                self.assertEqual(writtenfile, correctfile)
+                self.assertTrue(os.path.samefile(writtenfile, correctfile), (writtenfile, target))
 
                 # make sure correct data is in correct file
                 with open(writtenfile, "rb") as f:
@@ -929,26 +970,50 @@ class ExtractTests(unittest.TestCase):
 
                 unlink(writtenfile)
 
-        # remove the test file subdirectories
-        rmtree(os.path.join(os.getcwd(), 'ziptest2dir'))
+        unlink(TESTFN2)
+
+    def test_extract_with_target(self):
+        with temp_dir() as extdir:
+            self._test_extract_with_target(extdir)
+
+    def test_extract_with_target_pathlike(self):
+        with temp_dir() as extdir:
+            self._test_extract_with_target(pathlib.Path(extdir))
 
     def test_extract_all(self):
-        with zipfile.ZipFile(TESTFN2, "w", zipfile.ZIP_STORED) as zipfp:
-            for fpath, fdata in SMALL_TEST_DATA:
-                zipfp.writestr(fpath, fdata)
+        with temp_cwd():
+            self.make_test_file()
+            with zipfile.ZipFile(TESTFN2, "r") as zipfp:
+                zipfp.extractall()
+                for fpath, fdata in SMALL_TEST_DATA:
+                    outfile = os.path.join(os.getcwd(), fpath)
+
+                    with open(outfile, "rb") as f:
+                        self.assertEqual(fdata.encode(), f.read())
 
+                    unlink(outfile)
+
+    def _test_extract_all_with_target(self, target):
+        self.make_test_file()
         with zipfile.ZipFile(TESTFN2, "r") as zipfp:
-            zipfp.extractall()
+            zipfp.extractall(target)
             for fpath, fdata in SMALL_TEST_DATA:
-                outfile = os.path.join(os.getcwd(), fpath)
+                outfile = os.path.join(target, fpath)
 
                 with open(outfile, "rb") as f:
                     self.assertEqual(fdata.encode(), f.read())
 
                 unlink(outfile)
 
-        # remove the test file subdirectories
-        rmtree(os.path.join(os.getcwd(), 'ziptest2dir'))
+        unlink(TESTFN2)
+
+    def test_extract_all_with_target(self):
+        with temp_dir() as extdir:
+            self._test_extract_all_with_target(extdir)
+
+    def test_extract_all_with_target_pathlike(self):
+        with temp_dir() as extdir:
+            self._test_extract_all_with_target(pathlib.Path(extdir))
 
     def check_file(self, filename, content):
         self.assertTrue(os.path.isfile(filename))
@@ -1188,6 +1253,8 @@ class OtherTests(unittest.TestCase):
         with open(TESTFN, "w") as fp:
             fp.write("this is not a legal zip file\n")
         self.assertFalse(zipfile.is_zipfile(TESTFN))
+        # - passing a path-like object
+        self.assertFalse(zipfile.is_zipfile(pathlib.Path(TESTFN)))
         # - passing a file object
         with open(TESTFN, "rb") as fp:
             self.assertFalse(zipfile.is_zipfile(fp))
@@ -2033,6 +2100,26 @@ class ZipInfoTests(unittest.TestCase):
         zi = zipfile.ZipInfo.from_file(__file__)
         self.assertEqual(posixpath.basename(zi.filename), 'test_zipfile.py')
         self.assertFalse(zi.is_dir())
+        self.assertEqual(zi.file_size, os.path.getsize(__file__))
+
+    def test_from_file_pathlike(self):
+        zi = zipfile.ZipInfo.from_file(pathlib.Path(__file__))
+        self.assertEqual(posixpath.basename(zi.filename), 'test_zipfile.py')
+        self.assertFalse(zi.is_dir())
+        self.assertEqual(zi.file_size, os.path.getsize(__file__))
+
+    def test_from_file_bytes(self):
+        zi = zipfile.ZipInfo.from_file(os.fsencode(__file__), 'test')
+        self.assertEqual(posixpath.basename(zi.filename), 'test')
+        self.assertFalse(zi.is_dir())
+        self.assertEqual(zi.file_size, os.path.getsize(__file__))
+
+    def test_from_file_fileno(self):
+        with open(__file__, 'rb') as f:
+            zi = zipfile.ZipInfo.from_file(f.fileno(), 'test')
+            self.assertEqual(posixpath.basename(zi.filename), 'test')
+            self.assertFalse(zi.is_dir())
+            self.assertEqual(zi.file_size, os.path.getsize(__file__))
 
     def test_from_dir(self):
         dirpath = os.path.dirname(os.path.abspath(__file__))
index 93171358e41167b15686fbed5cb734aea5cbc5e6..b5c16dbc1295aece91f978bbb31f9e6a16aaed9d 100644 (file)
@@ -478,6 +478,8 @@ class ZipInfo (object):
         this will be the same as filename, but without a drive letter and with
         leading path separators removed).
         """
+        if isinstance(filename, os.PathLike):
+            filename = os.fspath(filename)
         st = os.stat(filename)
         isdir = stat.S_ISDIR(st.st_mode)
         mtime = time.localtime(st.st_mtime)
@@ -1069,6 +1071,8 @@ class ZipFile:
         self._comment = b''
 
         # Check if we were passed a file-like object
+        if isinstance(file, os.PathLike):
+            file = os.fspath(file)
         if isinstance(file, str):
             # No, it's a filename
             self._filePassed = 0
@@ -1469,11 +1473,10 @@ class ZipFile:
            as possible. `member' may be a filename or a ZipInfo object. You can
            specify a different directory using `path'.
         """
-        if not isinstance(member, ZipInfo):
-            member = self.getinfo(member)
-
         if path is None:
             path = os.getcwd()
+        else:
+            path = os.fspath(path)
 
         return self._extract_member(member, path, pwd)
 
@@ -1486,8 +1489,13 @@ class ZipFile:
         if members is None:
             members = self.namelist()
 
+        if path is None:
+            path = os.getcwd()
+        else:
+            path = os.fspath(path)
+
         for zipinfo in members:
-            self.extract(zipinfo, path, pwd)
+            self._extract_member(zipinfo, path, pwd)
 
     @classmethod
     def _sanitize_windows_name(cls, arcname, pathsep):
@@ -1508,6 +1516,9 @@ class ZipFile:
         """Extract the ZipInfo object 'member' to a physical
            file on the path targetpath.
         """
+        if not isinstance(member, ZipInfo):
+            member = self.getinfo(member)
+
         # build the destination pathname, replacing
         # forward slashes to platform specific separators.
         arcname = member.filename.replace('/', os.path.sep)
@@ -1800,6 +1811,7 @@ class PyZipFile(ZipFile):
         If filterfunc(pathname) is given, it is called with every argument.
         When it is False, the file or directory is skipped.
         """
+        pathname = os.fspath(pathname)
         if filterfunc and not filterfunc(pathname):
             if self.debug:
                 label = 'path' if os.path.isdir(pathname) else 'file'
index a8c8ec7d58cb114187c0e1c71ece97cf8732edb0..503ed83e46a035601bebc7862ff40beaf0c6c059 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -270,6 +270,9 @@ Extension Modules
 Library
 -------
 
+- bpo-28231: The zipfile module now accepts path-like objects for external
+  paths.
+
 - bpo-26915: index() and count() methods of collections.abc.Sequence now
   check identity before checking equality when do comparisons.