From 6bc39ae06a9003cb8c99f24420c487c9f1a8f822 Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Mon, 5 Oct 2026 15:23:07 +0100 Subject: [PATCH] gh-158858: Stop re-copying the tarfile stream buffer for every block Co-Authored-By: Claude Opus 5.5 --- Lib/tarfile.py | 10 +++++++--- Lib/test/test_tarfile.py | 20 +++++++++++++++++++ ...-10-05-16-05-00.gh-issue-158858.Tb4rWq.rst | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-05-16-05-00.gh-issue-158858.Tb4rWq.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index a4f9ce3311f6dad..d3fe3341d751ab9 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -478,9 +478,13 @@ def __write(self, s): is ready to be written. """ self.buf += s - while len(self.buf) > self.bufsize: - self.fileobj.write(self.buf[:self.bufsize]) - self.buf = self.buf[self.bufsize:] + if len(self.buf) > self.bufsize: + # Write whole blocks from buf and keep the rest. Slicing off + # each block would copy the whole remaining buffer every time. + end = (len(self.buf) - 1) // self.bufsize * self.bufsize + for i in range(0, end, self.bufsize): + self.fileobj.write(self.buf[i:i + self.bufsize]) + self.buf = self.buf[end:] def close(self): """Close the _Stream object. No operation should be diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 55f052d0504d67b..b3898d38b5c83dc 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -2977,6 +2977,26 @@ def test_pax_limits(self): class MiscTest(unittest.TestCase): + def test_stream_write_large_chunks(self): + # A chunk spanning many blocks is still written one block at a time. + data = bytes(range(256)) * 1024 + writes = [] + class Recorder(io.BytesIO): + def write(self, b): + writes.append(b) + return super().write(b) + fobj = Recorder() + with tarfile.open(fileobj=fobj, mode='w|', + copybufsize=len(data)) as tar: + tarinfo = tarfile.TarInfo('data') + tarinfo.size = len(data) + tar.addfile(tarinfo, io.BytesIO(data)) + self.assertEqual({(type(b), len(b)) for b in writes}, + {(bytes, tarfile.RECORDSIZE)}) + fobj.seek(0) + with tarfile.open(fileobj=fobj, mode='r|') as tar: + self.assertEqual(tar.extractfile(tar.next()).read(), data) + def test_char_fields(self): self.assertEqual(tarfile.stn("foo", 8, "ascii", "strict"), b"foo\0\0\0\0\0") diff --git a/Misc/NEWS.d/next/Library/2026-10-05-16-05-00.gh-issue-158858.Tb4rWq.rst b/Misc/NEWS.d/next/Library/2026-10-05-16-05-00.gh-issue-158858.Tb4rWq.rst new file mode 100644 index 000000000000000..85d013aa39d8508 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-05-16-05-00.gh-issue-158858.Tb4rWq.rst @@ -0,0 +1,2 @@ +Speed up writing :mod:`tarfile` streams (``w|`` modes) with a large +*copybufsize*: each chunk is no longer re-copied once per output block.