Skip to content

Commit d1a0a96

Browse files
authored
bpo-42043: Add support for zipfile.Path subclasses (#22716)
* bpo-42043: Add support for zipfile.Path inheritance as introduced in zipp 3.2.0. * Add blurb.
1 parent 14cdc21 commit d1a0a96

File tree

3 files changed

+240
-115
lines changed

3 files changed

+240
-115
lines changed

Lib/test/test_zipfile.py

Lines changed: 206 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import unittest
1414
import unittest.mock as mock
1515
import zipfile
16+
import functools
1617

1718

1819
from tempfile import TemporaryFile
@@ -2836,6 +2837,20 @@ def build_alpharep_fixture():
28362837
return zf
28372838

28382839

2840+
def pass_alpharep(meth):
2841+
"""
2842+
Given a method, wrap it in a for loop that invokes method
2843+
with each subtest.
2844+
"""
2845+
2846+
@functools.wraps(meth)
2847+
def wrapper(self):
2848+
for alpharep in self.zipfile_alpharep():
2849+
meth(self, alpharep=alpharep)
2850+
2851+
return wrapper
2852+
2853+
28392854
class TestPath(unittest.TestCase):
28402855
def setUp(self):
28412856
self.fixtures = contextlib.ExitStack()
@@ -2847,47 +2862,58 @@ def zipfile_alpharep(self):
28472862
with self.subTest():
28482863
yield add_dirs(build_alpharep_fixture())
28492864

2850-
def zipfile_ondisk(self):
2865+
def zipfile_ondisk(self, alpharep):
28512866
tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
2852-
for alpharep in self.zipfile_alpharep():
2853-
buffer = alpharep.fp
2854-
alpharep.close()
2855-
path = tmpdir / alpharep.filename
2856-
with path.open("wb") as strm:
2857-
strm.write(buffer.getvalue())
2858-
yield path
2859-
2860-
def test_iterdir_and_types(self):
2861-
for alpharep in self.zipfile_alpharep():
2862-
root = zipfile.Path(alpharep)
2863-
assert root.is_dir()
2864-
a, b, g = root.iterdir()
2865-
assert a.is_file()
2866-
assert b.is_dir()
2867-
assert g.is_dir()
2868-
c, f, d = b.iterdir()
2869-
assert c.is_file() and f.is_file()
2870-
e, = d.iterdir()
2871-
assert e.is_file()
2872-
h, = g.iterdir()
2873-
i, = h.iterdir()
2874-
assert i.is_file()
2875-
2876-
def test_subdir_is_dir(self):
2877-
for alpharep in self.zipfile_alpharep():
2878-
root = zipfile.Path(alpharep)
2879-
assert (root / 'b').is_dir()
2880-
assert (root / 'b/').is_dir()
2881-
assert (root / 'g').is_dir()
2882-
assert (root / 'g/').is_dir()
2883-
2884-
def test_open(self):
2885-
for alpharep in self.zipfile_alpharep():
2886-
root = zipfile.Path(alpharep)
2887-
a, b, g = root.iterdir()
2888-
with a.open() as strm:
2889-
data = strm.read()
2890-
assert data == "content of a"
2867+
buffer = alpharep.fp
2868+
alpharep.close()
2869+
path = tmpdir / alpharep.filename
2870+
with path.open("wb") as strm:
2871+
strm.write(buffer.getvalue())
2872+
return path
2873+
2874+
@pass_alpharep
2875+
def test_iterdir_and_types(self, alpharep):
2876+
root = zipfile.Path(alpharep)
2877+
assert root.is_dir()
2878+
a, b, g = root.iterdir()
2879+
assert a.is_file()
2880+
assert b.is_dir()
2881+
assert g.is_dir()
2882+
c, f, d = b.iterdir()
2883+
assert c.is_file() and f.is_file()
2884+
(e,) = d.iterdir()
2885+
assert e.is_file()
2886+
(h,) = g.iterdir()
2887+
(i,) = h.iterdir()
2888+
assert i.is_file()
2889+
2890+
@pass_alpharep
2891+
def test_is_file_missing(self, alpharep):
2892+
root = zipfile.Path(alpharep)
2893+
assert not root.joinpath('missing.txt').is_file()
2894+
2895+
@pass_alpharep
2896+
def test_iterdir_on_file(self, alpharep):
2897+
root = zipfile.Path(alpharep)
2898+
a, b, g = root.iterdir()
2899+
with self.assertRaises(ValueError):
2900+
a.iterdir()
2901+
2902+
@pass_alpharep
2903+
def test_subdir_is_dir(self, alpharep):
2904+
root = zipfile.Path(alpharep)
2905+
assert (root / 'b').is_dir()
2906+
assert (root / 'b/').is_dir()
2907+
assert (root / 'g').is_dir()
2908+
assert (root / 'g/').is_dir()
2909+
2910+
@pass_alpharep
2911+
def test_open(self, alpharep):
2912+
root = zipfile.Path(alpharep)
2913+
a, b, g = root.iterdir()
2914+
with a.open() as strm:
2915+
data = strm.read()
2916+
assert data == "content of a"
28912917

28922918
def test_open_write(self):
28932919
"""
@@ -2908,6 +2934,14 @@ def test_open_extant_directory(self):
29082934
with self.assertRaises(IsADirectoryError):
29092935
zf.joinpath('b').open()
29102936

2937+
@pass_alpharep
2938+
def test_open_binary_invalid_args(self, alpharep):
2939+
root = zipfile.Path(alpharep)
2940+
with self.assertRaises(ValueError):
2941+
root.joinpath('a.txt').open('rb', encoding='utf-8')
2942+
with self.assertRaises(ValueError):
2943+
root.joinpath('a.txt').open('rb', 'utf-8')
2944+
29112945
def test_open_missing_directory(self):
29122946
"""
29132947
Attempting to open a missing directory raises FileNotFoundError.
@@ -2916,75 +2950,87 @@ def test_open_missing_directory(self):
29162950
with self.assertRaises(FileNotFoundError):
29172951
zf.joinpath('z').open()
29182952

2919-
def test_read(self):
2920-
for alpharep in self.zipfile_alpharep():
2921-
root = zipfile.Path(alpharep)
2922-
a, b, g = root.iterdir()
2923-
assert a.read_text() == "content of a"
2924-
assert a.read_bytes() == b"content of a"
2925-
2926-
def test_joinpath(self):
2927-
for alpharep in self.zipfile_alpharep():
2928-
root = zipfile.Path(alpharep)
2929-
a = root.joinpath("a")
2930-
assert a.is_file()
2931-
e = root.joinpath("b").joinpath("d").joinpath("e.txt")
2932-
assert e.read_text() == "content of e"
2933-
2934-
def test_traverse_truediv(self):
2935-
for alpharep in self.zipfile_alpharep():
2936-
root = zipfile.Path(alpharep)
2937-
a = root / "a"
2938-
assert a.is_file()
2939-
e = root / "b" / "d" / "e.txt"
2940-
assert e.read_text() == "content of e"
2953+
@pass_alpharep
2954+
def test_read(self, alpharep):
2955+
root = zipfile.Path(alpharep)
2956+
a, b, g = root.iterdir()
2957+
assert a.read_text() == "content of a"
2958+
assert a.read_bytes() == b"content of a"
2959+
2960+
@pass_alpharep
2961+
def test_joinpath(self, alpharep):
2962+
root = zipfile.Path(alpharep)
2963+
a = root.joinpath("a.txt")
2964+
assert a.is_file()
2965+
e = root.joinpath("b").joinpath("d").joinpath("e.txt")
2966+
assert e.read_text() == "content of e"
2967+
2968+
@pass_alpharep
2969+
def test_traverse_truediv(self, alpharep):
2970+
root = zipfile.Path(alpharep)
2971+
a = root / "a.txt"
2972+
assert a.is_file()
2973+
e = root / "b" / "d" / "e.txt"
2974+
assert e.read_text() == "content of e"
2975+
2976+
@pass_alpharep
2977+
def test_traverse_simplediv(self, alpharep):
2978+
"""
2979+
Disable the __future__.division when testing traversal.
2980+
"""
2981+
code = compile(
2982+
source="zipfile.Path(alpharep) / 'a'",
2983+
filename="(test)",
2984+
mode="eval",
2985+
dont_inherit=True,
2986+
)
2987+
eval(code)
29412988

2942-
def test_pathlike_construction(self):
2989+
@pass_alpharep
2990+
def test_pathlike_construction(self, alpharep):
29432991
"""
29442992
zipfile.Path should be constructable from a path-like object
29452993
"""
2946-
for zipfile_ondisk in self.zipfile_ondisk():
2947-
pathlike = pathlib.Path(str(zipfile_ondisk))
2948-
zipfile.Path(pathlike)
2949-
2950-
def test_traverse_pathlike(self):
2951-
for alpharep in self.zipfile_alpharep():
2952-
root = zipfile.Path(alpharep)
2953-
root / pathlib.Path("a")
2954-
2955-
def test_parent(self):
2956-
for alpharep in self.zipfile_alpharep():
2957-
root = zipfile.Path(alpharep)
2958-
assert (root / 'a').parent.at == ''
2959-
assert (root / 'a' / 'b').parent.at == 'a/'
2960-
2961-
def test_dir_parent(self):
2962-
for alpharep in self.zipfile_alpharep():
2963-
root = zipfile.Path(alpharep)
2964-
assert (root / 'b').parent.at == ''
2965-
assert (root / 'b/').parent.at == ''
2966-
2967-
def test_missing_dir_parent(self):
2968-
for alpharep in self.zipfile_alpharep():
2969-
root = zipfile.Path(alpharep)
2970-
assert (root / 'missing dir/').parent.at == ''
2971-
2972-
def test_mutability(self):
2994+
zipfile_ondisk = self.zipfile_ondisk(alpharep)
2995+
pathlike = pathlib.Path(str(zipfile_ondisk))
2996+
zipfile.Path(pathlike)
2997+
2998+
@pass_alpharep
2999+
def test_traverse_pathlike(self, alpharep):
3000+
root = zipfile.Path(alpharep)
3001+
root / pathlib.Path("a")
3002+
3003+
@pass_alpharep
3004+
def test_parent(self, alpharep):
3005+
root = zipfile.Path(alpharep)
3006+
assert (root / 'a').parent.at == ''
3007+
assert (root / 'a' / 'b').parent.at == 'a/'
3008+
3009+
@pass_alpharep
3010+
def test_dir_parent(self, alpharep):
3011+
root = zipfile.Path(alpharep)
3012+
assert (root / 'b').parent.at == ''
3013+
assert (root / 'b/').parent.at == ''
3014+
3015+
@pass_alpharep
3016+
def test_missing_dir_parent(self, alpharep):
3017+
root = zipfile.Path(alpharep)
3018+
assert (root / 'missing dir/').parent.at == ''
3019+
3020+
@pass_alpharep
3021+
def test_mutability(self, alpharep):
29733022
"""
29743023
If the underlying zipfile is changed, the Path object should
29753024
reflect that change.
29763025
"""
2977-
for alpharep in self.zipfile_alpharep():
2978-
root = zipfile.Path(alpharep)
2979-
a, b, g = root.iterdir()
2980-
alpharep.writestr('foo.txt', 'foo')
2981-
alpharep.writestr('bar/baz.txt', 'baz')
2982-
assert any(
2983-
child.name == 'foo.txt'
2984-
for child in root.iterdir())
2985-
assert (root / 'foo.txt').read_text() == 'foo'
2986-
baz, = (root / 'bar').iterdir()
2987-
assert baz.read_text() == 'baz'
3026+
root = zipfile.Path(alpharep)
3027+
a, b, g = root.iterdir()
3028+
alpharep.writestr('foo.txt', 'foo')
3029+
alpharep.writestr('bar/baz.txt', 'baz')
3030+
assert any(child.name == 'foo.txt' for child in root.iterdir())
3031+
assert (root / 'foo.txt').read_text() == 'foo'
3032+
(baz,) = (root / 'bar').iterdir()
3033+
assert baz.read_text() == 'baz'
29883034

29893035
HUGE_ZIPFILE_NUM_ENTRIES = 2 ** 13
29903036

@@ -3013,11 +3059,65 @@ def test_implied_dirs_performance(self):
30133059
data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
30143060
zipfile.CompleteDirs._implied_dirs(data)
30153061

3016-
def test_read_does_not_close(self):
3017-
for alpharep in self.zipfile_ondisk():
3018-
with zipfile.ZipFile(alpharep) as file:
3019-
for rep in range(2):
3020-
zipfile.Path(file, 'a.txt').read_text()
3062+
@pass_alpharep
3063+
def test_read_does_not_close(self, alpharep):
3064+
alpharep = self.zipfile_ondisk(alpharep)
3065+
with zipfile.ZipFile(alpharep) as file:
3066+
for rep in range(2):
3067+
zipfile.Path(file, 'a.txt').read_text()
3068+
3069+
@pass_alpharep
3070+
def test_subclass(self, alpharep):
3071+
class Subclass(zipfile.Path):
3072+
pass
3073+
3074+
root = Subclass(alpharep)
3075+
assert isinstance(root / 'b', Subclass)
3076+
3077+
@pass_alpharep
3078+
def test_filename(self, alpharep):
3079+
root = zipfile.Path(alpharep)
3080+
assert root.filename == pathlib.Path('alpharep.zip')
3081+
3082+
@pass_alpharep
3083+
def test_root_name(self, alpharep):
3084+
"""
3085+
The name of the root should be the name of the zipfile
3086+
"""
3087+
root = zipfile.Path(alpharep)
3088+
assert root.name == 'alpharep.zip' == root.filename.name
3089+
3090+
@pass_alpharep
3091+
def test_root_parent(self, alpharep):
3092+
root = zipfile.Path(alpharep)
3093+
assert root.parent == pathlib.Path('.')
3094+
root.root.filename = 'foo/bar.zip'
3095+
assert root.parent == pathlib.Path('foo')
3096+
3097+
@pass_alpharep
3098+
def test_root_unnamed(self, alpharep):
3099+
"""
3100+
It is an error to attempt to get the name
3101+
or parent of an unnamed zipfile.
3102+
"""
3103+
alpharep.filename = None
3104+
root = zipfile.Path(alpharep)
3105+
with self.assertRaises(TypeError):
3106+
root.name
3107+
with self.assertRaises(TypeError):
3108+
root.parent
3109+
3110+
# .name and .parent should still work on subs
3111+
sub = root / "b"
3112+
assert sub.name == "b"
3113+
assert sub.parent
3114+
3115+
@pass_alpharep
3116+
def test_inheritance(self, alpharep):
3117+
cls = type('PathChild', (zipfile.Path,), {})
3118+
for alpharep in self.zipfile_alpharep():
3119+
file = cls(alpharep).joinpath('some dir').parent
3120+
assert isinstance(file, cls)
30213121

30223122

30233123
if __name__ == "__main__":

0 commit comments

Comments
 (0)