Skip to content

Commit fe0d239

Browse files
Stream package extraction in 10MB chunks to limit peak memory usage (#410)
Fixes #409 10MB was chosen so that files in a standard Python distribution are all still copied in a single step. However, custom packages with larger files are now streamed.
1 parent d14e65d commit fe0d239

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

src/manage/install_command.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def validate_package(install, dest, *, delete=True):
166166

167167

168168
def extract_package(package, prefix, calculate_dest=Path, *, on_progress=None, repair=False):
169+
import shutil
169170
import zipfile
170171

171172
LOGGER.debug("Starting extract of %s to %s", package, prefix)
@@ -205,8 +206,8 @@ def _calc(prefix, filename, calculate_dest=calculate_dest):
205206
warn_overwrite.append(dest)
206207
continue
207208
ensure_tree(dest)
208-
with open(dest, "wb") as f:
209-
f.write(zf.read(member))
209+
with zf.open(member) as source, open(dest, "wb") as f:
210+
shutil.copyfileobj(source, f, length=10 * 1024 * 1024)
210211
on_progress(100)
211212

212213
if warn_out_of_prefix:

tests/test_install_command.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,51 @@
1010
from manage.logging import LOGGER
1111

1212

13+
@pytest.mark.parametrize("suffix", [".zip", ".nupkg"])
14+
@pytest.mark.parametrize("repair", [False, True])
15+
def test_extract_package_streaming(tmp_path, monkeypatch, suffix, repair):
16+
"""Extract in bounded reads while preserving overwrite and repair behavior."""
17+
import zipfile
18+
19+
package = tmp_path / ("package" + suffix)
20+
prefix = tmp_path / "install"
21+
prefix.mkdir()
22+
existing = prefix / "existing.txt"
23+
existing.write_bytes(b"original")
24+
chunk_size = 10 * 1024 * 1024
25+
data = bytes(range(256)) * (chunk_size // 256 + 1)
26+
archive_prefix = "tools/" if suffix == ".nupkg" else ""
27+
with zipfile.ZipFile(package, "w", zipfile.ZIP_DEFLATED) as zf:
28+
zf.writestr(archive_prefix + "nested/data.bin", data)
29+
zf.writestr(archive_prefix + "empty.txt", b"")
30+
zf.writestr(archive_prefix + "existing.txt", b"replacement")
31+
if suffix == ".nupkg":
32+
zf.writestr("metadata.txt", b"ignored")
33+
34+
reads = []
35+
original_read = zipfile.ZipExtFile.read
36+
37+
def bounded_read(self, n=-1):
38+
assert n == chunk_size
39+
result = original_read(self, n)
40+
if self.name == archive_prefix + "nested/data.bin":
41+
reads.append(len(result))
42+
return result
43+
44+
monkeypatch.setattr(zipfile.ZipExtFile, "read", bounded_read)
45+
progress = []
46+
IC.extract_package(package, prefix, calculate_dest=Path,
47+
on_progress=progress.append, repair=repair)
48+
assert (prefix / "nested/data.bin").read_bytes() == data
49+
assert (prefix / "empty.txt").read_bytes() == b""
50+
assert existing.read_bytes() == (b"replacement" if repair else b"original")
51+
assert not (prefix / "metadata.txt").exists()
52+
assert reads == [chunk_size, 256, 0]
53+
assert progress[0] == 0
54+
assert 100 in progress
55+
assert (None in progress) == (not repair)
56+
57+
1358
def test_print_cli_shortcuts(patched_installs, assert_log, monkeypatch, tmp_path):
1459
class Cmd:
1560
scratch = {}

0 commit comments

Comments
 (0)