Repository navigation
Conversation
CompressedFile.flush() called the compressor's flush() with its default mode, which finishes the compressed stream. WriteToFiles flushes the sink and then closes the file, and close() finishes the stream a second time, so it raised for every compression type: zlib.error for gzip and deflate, ValueError for bzip2 and lzma, and ZstdError for zstd. flush() now writes out buffered data without finishing the stream: a sync flush for gzip and deflate, a block flush for zstd, and nothing for bzip2 and lzma, which cannot flush without finishing. bz2.BZ2File and lzma.LZMAFile behave the same way. close() still finishes the stream.
There was a problem hiding this comment.
📋 Review Summary
This pull request properly resolves a bug where CompressedFile.flush() prematurely finalized the compression stream, causing subsequent write() or close() calls (such as in fileio.WriteToFiles) to fail across all supported compression types. The changes align CompressedFile.flush() behavior with standard library implementations (gzip.GzipFile, bz2.BZ2File, lzma.LZMAFile) and zstandard, and include thorough unit and pipeline tests.
🔍 General Feedback
- The use of
zlib.Z_SYNC_FLUSHfor GZIP/DEFLATE,zstandard.COMPRESSOBJ_FLUSH_BLOCKfor ZSTD, and omitting compressor flushes untilclose()for BZIP2/LZMA is accurate and matches standard library semantics. - The unit tests in
filesystem_test.pyand end-to-end pipeline test infileio_test.pyprovide comprehensive coverage for bothflush()->close()andflush()->write()->close()workflows across compression types.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
|
Assigning reviewers: R: @shunping for label python. This pull request likely touches a core component ("core" label). Please review with scrutiny. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #40966 +/- ##
============================================
- Coverage 58.87% 57.42% -1.45%
+ Complexity 4768 2492 -2276
============================================
Files 1461 1202 -259
Lines 185377 170995 -14382
Branches 3585 2103 -1482
============================================
- Hits 109140 98201 -10939
+ Misses 72892 70145 -2747
+ Partials 3345 2649 -696
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
shunping
left a comment
There was a problem hiding this comment.
LGTM! Please resolve the conflict and we should be good to go.
…le-flush # Conflicts: # CHANGES.md
|
Thanks! I merged master and resolved the conflict in |
Fixes #31863. #35363 hit the same failure with gzip.
fileio.WriteToFilesfails when a sink asks for a compressed file throughcreate_metadata, which is how theFileSinkdocstring says to choose the metadata passed toFileSystems.create:WriteToFilescallssink.flush()and thenwriter.close().TextSink.flush()callsCompressedFile.flush(), which called the compressor'sflush()with its default mode. That mode finishes the compressed stream.close()then finished it a second time, and every compression type raises at that point:CompressedFile.close()zlib.error: Error -2 while flushing: inconsistent stream stateValueError: Repeated call to flush()ZstdError: compressor object already finishedA write after a flush also raises, because the compressor is already finished.
Change
CompressedFile.flush()now writes buffered data to the file without finishing the stream. Onlyclose()finishes it.flush(zlib.Z_SYNC_FLUSH), the same modegzip.GzipFile.flush()uses.flush(zstandard.COMPRESSOBJ_FLUSH_BLOCK).bz2.BZ2File.flush()andlzma.LZMAFile.flush()do not flush the compressor either. The data is written byclose().The underlying file is still flushed in every case.
close()is unchanged.Before and after
A pipeline that writes five lines through
WriteToFileswith the sink above, once per compression type, on the FnApiRunner. Each written file is then decompressed with the standard library orzstandard.Beam 2.77.0 (current master is the same):
With this change:
Tests
filesystem_test.py:test_close_after_flush(write, flush, close) andtest_write_after_flush(write, flush, write, close). Each runs for all five compression types and reads the file back throughCompressedFile.fileio_test.py:test_write_compressed_filesrunsWriteToFileswith a sink that asks for bzip2 and then gzip, and reads the output back withReadMatches. The compression is inferred from the.bz2and.gzsuffixes.With the
filesystem.pychange reverted, all 10CompressedFilesubtests fail. The close-after-flush cases fail inclose()with the errors in the table above, and the write-after-flush cases fail on the secondwrite().test_write_compressed_filesfails withValueError: Repeated call to flush() [while running 'WriteToFiles/...']. With the change, they all pass.Run locally on Python 3.12, macOS, one process:
filesystem_test.py: 41 passed, 10 subtests passed.fileio_test.py,WriteFilesTestandReadTest: 13 passed and 1 skipped on the FnApiRunner. The two streaming tests (test_streaming_complex_timing,test_streaming_different_file_types) passed on the BundleBasedDirectRunner, because they useTestStream.textio_test.py, the compressedTextSinkTestcases: 10 passed.WriteToTextcloses without flushing, so this checks that path is unchanged.yapf --diff(0.43.0),ruff check(0.15.22) andpyrefly check(1.1.1) on the three changed Python files: clean.I did not run the tests on the default Prism runner, on other Python versions, or the full Python suite. That is left to CI.
CHANGES.mdhas a Bugfixes entry.