From 883fdc9c5088fd327f4e80da43f659fc75bfcb19 Mon Sep 17 00:00:00 2001 From: Parker Hancock <633163+parkerhancock@users.noreply.github.com> Date: Tue, 31 May 2022 14:59:40 -0500 Subject: [PATCH 01/11] create lazy loading lookup class to support pickling --- test_zipp.py | 19 +++++++++++++++++++ zipp.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/test_zipp.py b/test_zipp.py index 77716a0..4124b3e 100644 --- a/test_zipp.py +++ b/test_zipp.py @@ -7,6 +7,7 @@ import shutil import string import functools +import pickle import jaraco.itertools import func_timeout @@ -416,3 +417,21 @@ def test_inheritance(self, alpharep): for alpharep in self.zipfile_alpharep(): file = cls(alpharep).joinpath('some dir').parent assert isinstance(file, cls) + + def test_can_pickle_string_path(self): + try: + path_1 = zipp.Path("/path/to/a/file.zip") + path_1_pickle = pickle.dumps(path_1) + path_2 = zipp.Path("/path/to/a/file.zip", at="something.txt") + path_2_pickle = pickle.dumps(path_2) + except TypeError as exec: + assert False, "TypeError: Path is not pickleable" + + def test_can_pickle_pathlib_path(self): + try: + path_1 = zipp.Path(pathlib.Path("/path/to/a/file.zip")) + path_1_pickle = pickle.dumps(path_1) + path_2 = zipp.Path(pathlib.Path("/path/to/a/file.zip", at="something.txt")) + path_2_pickle = pickle.dumps(path_2) + except TypeError as exec: + assert False, "TypeError: Path is not pickleable" diff --git a/zipp.py b/zipp.py index 52c82a0..941a287 100644 --- a/zipp.py +++ b/zipp.py @@ -61,6 +61,23 @@ def _difference(minuend, subtrahend): """ return itertools.filterfalse(set(subtrahend).__contains__, minuend) +class LazyClass(): + """ + Utility object that wraps another object that, for example, + might hold a file object, and emits a new object on every + method call""" + _class = object + _exempt_names = ("__getstate__", "__setstate__") + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + def __getattr__(self, name): + if name in self._exempt_names: + raise AttributeError(f"type object '{type(self)}' has not attribute '{name}'") + obj = self._class(*self.args, **self.kwargs) + return getattr(obj, name) + class CompleteDirs(zipfile.ZipFile): """ @@ -129,6 +146,9 @@ def _name_set(self): self.__lookup = super(FastLookup, self)._name_set() return self.__lookup +class LazyFastLookup(LazyClass): + _class = FastLookup + class Path: """ @@ -220,9 +240,32 @@ def __init__(self, root, at=""): original type, the caller should either create a separate ZipFile object or pass a filename. """ - self.root = FastLookup.make(root) + self.root = self._make_root(root) self.at = at + def _make_root(self, source): + """ + Given a source (filename or zipfile), return an + appropriate root object. + """ + if isinstance(source, CompleteDirs): + return source + + if isinstance(source, (str, pathlib.Path)): + return LazyFastLookup(source) + + if not isinstance(source, zipfile.ZipFile): + return FastLookup(source) + + # Only allow for FastLookup when supplied zipfile is read-only + if 'r' not in source.mode: + subcls = CompleteDirs + else: + subcls = FastLookup + + source.__class__ = subcls + return source + def open(self, mode='r', *args, pwd=None, **kwargs): """ Open this entry as text or binary following the semantics From 754b659b06abdc406dfe41a213a855ee0b4a98c0 Mon Sep 17 00:00:00 2001 From: Parker Hancock <633163+parkerhancock@users.noreply.github.com> Date: Tue, 31 May 2022 15:48:37 -0500 Subject: [PATCH 02/11] memoized form of pickling utility class and improved tests --- test_zipp.py | 8 ++++++++ zipp.py | 46 +++++++++++++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/test_zipp.py b/test_zipp.py index 4124b3e..92febdc 100644 --- a/test_zipp.py +++ b/test_zipp.py @@ -422,8 +422,12 @@ def test_can_pickle_string_path(self): try: path_1 = zipp.Path("/path/to/a/file.zip") path_1_pickle = pickle.dumps(path_1) + path_1_load = pickle.loads(path_1_pickle) + assert path_1.root == path_1_load.root path_2 = zipp.Path("/path/to/a/file.zip", at="something.txt") path_2_pickle = pickle.dumps(path_2) + path_2_load = pickle.loads(path_2_pickle) + assert path_2.root == path_2_load.root except TypeError as exec: assert False, "TypeError: Path is not pickleable" @@ -431,7 +435,11 @@ def test_can_pickle_pathlib_path(self): try: path_1 = zipp.Path(pathlib.Path("/path/to/a/file.zip")) path_1_pickle = pickle.dumps(path_1) + path_1_load = pickle.loads(path_1_pickle) + assert path_1.root == path_1_load.root path_2 = zipp.Path(pathlib.Path("/path/to/a/file.zip", at="something.txt")) path_2_pickle = pickle.dumps(path_2) + path_2_load = pickle.loads(path_2_pickle) + assert path_2.root == path_2_load.root except TypeError as exec: assert False, "TypeError: Path is not pickleable" diff --git a/zipp.py b/zipp.py index 941a287..fa48868 100644 --- a/zipp.py +++ b/zipp.py @@ -61,22 +61,38 @@ def _difference(minuend, subtrahend): """ return itertools.filterfalse(set(subtrahend).__contains__, minuend) -class LazyClass(): +class PickleableClass(): """ - Utility object that wraps another object that, for example, - might hold a file object, and emits a new object on every - method call""" + Utility object that wraps another un-pickleable object that, + for example, might hold a file object, and saves the + initialization parameters. When pickeled, the un-pickleable + object is discarded, and when loaded, it is rebuilt from the + initialization params.""" _class = object - _exempt_names = ("__getstate__", "__setstate__") + def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs + self._args = args + self._kwargs = kwargs + self._obj = None def __getattr__(self, name): - if name in self._exempt_names: - raise AttributeError(f"type object '{type(self)}' has not attribute '{name}'") - obj = self._class(*self.args, **self.kwargs) - return getattr(obj, name) + if self._obj is None: + self._obj = self._class(*self._args, **self._kwargs) + return getattr(self._obj, name) + + def __getstate__(self): + state = self.__dict__.copy() + state['_obj'] = None + return state + + def __setstate__(self, state): + self.__dict__.update(state) + + def __repr__(self): + return f"{self.__class__.__name__}(class={self._class}, args={self._args}, kwargs={self._kwargs})" + + def __eq__(self, other): + return self._class == other._class and self._args == other._args and self._kwargs == other._kwargs class CompleteDirs(zipfile.ZipFile): @@ -146,9 +162,13 @@ def _name_set(self): self.__lookup = super(FastLookup, self)._name_set() return self.__lookup -class LazyFastLookup(LazyClass): +class PickleableFastLookup(PickleableClass): _class = FastLookup + @property + def filename(self): + return self._args[0] + class Path: """ @@ -252,7 +272,7 @@ def _make_root(self, source): return source if isinstance(source, (str, pathlib.Path)): - return LazyFastLookup(source) + return PickleableFastLookup(source) if not isinstance(source, zipfile.ZipFile): return FastLookup(source) From 25285159390d1ff78f62b0361f647ad0a9dd0ce4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 11:20:14 -0400 Subject: [PATCH 03/11] Test assertions directly --- test_zipp.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/test_zipp.py b/test_zipp.py index 92febdc..78104e5 100644 --- a/test_zipp.py +++ b/test_zipp.py @@ -419,27 +419,21 @@ def test_inheritance(self, alpharep): assert isinstance(file, cls) def test_can_pickle_string_path(self): - try: - path_1 = zipp.Path("/path/to/a/file.zip") - path_1_pickle = pickle.dumps(path_1) - path_1_load = pickle.loads(path_1_pickle) - assert path_1.root == path_1_load.root - path_2 = zipp.Path("/path/to/a/file.zip", at="something.txt") - path_2_pickle = pickle.dumps(path_2) - path_2_load = pickle.loads(path_2_pickle) - assert path_2.root == path_2_load.root - except TypeError as exec: - assert False, "TypeError: Path is not pickleable" + path_1 = zipp.Path("/path/to/a/file.zip") + path_1_pickle = pickle.dumps(path_1) + path_1_load = pickle.loads(path_1_pickle) + assert path_1.root == path_1_load.root + path_2 = zipp.Path("/path/to/a/file.zip", at="something.txt") + path_2_pickle = pickle.dumps(path_2) + path_2_load = pickle.loads(path_2_pickle) + assert path_2.root == path_2_load.root def test_can_pickle_pathlib_path(self): - try: - path_1 = zipp.Path(pathlib.Path("/path/to/a/file.zip")) - path_1_pickle = pickle.dumps(path_1) - path_1_load = pickle.loads(path_1_pickle) - assert path_1.root == path_1_load.root - path_2 = zipp.Path(pathlib.Path("/path/to/a/file.zip", at="something.txt")) - path_2_pickle = pickle.dumps(path_2) - path_2_load = pickle.loads(path_2_pickle) - assert path_2.root == path_2_load.root - except TypeError as exec: - assert False, "TypeError: Path is not pickleable" + path_1 = zipp.Path(pathlib.Path("/path/to/a/file.zip")) + path_1_pickle = pickle.dumps(path_1) + path_1_load = pickle.loads(path_1_pickle) + assert path_1.root == path_1_load.root + path_2 = zipp.Path(pathlib.Path("/path/to/a/file.zip", at="something.txt")) + path_2_pickle = pickle.dumps(path_2) + path_2_load = pickle.loads(path_2_pickle) + assert path_2.root == path_2_load.root From 0abd2dc80f1e3adfe8c752681fb14bab8831cef8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 11:23:00 -0400 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test_zipp.py | 2 +- zipp.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/test_zipp.py b/test_zipp.py index 78104e5..af2fed3 100644 --- a/test_zipp.py +++ b/test_zipp.py @@ -418,7 +418,7 @@ def test_inheritance(self, alpharep): file = cls(alpharep).joinpath('some dir').parent assert isinstance(file, cls) - def test_can_pickle_string_path(self): + def test_can_pickle_string_path(self): path_1 = zipp.Path("/path/to/a/file.zip") path_1_pickle = pickle.dumps(path_1) path_1_load = pickle.loads(path_1_pickle) diff --git a/zipp.py b/zipp.py index fa48868..97a7977 100644 --- a/zipp.py +++ b/zipp.py @@ -61,13 +61,15 @@ def _difference(minuend, subtrahend): """ return itertools.filterfalse(set(subtrahend).__contains__, minuend) -class PickleableClass(): + +class PickleableClass: """ - Utility object that wraps another un-pickleable object that, - for example, might hold a file object, and saves the + Utility object that wraps another un-pickleable object that, + for example, might hold a file object, and saves the initialization parameters. When pickeled, the un-pickleable object is discarded, and when loaded, it is rebuilt from the initialization params.""" + _class = object def __init__(self, *args, **kwargs): @@ -92,7 +94,11 @@ def __repr__(self): return f"{self.__class__.__name__}(class={self._class}, args={self._args}, kwargs={self._kwargs})" def __eq__(self, other): - return self._class == other._class and self._args == other._args and self._kwargs == other._kwargs + return ( + self._class == other._class + and self._args == other._args + and self._kwargs == other._kwargs + ) class CompleteDirs(zipfile.ZipFile): @@ -162,6 +168,7 @@ def _name_set(self): self.__lookup = super(FastLookup, self)._name_set() return self.__lookup + class PickleableFastLookup(PickleableClass): _class = FastLookup From 7c49e12f37399ee1ab68bd528d468e28528f3d0b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 11:24:58 -0400 Subject: [PATCH 05/11] Fix long line --- zipp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zipp.py b/zipp.py index 97a7977..6d68643 100644 --- a/zipp.py +++ b/zipp.py @@ -91,7 +91,8 @@ def __setstate__(self, state): self.__dict__.update(state) def __repr__(self): - return f"{self.__class__.__name__}(class={self._class}, args={self._args}, kwargs={self._kwargs})" + args = f"class={self._class}, args={self._args}, kwargs={self._kwargs}" + return f"{self.__class__.__name__}({args})" def __eq__(self, other): return ( From 9dffcf3c1a772325311345ed1d1137c6ad5c79b3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 11:26:24 -0400 Subject: [PATCH 06/11] Declare a type of 'type' for class_; addressing mypy failure. --- zipp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zipp.py b/zipp.py index 6d68643..6bdc0a2 100644 --- a/zipp.py +++ b/zipp.py @@ -70,7 +70,7 @@ class PickleableClass: object is discarded, and when loaded, it is rebuilt from the initialization params.""" - _class = object + _class: type = object def __init__(self, *args, **kwargs): self._args = args From 6f700a439e7535878fdc5639e6368ee3a9691221 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 11:55:35 -0400 Subject: [PATCH 07/11] Create a mix-in for InitializedState --- zipp.py | 74 ++++++++------------------------------------------------- 1 file changed, 10 insertions(+), 64 deletions(-) diff --git a/zipp.py b/zipp.py index 6bdc0a2..07b81d5 100644 --- a/zipp.py +++ b/zipp.py @@ -62,47 +62,24 @@ def _difference(minuend, subtrahend): return itertools.filterfalse(set(subtrahend).__contains__, minuend) -class PickleableClass: +class InitializedState: + """ + Mix-in to save the initialization state for pickling. """ - Utility object that wraps another un-pickleable object that, - for example, might hold a file object, and saves the - initialization parameters. When pickeled, the un-pickleable - object is discarded, and when loaded, it is rebuilt from the - initialization params.""" - - _class: type = object def __init__(self, *args, **kwargs): - self._args = args - self._kwargs = kwargs - self._obj = None - - def __getattr__(self, name): - if self._obj is None: - self._obj = self._class(*self._args, **self._kwargs) - return getattr(self._obj, name) + self.__args = args + self.__kwargs = kwargs + super().__init__(*args, **kwargs) def __getstate__(self): - state = self.__dict__.copy() - state['_obj'] = None - return state + return dict(args=self.__args, kwargs=self.__kwargs) def __setstate__(self, state): - self.__dict__.update(state) + super().__init__(*state['args'], **state['kwargs']) - def __repr__(self): - args = f"class={self._class}, args={self._args}, kwargs={self._kwargs}" - return f"{self.__class__.__name__}({args})" - def __eq__(self, other): - return ( - self._class == other._class - and self._args == other._args - and self._kwargs == other._kwargs - ) - - -class CompleteDirs(zipfile.ZipFile): +class CompleteDirs(InitializedState, zipfile.ZipFile): """ A ZipFile subclass that ensures that implied directories are always included in the namelist. @@ -170,14 +147,6 @@ def _name_set(self): return self.__lookup -class PickleableFastLookup(PickleableClass): - _class = FastLookup - - @property - def filename(self): - return self._args[0] - - class Path: """ A pathlib-compatible interface for zip files. @@ -268,32 +237,9 @@ def __init__(self, root, at=""): original type, the caller should either create a separate ZipFile object or pass a filename. """ - self.root = self._make_root(root) + self.root = FastLookup.make(root) self.at = at - def _make_root(self, source): - """ - Given a source (filename or zipfile), return an - appropriate root object. - """ - if isinstance(source, CompleteDirs): - return source - - if isinstance(source, (str, pathlib.Path)): - return PickleableFastLookup(source) - - if not isinstance(source, zipfile.ZipFile): - return FastLookup(source) - - # Only allow for FastLookup when supplied zipfile is read-only - if 'r' not in source.mode: - subcls = CompleteDirs - else: - subcls = FastLookup - - source.__class__ = subcls - return source - def open(self, mode='r', *args, pwd=None, **kwargs): """ Open this entry as text or binary following the semantics From f945f0a4b9abab2449f5fd75d62e09cfe6dc35a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 12:35:05 -0400 Subject: [PATCH 08/11] Update tests to use existing files on disk. Fixes failing tests. --- test_zipp.py | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/test_zipp.py b/test_zipp.py index af2fed3..b8305c3 100644 --- a/test_zipp.py +++ b/test_zipp.py @@ -418,22 +418,30 @@ def test_inheritance(self, alpharep): file = cls(alpharep).joinpath('some dir').parent assert isinstance(file, cls) - def test_can_pickle_string_path(self): - path_1 = zipp.Path("/path/to/a/file.zip") - path_1_pickle = pickle.dumps(path_1) - path_1_load = pickle.loads(path_1_pickle) - assert path_1.root == path_1_load.root - path_2 = zipp.Path("/path/to/a/file.zip", at="something.txt") - path_2_pickle = pickle.dumps(path_2) - path_2_load = pickle.loads(path_2_pickle) - assert path_2.root == path_2_load.root - - def test_can_pickle_pathlib_path(self): - path_1 = zipp.Path(pathlib.Path("/path/to/a/file.zip")) - path_1_pickle = pickle.dumps(path_1) - path_1_load = pickle.loads(path_1_pickle) - assert path_1.root == path_1_load.root - path_2 = zipp.Path(pathlib.Path("/path/to/a/file.zip", at="something.txt")) - path_2_pickle = pickle.dumps(path_2) - path_2_load = pickle.loads(path_2_pickle) - assert path_2.root == path_2_load.root + @pass_alpharep + def test_can_pickle_string_path(self, alpharep): + zipfile_ondisk = str(self.zipfile_ondisk(alpharep)) + + saved_1 = pickle.dumps(zipp.Path(zipfile_ondisk)) + restored_1 = pickle.loads(saved_1) + a, b, g = restored_1.iterdir() + assert a.read_text() == "content of a" + + saved_2 = pickle.dumps(zipp.Path(zipfile_ondisk, at="b/")) + restored_2 = pickle.loads(saved_2) + c, d, f = restored_2.iterdir() + assert c.read_text() == 'content of c' + + @pass_alpharep + def test_can_pickle_pathlib_path(self, alpharep): + zipfile_ondisk = self.zipfile_ondisk(alpharep) + + saved_1 = pickle.dumps(zipp.Path(zipfile_ondisk)) + restored_1 = pickle.loads(saved_1) + a, b, g = restored_1.iterdir() + assert a.read_text() == "content of a" + + saved_2 = pickle.dumps(zipp.Path(zipfile_ondisk, at="b/")) + restored_2 = pickle.loads(saved_2) + c, d, f = restored_2.iterdir() + assert c.read_text() == 'content of c' From b376033a8b420097ca72588ba2b5ca6fab28a9fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 13:42:32 -0400 Subject: [PATCH 09/11] Consolidate pickle tests into a single, parameterized test. --- test_zipp.py | 55 +++++++++++++++++++++------------------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/test_zipp.py b/test_zipp.py index 3f7b0f4..c3890d7 100644 --- a/test_zipp.py +++ b/test_zipp.py @@ -7,6 +7,7 @@ import shutil import string import pickle +import itertools import jaraco.itertools import func_timeout @@ -75,13 +76,12 @@ def temp_dir(): shutil.rmtree(tmpdir) -pass_alpharep = parameterize( - ['alpharep'], - [ - Invoked.wrap(build_alpharep_fixture), - Invoked.wrap(compose(add_dirs, build_alpharep_fixture)), - ], -) +alpharep_generators = [ + Invoked.wrap(build_alpharep_fixture), + Invoked.wrap(compose(add_dirs, build_alpharep_fixture)), +] + +pass_alpharep = parameterize(['alpharep'], alpharep_generators) class TestPath(unittest.TestCase): @@ -409,30 +409,19 @@ def test_inheritance(self, alpharep): file = cls(alpharep).joinpath('some dir').parent assert isinstance(file, cls) - @pass_alpharep - def test_can_pickle_string_path(self, alpharep): - zipfile_ondisk = str(self.zipfile_ondisk(alpharep)) - - saved_1 = pickle.dumps(zipp.Path(zipfile_ondisk)) - restored_1 = pickle.loads(saved_1) - a, b, g = restored_1.iterdir() - assert a.read_text() == "content of a" - - saved_2 = pickle.dumps(zipp.Path(zipfile_ondisk, at="b/")) - restored_2 = pickle.loads(saved_2) - c, d, f = restored_2.iterdir() - assert c.read_text() == 'content of c' - - @pass_alpharep - def test_can_pickle_pathlib_path(self, alpharep): - zipfile_ondisk = self.zipfile_ondisk(alpharep) - - saved_1 = pickle.dumps(zipp.Path(zipfile_ondisk)) + @parameterize( + ['alpharep', 'path_type', 'subpath'], + itertools.product( + alpharep_generators, + [str, pathlib.Path], + ['', 'b/'], + ), + ) + def test_pickle(self, alpharep, path_type, subpath): + print(alpharep, path_type, subpath) + zipfile_ondisk = path_type(self.zipfile_ondisk(alpharep)) + + saved_1 = pickle.dumps(zipp.Path(zipfile_ondisk, at=subpath)) restored_1 = pickle.loads(saved_1) - a, b, g = restored_1.iterdir() - assert a.read_text() == "content of a" - - saved_2 = pickle.dumps(zipp.Path(zipfile_ondisk, at="b/")) - restored_2 = pickle.loads(saved_2) - c, d, f = restored_2.iterdir() - assert c.read_text() == 'content of c' + first, *rest = restored_1.iterdir() + assert first.read_text().startswith('content of ') From f03985236ffa4e3bdd76c3decd21bca9cae41ae3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 13:45:15 -0400 Subject: [PATCH 10/11] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0983837..3ec5467 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v3.9.0 +====== + +* #81: ``Path`` objects are now pickleable if they've been + constructed from pickleable objects. Any restored objects + will re-construct the zip file with the original arguments. + v3.8.1 ====== From c017444bc2f2b7d04bf974f6be818debd2ff58df Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Oct 2022 13:55:22 -0400 Subject: [PATCH 11/11] No need to construct a dict for the state. --- zipp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zipp.py b/zipp.py index 07b81d5..be1bdb0 100644 --- a/zipp.py +++ b/zipp.py @@ -73,10 +73,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def __getstate__(self): - return dict(args=self.__args, kwargs=self.__kwargs) + return self.__args, self.__kwargs def __setstate__(self, state): - super().__init__(*state['args'], **state['kwargs']) + args, kwargs = state + super().__init__(*args, **kwargs) class CompleteDirs(InitializedState, zipfile.ZipFile):