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.
This commit is contained in:
Jason R. Coombs 2022-11-26 13:05:41 -05:00 committed by GitHub
parent 5f8898216e
commit 93f22d30eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 110 additions and 22 deletions

View file

@ -0,0 +1,9 @@
import functools
# from jaraco.functools 3.5.2
def compose(*funcs):
def compose_two(f1, f2):
return lambda *args, **kwargs: f1(f2(*args, **kwargs))
return functools.reduce(compose_two, funcs)

View file

@ -0,0 +1,12 @@
# from more_itertools v8.13.0
def always_iterable(obj, base_type=(str, bytes)):
if obj is None:
return iter(())
if (base_type is not None) and isinstance(obj, base_type):
return iter((obj,))
try:
return iter(obj)
except TypeError:
return iter((obj,))

View file

@ -0,0 +1,39 @@
import types
import functools
from ._itertools import always_iterable
def parameterize(names, value_groups):
"""
Decorate a test method to run it as a set of subtests.
Modeled after pytest.parametrize.
"""
def decorator(func):
@functools.wraps(func)
def wrapped(self):
for values in value_groups:
resolved = map(Invoked.eval, always_iterable(values))
params = dict(zip(always_iterable(names), resolved))
with self.subTest(**params):
func(self, **params)
return wrapped
return decorator
class Invoked(types.SimpleNamespace):
"""
Wrap a function to be invoked for each usage.
"""
@classmethod
def wrap(cls, func):
return cls(func=func)
@classmethod
def eval(cls, cand):
return cand.func() if isinstance(cand, cls) else cand

View file

@ -4,7 +4,12 @@
import pathlib
import unittest
import string
import functools
import pickle
import itertools
from ._test_params import parameterize, Invoked
from ._functools import compose
from test.support.os_helper import temp_dir
@ -76,18 +81,12 @@ def build_alpharep_fixture():
return zf
def pass_alpharep(meth):
"""
Given a method, wrap it in a for loop that invokes method
with each subtest.
"""
alpharep_generators = [
Invoked.wrap(build_alpharep_fixture),
Invoked.wrap(compose(add_dirs, build_alpharep_fixture)),
]
@functools.wraps(meth)
def wrapper(self):
for alpharep in self.zipfile_alpharep():
meth(self, alpharep=alpharep)
return wrapper
pass_alpharep = parameterize(['alpharep'], alpharep_generators)
class TestPath(unittest.TestCase):
@ -95,12 +94,6 @@ def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
def zipfile_alpharep(self):
with self.subTest():
yield build_alpharep_fixture()
with self.subTest():
yield add_dirs(build_alpharep_fixture())
def zipfile_ondisk(self, alpharep):
tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
buffer = alpharep.fp
@ -418,6 +411,21 @@ def test_root_unnamed(self, alpharep):
@pass_alpharep
def test_inheritance(self, alpharep):
cls = type('PathChild', (zipfile.Path,), {})
for alpharep in self.zipfile_alpharep():
file = cls(alpharep).joinpath('some dir').parent
assert isinstance(file, cls)
file = cls(alpharep).joinpath('some dir').parent
assert isinstance(file, cls)
@parameterize(
['alpharep', 'path_type', 'subpath'],
itertools.product(
alpharep_generators,
[str, pathlib.Path],
['', 'b/'],
),
)
def test_pickle(self, alpharep, path_type, subpath):
zipfile_ondisk = path_type(self.zipfile_ondisk(alpharep))
saved_1 = pickle.dumps(zipfile.Path(zipfile_ondisk, at=subpath))
restored_1 = pickle.loads(saved_1)
first, *rest = restored_1.iterdir()
assert first.read_text().startswith('content of ')

View file

@ -62,7 +62,25 @@ def _difference(minuend, subtrahend):
return itertools.filterfalse(set(subtrahend).__contains__, minuend)
class CompleteDirs(zipfile.ZipFile):
class InitializedState:
"""
Mix-in to save the initialization state for pickling.
"""
def __init__(self, *args, **kwargs):
self.__args = args
self.__kwargs = kwargs
super().__init__(*args, **kwargs)
def __getstate__(self):
return self.__args, self.__kwargs
def __setstate__(self, state):
args, kwargs = state
super().__init__(*args, **kwargs)
class CompleteDirs(InitializedState, zipfile.ZipFile):
"""
A ZipFile subclass that ensures that implied directories
are always included in the namelist.

View file

@ -0,0 +1,2 @@
``zipfile.Path`` is now pickleable if its initialization parameters were
pickleable (e.g. for file system paths).