bpo-28231: The zipfile module now accepts path-like objects for external paths. (#511)
This commit is contained in:
parent
c351ce6a2c
commit
8606e9524a
|
@ -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()
|
||||
|
|
|
@ -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__))
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue