]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-98108: Add limited pickleability to zipfile.Path (GH-98109)
authorJason R. Coombs <jaraco@jaraco.com>
Sat, 26 Nov 2022 18:05:41 +0000 (13:05 -0500)
committerGitHub <noreply@github.com>
Sat, 26 Nov 2022 18:05:41 +0000 (13:05 -0500)
* 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 [new file with mode: 0644]
Lib/test/test_zipfile/_itertools.py [new file with mode: 0644]
Lib/test/test_zipfile/_test_params.py [new file with mode: 0644]
Lib/test/test_zipfile/test_path.py
Lib/zipfile/_path.py
Misc/NEWS.d/next/Library/2022-10-08-19-20-33.gh-issue-98108.WUObqM.rst [new file with mode: 0644]

diff --git a/Lib/test/test_zipfile/_functools.py b/Lib/test/test_zipfile/_functools.py
new file mode 100644 (file)
index 0000000..75f2b20
--- /dev/null
@@ -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 (file)
index 0000000..559f3f1
--- /dev/null
@@ -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 (file)
index 0000000..bc95b4e
--- /dev/null
@@ -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
index 3c62e9a0b0e65dc720033c080845fdf4fb58c3d8..02253c59e959fbfd9ac13753bfd3cd20658f2fe7 100644 (file)
@@ -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 ')
index 67ef07a130d1ad79338bb04661b3386c85e56350..aea17b65b6aa2dfabac1875235f0b434d0480639 100644 (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.
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 (file)
index 0000000..7e96258
--- /dev/null
@@ -0,0 +1,2 @@
+``zipfile.Path`` is now pickleable if its initialization parameters were
+pickleable (e.g. for file system paths).