From 93f22d30eb7bf579d511b1866674bc1c2513dde9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Nov 2022 13:05:41 -0500 Subject: 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. --- Lib/test/test_zipfile/_functools.py | 9 ++++ Lib/test/test_zipfile/_itertools.py | 12 ++++++ Lib/test/test_zipfile/_test_params.py | 39 +++++++++++++++++ Lib/test/test_zipfile/test_path.py | 50 +++++++++++++--------- Lib/zipfile/_path.py | 20 ++++++++- .../2022-10-08-19-20-33.gh-issue-98108.WUObqM.rst | 2 + 6 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 Lib/test/test_zipfile/_functools.py create mode 100644 Lib/test/test_zipfile/_itertools.py create mode 100644 Lib/test/test_zipfile/_test_params.py create mode 100644 Misc/NEWS.d/next/Library/2022-10-08-19-20-33.gh-issue-98108.WUObqM.rst diff --git a/Lib/test/test_zipfile/_functools.py b/Lib/test/test_zipfile/_functools.py new file mode 100644 index 0000000..75f2b20 --- /dev/null +++ b/Lib/test/test_zipfile/_functools.py @@ -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) diff --git a/Lib/test/test_zipfile/_itertools.py b/Lib/test/test_zipfile/_itertools.py new file mode 100644 index 0000000..559f3f1 --- /dev/null +++ b/Lib/test/test_zipfile/_itertools.py @@ -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,)) diff --git a/Lib/test/test_zipfile/_test_params.py b/Lib/test/test_zipfile/_test_params.py new file mode 100644 index 0000000..bc95b4e --- /dev/null +++ b/Lib/test/test_zipfile/_test_params.py @@ -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 diff --git a/Lib/test/test_zipfile/test_path.py b/Lib/test/test_zipfile/test_path.py index 3c62e9a..02253c5 100644 --- a/Lib/test/test_zipfile/test_path.py +++ b/Lib/test/test_zipfile/test_path.py @@ -4,7 +4,12 @@ import contextlib 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. - """ - - @functools.wraps(meth) - def wrapper(self): - for alpharep in self.zipfile_alpharep(): - meth(self, alpharep=alpharep) +alpharep_generators = [ + Invoked.wrap(build_alpharep_fixture), + Invoked.wrap(compose(add_dirs, build_alpharep_fixture)), +] - return wrapper +pass_alpharep = parameterize(['alpharep'], alpharep_generators) class TestPath(unittest.TestCase): @@ -95,12 +94,6 @@ class TestPath(unittest.TestCase): 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 @@ class TestPath(unittest.TestCase): @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 ') diff --git a/Lib/zipfile/_path.py b/Lib/zipfile/_path.py index 67ef07a..aea17b6 100644 --- a/Lib/zipfile/_path.py +++ b/Lib/zipfile/_path.py @@ -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. diff --git a/Misc/NEWS.d/next/Library/2022-10-08-19-20-33.gh-issue-98108.WUObqM.rst b/Misc/NEWS.d/next/Library/2022-10-08-19-20-33.gh-issue-98108.WUObqM.rst new file mode 100644 index 0000000..7e96258 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-10-08-19-20-33.gh-issue-98108.WUObqM.rst @@ -0,0 +1,2 @@ +``zipfile.Path`` is now pickleable if its initialization parameters were +pickleable (e.g. for file system paths). -- cgit v0.12