Skip to content

Commit 93f22d3

Browse files
authored
gh-98108: Add limited pickleability to zipfile.Path (GH-98109)
* gh-98098: Move zipfile into a package. * Moved test_zipfile to a package * Extracted module for test_path. * Add blurb * Add jaraco as owner of zipfile.Path. * Synchronize with minor changes found at jaraco/zipp@d9e7f4352d. * gh-98108: Sync with zipp 3.9.1 adding pickleability.
1 parent 5f88982 commit 93f22d3

File tree

6 files changed

+110
-22
lines changed

6 files changed

+110
-22
lines changed

Lib/test/test_zipfile/_functools.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import functools
2+
3+
4+
# from jaraco.functools 3.5.2
5+
def compose(*funcs):
6+
def compose_two(f1, f2):
7+
return lambda *args, **kwargs: f1(f2(*args, **kwargs))
8+
9+
return functools.reduce(compose_two, funcs)

Lib/test/test_zipfile/_itertools.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# from more_itertools v8.13.0
2+
def always_iterable(obj, base_type=(str, bytes)):
3+
if obj is None:
4+
return iter(())
5+
6+
if (base_type is not None) and isinstance(obj, base_type):
7+
return iter((obj,))
8+
9+
try:
10+
return iter(obj)
11+
except TypeError:
12+
return iter((obj,))

Lib/test/test_zipfile/_test_params.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import types
2+
import functools
3+
4+
from ._itertools import always_iterable
5+
6+
7+
def parameterize(names, value_groups):
8+
"""
9+
Decorate a test method to run it as a set of subtests.
10+
11+
Modeled after pytest.parametrize.
12+
"""
13+
14+
def decorator(func):
15+
@functools.wraps(func)
16+
def wrapped(self):
17+
for values in value_groups:
18+
resolved = map(Invoked.eval, always_iterable(values))
19+
params = dict(zip(always_iterable(names), resolved))
20+
with self.subTest(**params):
21+
func(self, **params)
22+
23+
return wrapped
24+
25+
return decorator
26+
27+
28+
class Invoked(types.SimpleNamespace):
29+
"""
30+
Wrap a function to be invoked for each usage.
31+
"""
32+
33+
@classmethod
34+
def wrap(cls, func):
35+
return cls(func=func)
36+
37+
@classmethod
38+
def eval(cls, cand):
39+
return cand.func() if isinstance(cand, cls) else cand

Lib/test/test_zipfile/test_path.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
import pathlib
55
import unittest
66
import string
7-
import functools
7+
import pickle
8+
import itertools
9+
10+
from ._test_params import parameterize, Invoked
11+
from ._functools import compose
12+
813

914
from test.support.os_helper import temp_dir
1015

@@ -76,31 +81,19 @@ def build_alpharep_fixture():
7681
return zf
7782

7883

79-
def pass_alpharep(meth):
80-
"""
81-
Given a method, wrap it in a for loop that invokes method
82-
with each subtest.
83-
"""
84-
85-
@functools.wraps(meth)
86-
def wrapper(self):
87-
for alpharep in self.zipfile_alpharep():
88-
meth(self, alpharep=alpharep)
84+
alpharep_generators = [
85+
Invoked.wrap(build_alpharep_fixture),
86+
Invoked.wrap(compose(add_dirs, build_alpharep_fixture)),
87+
]
8988

90-
return wrapper
89+
pass_alpharep = parameterize(['alpharep'], alpharep_generators)
9190

9291

9392
class TestPath(unittest.TestCase):
9493
def setUp(self):
9594
self.fixtures = contextlib.ExitStack()
9695
self.addCleanup(self.fixtures.close)
9796

98-
def zipfile_alpharep(self):
99-
with self.subTest():
100-
yield build_alpharep_fixture()
101-
with self.subTest():
102-
yield add_dirs(build_alpharep_fixture())
103-
10497
def zipfile_ondisk(self, alpharep):
10598
tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
10699
buffer = alpharep.fp
@@ -418,6 +411,21 @@ def test_root_unnamed(self, alpharep):
418411
@pass_alpharep
419412
def test_inheritance(self, alpharep):
420413
cls = type('PathChild', (zipfile.Path,), {})
421-
for alpharep in self.zipfile_alpharep():
422-
file = cls(alpharep).joinpath('some dir').parent
423-
assert isinstance(file, cls)
414+
file = cls(alpharep).joinpath('some dir').parent
415+
assert isinstance(file, cls)
416+
417+
@parameterize(
418+
['alpharep', 'path_type', 'subpath'],
419+
itertools.product(
420+
alpharep_generators,
421+
[str, pathlib.Path],
422+
['', 'b/'],
423+
),
424+
)
425+
def test_pickle(self, alpharep, path_type, subpath):
426+
zipfile_ondisk = path_type(self.zipfile_ondisk(alpharep))
427+
428+
saved_1 = pickle.dumps(zipfile.Path(zipfile_ondisk, at=subpath))
429+
restored_1 = pickle.loads(saved_1)
430+
first, *rest = restored_1.iterdir()
431+
assert first.read_text().startswith('content of ')

Lib/zipfile/_path.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,25 @@ def _difference(minuend, subtrahend):
6262
return itertools.filterfalse(set(subtrahend).__contains__, minuend)
6363

6464

65-
class CompleteDirs(zipfile.ZipFile):
65+
class InitializedState:
66+
"""
67+
Mix-in to save the initialization state for pickling.
68+
"""
69+
70+
def __init__(self, *args, **kwargs):
71+
self.__args = args
72+
self.__kwargs = kwargs
73+
super().__init__(*args, **kwargs)
74+
75+
def __getstate__(self):
76+
return self.__args, self.__kwargs
77+
78+
def __setstate__(self, state):
79+
args, kwargs = state
80+
super().__init__(*args, **kwargs)
81+
82+
83+
class CompleteDirs(InitializedState, zipfile.ZipFile):
6684
"""
6785
A ZipFile subclass that ensures that implied directories
6886
are always included in the namelist.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``zipfile.Path`` is now pickleable if its initialization parameters were
2+
pickleable (e.g. for file system paths).

0 commit comments

Comments
 (0)