Skip to content

Commit 8606e95

Browse files
bpo-28231: The zipfile module now accepts path-like objects for external paths. (#511)
1 parent c351ce6 commit 8606e95

File tree

4 files changed

+143
-21
lines changed

4 files changed

+143
-21
lines changed

Doc/library/zipfile.rst

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ ZipFile Objects
132132

133133
.. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True)
134134

135-
Open a ZIP file, where *file* can be either a path to a file (a string) or a
136-
file-like object. The *mode* parameter should be ``'r'`` to read an existing
135+
Open a ZIP file, where *file* can be a path to a file (a string), a
136+
file-like object or a :term:`path-like object`.
137+
The *mode* parameter should be ``'r'`` to read an existing
137138
file, ``'w'`` to truncate and write a new file, ``'a'`` to append to an
138139
existing file, or ``'x'`` to exclusively create and write a new file.
139140
If *mode* is ``'x'`` and *file* refers to an existing file,
@@ -183,6 +184,9 @@ ZipFile Objects
183184
Previously, a plain :exc:`RuntimeError` was raised for unrecognized
184185
compression values.
185186

187+
.. versionchanged:: 3.6.2
188+
The *file* parameter accepts a :term:`path-like object`.
189+
186190

187191
.. method:: ZipFile.close()
188192

@@ -284,6 +288,9 @@ ZipFile Objects
284288
Calling :meth:`extract` on a closed ZipFile will raise a
285289
:exc:`ValueError`. Previously, a :exc:`RuntimeError` was raised.
286290

291+
.. versionchanged:: 3.6.2
292+
The *path* parameter accepts a :term:`path-like object`.
293+
287294

288295
.. method:: ZipFile.extractall(path=None, members=None, pwd=None)
289296

@@ -304,6 +311,9 @@ ZipFile Objects
304311
Calling :meth:`extractall` on a closed ZipFile will raise a
305312
:exc:`ValueError`. Previously, a :exc:`RuntimeError` was raised.
306313

314+
.. versionchanged:: 3.6.2
315+
The *path* parameter accepts a :term:`path-like object`.
316+
307317

308318
.. method:: ZipFile.printdir()
309319

@@ -403,6 +413,9 @@ ZipFile Objects
403413

404414
The following data attributes are also available:
405415

416+
.. attribute:: ZipFile.filename
417+
418+
Name of the ZIP file.
406419

407420
.. attribute:: ZipFile.debug
408421

@@ -488,6 +501,9 @@ The :class:`PyZipFile` constructor takes the same parameters as the
488501
.. versionadded:: 3.4
489502
The *filterfunc* parameter.
490503

504+
.. versionchanged:: 3.6.2
505+
The *pathname* parameter accepts a :term:`path-like object`.
506+
491507

492508
.. _zipinfo-objects:
493509

@@ -514,6 +530,10 @@ file:
514530

515531
.. versionadded:: 3.6
516532

533+
.. versionchanged:: 3.6.2
534+
The *filename* parameter accepts a :term:`path-like object`.
535+
536+
517537
Instances have the following methods and attributes:
518538

519539
.. method:: ZipInfo.is_dir()

Lib/test/test_zipfile.py

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import io
33
import os
44
import importlib.util
5+
import pathlib
56
import posixpath
67
import time
78
import struct
@@ -13,7 +14,7 @@
1314
from random import randint, random, getrandbits
1415

1516
from test.support import script_helper
16-
from test.support import (TESTFN, findfile, unlink, rmtree, temp_dir,
17+
from test.support import (TESTFN, findfile, unlink, rmtree, temp_dir, temp_cwd,
1718
requires_zlib, requires_bz2, requires_lzma,
1819
captured_stdout, check_warnings)
1920

@@ -148,6 +149,12 @@ def test_open(self):
148149
for f in get_files(self):
149150
self.zip_open_test(f, self.compression)
150151

152+
def test_open_with_pathlike(self):
153+
path = pathlib.Path(TESTFN2)
154+
self.zip_open_test(path, self.compression)
155+
with zipfile.ZipFile(path, "r", self.compression) as zipfp:
156+
self.assertIsInstance(zipfp.filename, str)
157+
151158
def zip_random_open_test(self, f, compression):
152159
self.make_test_archive(f, compression)
153160

@@ -906,49 +913,107 @@ def test_write_pyfile_bad_syntax(self):
906913
finally:
907914
rmtree(TESTFN2)
908915

916+
def test_write_pathlike(self):
917+
os.mkdir(TESTFN2)
918+
try:
919+
with open(os.path.join(TESTFN2, "mod1.py"), "w") as fp:
920+
fp.write("print(42)\n")
921+
922+
with TemporaryFile() as t, zipfile.PyZipFile(t, "w") as zipfp:
923+
zipfp.writepy(pathlib.Path(TESTFN2) / "mod1.py")
924+
names = zipfp.namelist()
925+
self.assertCompiledIn('mod1.py', names)
926+
finally:
927+
rmtree(TESTFN2)
928+
909929

910930
class ExtractTests(unittest.TestCase):
911-
def test_extract(self):
931+
932+
def make_test_file(self):
912933
with zipfile.ZipFile(TESTFN2, "w", zipfile.ZIP_STORED) as zipfp:
913934
for fpath, fdata in SMALL_TEST_DATA:
914935
zipfp.writestr(fpath, fdata)
915936

937+
def test_extract(self):
938+
with temp_cwd():
939+
self.make_test_file()
940+
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
941+
for fpath, fdata in SMALL_TEST_DATA:
942+
writtenfile = zipfp.extract(fpath)
943+
944+
# make sure it was written to the right place
945+
correctfile = os.path.join(os.getcwd(), fpath)
946+
correctfile = os.path.normpath(correctfile)
947+
948+
self.assertEqual(writtenfile, correctfile)
949+
950+
# make sure correct data is in correct file
951+
with open(writtenfile, "rb") as f:
952+
self.assertEqual(fdata.encode(), f.read())
953+
954+
unlink(writtenfile)
955+
956+
def _test_extract_with_target(self, target):
957+
self.make_test_file()
916958
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
917959
for fpath, fdata in SMALL_TEST_DATA:
918-
writtenfile = zipfp.extract(fpath)
960+
writtenfile = zipfp.extract(fpath, target)
919961

920962
# make sure it was written to the right place
921-
correctfile = os.path.join(os.getcwd(), fpath)
963+
correctfile = os.path.join(target, fpath)
922964
correctfile = os.path.normpath(correctfile)
923-
924-
self.assertEqual(writtenfile, correctfile)
965+
self.assertTrue(os.path.samefile(writtenfile, correctfile), (writtenfile, target))
925966

926967
# make sure correct data is in correct file
927968
with open(writtenfile, "rb") as f:
928969
self.assertEqual(fdata.encode(), f.read())
929970

930971
unlink(writtenfile)
931972

932-
# remove the test file subdirectories
933-
rmtree(os.path.join(os.getcwd(), 'ziptest2dir'))
973+
unlink(TESTFN2)
974+
975+
def test_extract_with_target(self):
976+
with temp_dir() as extdir:
977+
self._test_extract_with_target(extdir)
978+
979+
def test_extract_with_target_pathlike(self):
980+
with temp_dir() as extdir:
981+
self._test_extract_with_target(pathlib.Path(extdir))
934982

935983
def test_extract_all(self):
936-
with zipfile.ZipFile(TESTFN2, "w", zipfile.ZIP_STORED) as zipfp:
937-
for fpath, fdata in SMALL_TEST_DATA:
938-
zipfp.writestr(fpath, fdata)
984+
with temp_cwd():
985+
self.make_test_file()
986+
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
987+
zipfp.extractall()
988+
for fpath, fdata in SMALL_TEST_DATA:
989+
outfile = os.path.join(os.getcwd(), fpath)
990+
991+
with open(outfile, "rb") as f:
992+
self.assertEqual(fdata.encode(), f.read())
939993

994+
unlink(outfile)
995+
996+
def _test_extract_all_with_target(self, target):
997+
self.make_test_file()
940998
with zipfile.ZipFile(TESTFN2, "r") as zipfp:
941-
zipfp.extractall()
999+
zipfp.extractall(target)
9421000
for fpath, fdata in SMALL_TEST_DATA:
943-
outfile = os.path.join(os.getcwd(), fpath)
1001+
outfile = os.path.join(target, fpath)
9441002

9451003
with open(outfile, "rb") as f:
9461004
self.assertEqual(fdata.encode(), f.read())
9471005

9481006
unlink(outfile)
9491007

950-
# remove the test file subdirectories
951-
rmtree(os.path.join(os.getcwd(), 'ziptest2dir'))
1008+
unlink(TESTFN2)
1009+
1010+
def test_extract_all_with_target(self):
1011+
with temp_dir() as extdir:
1012+
self._test_extract_all_with_target(extdir)
1013+
1014+
def test_extract_all_with_target_pathlike(self):
1015+
with temp_dir() as extdir:
1016+
self._test_extract_all_with_target(pathlib.Path(extdir))
9521017

9531018
def check_file(self, filename, content):
9541019
self.assertTrue(os.path.isfile(filename))
@@ -1188,6 +1253,8 @@ def test_is_zip_erroneous_file(self):
11881253
with open(TESTFN, "w") as fp:
11891254
fp.write("this is not a legal zip file\n")
11901255
self.assertFalse(zipfile.is_zipfile(TESTFN))
1256+
# - passing a path-like object
1257+
self.assertFalse(zipfile.is_zipfile(pathlib.Path(TESTFN)))
11911258
# - passing a file object
11921259
with open(TESTFN, "rb") as fp:
11931260
self.assertFalse(zipfile.is_zipfile(fp))
@@ -2033,6 +2100,26 @@ def test_from_file(self):
20332100
zi = zipfile.ZipInfo.from_file(__file__)
20342101
self.assertEqual(posixpath.basename(zi.filename), 'test_zipfile.py')
20352102
self.assertFalse(zi.is_dir())
2103+
self.assertEqual(zi.file_size, os.path.getsize(__file__))
2104+
2105+
def test_from_file_pathlike(self):
2106+
zi = zipfile.ZipInfo.from_file(pathlib.Path(__file__))
2107+
self.assertEqual(posixpath.basename(zi.filename), 'test_zipfile.py')
2108+
self.assertFalse(zi.is_dir())
2109+
self.assertEqual(zi.file_size, os.path.getsize(__file__))
2110+
2111+
def test_from_file_bytes(self):
2112+
zi = zipfile.ZipInfo.from_file(os.fsencode(__file__), 'test')
2113+
self.assertEqual(posixpath.basename(zi.filename), 'test')
2114+
self.assertFalse(zi.is_dir())
2115+
self.assertEqual(zi.file_size, os.path.getsize(__file__))
2116+
2117+
def test_from_file_fileno(self):
2118+
with open(__file__, 'rb') as f:
2119+
zi = zipfile.ZipInfo.from_file(f.fileno(), 'test')
2120+
self.assertEqual(posixpath.basename(zi.filename), 'test')
2121+
self.assertFalse(zi.is_dir())
2122+
self.assertEqual(zi.file_size, os.path.getsize(__file__))
20362123

20372124
def test_from_dir(self):
20382125
dirpath = os.path.dirname(os.path.abspath(__file__))

Lib/zipfile.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ def from_file(cls, filename, arcname=None):
478478
this will be the same as filename, but without a drive letter and with
479479
leading path separators removed).
480480
"""
481+
if isinstance(filename, os.PathLike):
482+
filename = os.fspath(filename)
481483
st = os.stat(filename)
482484
isdir = stat.S_ISDIR(st.st_mode)
483485
mtime = time.localtime(st.st_mtime)
@@ -1069,6 +1071,8 @@ def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True):
10691071
self._comment = b''
10701072

10711073
# Check if we were passed a file-like object
1074+
if isinstance(file, os.PathLike):
1075+
file = os.fspath(file)
10721076
if isinstance(file, str):
10731077
# No, it's a filename
10741078
self._filePassed = 0
@@ -1469,11 +1473,10 @@ def extract(self, member, path=None, pwd=None):
14691473
as possible. `member' may be a filename or a ZipInfo object. You can
14701474
specify a different directory using `path'.
14711475
"""
1472-
if not isinstance(member, ZipInfo):
1473-
member = self.getinfo(member)
1474-
14751476
if path is None:
14761477
path = os.getcwd()
1478+
else:
1479+
path = os.fspath(path)
14771480

14781481
return self._extract_member(member, path, pwd)
14791482

@@ -1486,8 +1489,13 @@ def extractall(self, path=None, members=None, pwd=None):
14861489
if members is None:
14871490
members = self.namelist()
14881491

1492+
if path is None:
1493+
path = os.getcwd()
1494+
else:
1495+
path = os.fspath(path)
1496+
14891497
for zipinfo in members:
1490-
self.extract(zipinfo, path, pwd)
1498+
self._extract_member(zipinfo, path, pwd)
14911499

14921500
@classmethod
14931501
def _sanitize_windows_name(cls, arcname, pathsep):
@@ -1508,6 +1516,9 @@ def _extract_member(self, member, targetpath, pwd):
15081516
"""Extract the ZipInfo object 'member' to a physical
15091517
file on the path targetpath.
15101518
"""
1519+
if not isinstance(member, ZipInfo):
1520+
member = self.getinfo(member)
1521+
15111522
# build the destination pathname, replacing
15121523
# forward slashes to platform specific separators.
15131524
arcname = member.filename.replace('/', os.path.sep)
@@ -1800,6 +1811,7 @@ def writepy(self, pathname, basename="", filterfunc=None):
18001811
If filterfunc(pathname) is given, it is called with every argument.
18011812
When it is False, the file or directory is skipped.
18021813
"""
1814+
pathname = os.fspath(pathname)
18031815
if filterfunc and not filterfunc(pathname):
18041816
if self.debug:
18051817
label = 'path' if os.path.isdir(pathname) else 'file'

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ Extension Modules
270270
Library
271271
-------
272272

273+
- bpo-28231: The zipfile module now accepts path-like objects for external
274+
paths.
275+
273276
- bpo-26915: index() and count() methods of collections.abc.Sequence now
274277
check identity before checking equality when do comparisons.
275278

0 commit comments

Comments
 (0)