From 033ca060edfc5d55b9251cc1b4cb905bec3ef035 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:16:40 -0700 Subject: [PATCH 1/2] Fix transactions with compressed files Compressed wrappers do not implement commit or discard, so tracking the wrapper caused transaction exit to raise AttributeError. Track the raw filesystem file before applying compression so it can be committed or discarded after the wrapper flushes. Add a local filesystem regression test for compressed text writes. Fixes #1584 --- fsspec/implementations/tests/test_local.py | 13 +++++++++++++ fsspec/spec.py | 5 ++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/fsspec/implementations/tests/test_local.py b/fsspec/implementations/tests/test_local.py index d74b168d9..52d11a980 100644 --- a/fsspec/implementations/tests/test_local.py +++ b/fsspec/implementations/tests/test_local.py @@ -509,6 +509,19 @@ def test_commit_discard(tmpdir): assert not fs.exists(tmpdir + "/bfile") +def test_transaction_with_compression(tmpdir): + path = str(tmpdir / "afile.gz") + fs = LocalFileSystem() + + with fs.transaction: + with fs.open(path, "wt", compression="gzip") as f: + f.write("data") + assert not fs.exists(path) + + with gzip.open(path, "rt") as f: + assert f.read() == "data" + + def test_same_permissions_with_and_without_transaction(tmpdir): tmpdir = str(tmpdir) diff --git a/fsspec/spec.py b/fsspec/spec.py index a390bf060..429b73cd9 100644 --- a/fsspec/spec.py +++ b/fsspec/spec.py @@ -1363,6 +1363,8 @@ def open( cache_options=cache_options, **kwargs, ) + if not ac and "r" not in mode: + self.transaction.files.append(f) if compression is not None: from fsspec.compression import compr from fsspec.core import get_compression @@ -1370,9 +1372,6 @@ def open( compression = get_compression(path, compression) compress = compr[compression] f = compress(f, mode=mode[0]) - - if not ac and "r" not in mode: - self.transaction.files.append(f) return f def touch(self, path, truncate=True, **kwargs): From 4f1862308c0d5a7174b02b2a855a81dd7c142148 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:00:39 -0700 Subject: [PATCH 2/2] Close local temp file before commit in transactions A compression wrapper such as GzipFile does not close the underlying file object it wraps, so on transaction commit the temp file could still hold buffered bytes and a pending gzip trailer. On Linux/macOS the subsequent rename happened to work and the data was flushed on interpreter cleanup, but on Windows the move ran against a still-open handle and produced a truncated archive (EOFError: Compressed file ended before the end-of-stream marker was reached). Close the temp file, if still open, before moving it into place. --- fsspec/implementations/local.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fsspec/implementations/local.py b/fsspec/implementations/local.py index b549b93e2..8b180c95c 100644 --- a/fsspec/implementations/local.py +++ b/fsspec/implementations/local.py @@ -437,6 +437,11 @@ def __getstate__(self): def commit(self): if self.autocommit: raise RuntimeError("Can only commit if not already set to autocommit") + if not self.f.closed: + # a compression wrapper (e.g., GzipFile) does not close the file + # object it was given, so buffered bytes and any trailer may still + # be pending here. Windows also refuses to rename an open file. + self.f.close() try: shutil.move(self.temp, self.path) except PermissionError as e: