From 38dde6b444a43ffe1148a92e6bb69d6c92c79717 Mon Sep 17 00:00:00 2001 From: Grada Date: Mon, 14 Sep 2026 00:31:44 +0800 Subject: [PATCH] Create missing ZIP archives in append mode --- docs/source/changelog.rst | 2 ++ fsspec/implementations/tests/test_zip.py | 19 +++++++++++++++++++ fsspec/implementations/zip.py | 11 +++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 4a5a8789e..440521582 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,8 @@ Dev Fixes +- Create missing ZIP archives in append mode without truncating existing archives + - Allow filesystem implementations to assign ``protocol`` per instance - Avoid mutating live ``BlockCache`` and ``BackgroundBlockCache`` instances diff --git a/fsspec/implementations/tests/test_zip.py b/fsspec/implementations/tests/test_zip.py index 51166e133..dc9ece316 100644 --- a/fsspec/implementations/tests/test_zip.py +++ b/fsspec/implementations/tests/test_zip.py @@ -124,6 +124,25 @@ def test_zip_glob_star(m): assert len(outfiles) == 1 +@pytest.mark.parametrize("backend", ["memory", "local"]) +def test_append_creates_archive(m, tmp_path, backend): + path = "memory://new.zip" if backend == "memory" else tmp_path / "new.zip" + for name, content in [("first", b"original"), ("second", b"appended")]: + fs = ZipFileSystem(fo=path, mode="a") + try: + fs.pipe_file(name, content) + finally: + fs.close() + + fs = ZipFileSystem(fo=path) + try: + assert fs.cat("first") == b"original" + assert fs.cat("second") == b"appended" + assert fs.find("") == ["first", "second"] + finally: + fs.close() + + def test_append(m, tmpdir): fs = fsspec.filesystem("zip", fo="memory://out.zip", mode="w") with fs.open("afile", "wb") as f: diff --git a/fsspec/implementations/zip.py b/fsspec/implementations/zip.py index 2a0e598d8..824c7212e 100644 --- a/fsspec/implementations/zip.py +++ b/fsspec/implementations/zip.py @@ -32,7 +32,8 @@ def __init__( Parameters ---------- fo: str or file-like - Contains ZIP, and must exist. If a str, will fetch file using + Contains ZIP. In append mode, a missing archive is created. + If a str, will fetch file using :meth:`~fsspec.open_files`, which must return one file exactly. mode: str Accept: "r", "w", "a" @@ -59,7 +60,13 @@ def __init__( ) self.force_zip_64 = allowZip64 self.of = fo - self.fo = fo.__enter__() # the whole instance is a context + try: + self.fo = fo.__enter__() # the whole instance is a context + except FileNotFoundError: + if mode != "a" or not isinstance(fo, fsspec.core.OpenFile): + raise + fo.mode = "w+b" + self.fo = fo.__enter__() self.zip = zipfile.ZipFile( self.fo, mode=mode,