Repository: jaraco/zipp

Issue:
Usage of zipp.Path causes attempt to write after ZipFile is closed
Hello! I am attempting to use `zipp.Path` to construct a zip archive. I encountered the following issue:

```python
import io
from zipfile import ZipFile
import zipp

def make_zip():
    """Make zip file and return bytes."""
    bytes_io = io.BytesIO()
    zip_file = ZipFile(bytes_io, mode="w")
    zipp_path = zipp.Path(zip_file)
    
    # use zipp.Path.open
    with zipp_path.joinpath("file-a").open(mode="wb") as fp:
        fp.write(b"contents of file-a")

    zip_file.close()

    data = bytes_io.getvalue()

    bytes_io.close()
    
    return data

zip_data = make_zip()
# Exception ignored in: <bound method ZipFile.__del__ of <zipp.CompleteDirs [closed]>>
# Traceback (most recent call last):
#   File "/Users/nw_henderson/.pyenv/versions/3.6.10/lib/python3.6/zipfile.py", line 1686, in __del__
#     self.close()
#   File "/Users/nw_henderson/.pyenv/versions/3.6.10/lib/python3.6/zipfile.py", line 1703, in close
#     self.fp.seek(self.start_dir)
# ValueError: I/O operation on closed file.
```

Following the [zipfile](https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile.close) docs, I call `close()` before extracting the data from `bytes_io`. It looks like `zipp.Path` keeps some other reference to the file object and attempts to write when `make_zip` returns and objects are cleaned up.  Python 3.8 exhibits the same issue.

Comments:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 159e43d..045a3f0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,11 @@
+v3.2.0
+======
+
+#57 and bpo-40564: Mutate the passed ZipFile object
+type instead of making a copy. Prevents issues when
+both the local copy and the caller's copy attempt to
+close the same file handle.
+
 v3.1.0
 ======
 
diff --git a/zipp.py b/zipp.py
index 697d4a9..99e36b8 100644
--- a/zipp.py
+++ b/zipp.py
@@ -109,9 +109,8 @@ class CompleteDirs(zipfile.ZipFile):
         if 'r' not in source.mode:
             cls = CompleteDirs
 
-        res = cls.__new__(cls)
-        vars(res).update(vars(source))
-        return res
+        source.__class__ = cls
+        return source
 
 
 class FastLookup(CompleteDirs):
@@ -211,6 +210,15 @@ class Path:
     __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
 
     def __init__(self, root, at=""):
+        """
+        Construct a Path from a ZipFile or filename.
+
+        Note: When the source is an existing ZipFile object,
+        its type (__class__) will be mutated to a
+        specialized type. If the caller wishes to retain the
+        original type, the caller should either create a
+        separate ZipFile object or pass a filename.
+        """
         self.root = FastLookup.make(root)
         self.at = at
 


Test patch:
diff --git a/test_zipp.py b/test_zipp.py
index a6fbf39..727f928 100644
--- a/test_zipp.py
+++ b/test_zipp.py
@@ -259,3 +259,9 @@ class TestPath(unittest.TestCase):
     def test_implied_dirs_performance(self):
         data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
         zipp.CompleteDirs._implied_dirs(data)
+
+    def test_read_does_not_close(self):
+        for alpharep in self.zipfile_ondisk():
+            with zipfile.ZipFile(alpharep) as file:
+                for rep in range(2):
+                    zipp.Path(file, 'a.txt').read_text()