Skip to content

Commit 4b21d15

Browse files
authored
gh-161106: Use repeat counts in plistlib binary writer struct formats (GH-161107)
Compiled struct formats with one code per item stay in the struct module's cache and take about 33 bytes per item. Use the repeat-count form the reader already uses; the output is unchanged. ANT-2026-1L9EBYNF
1 parent a7c0383 commit 4b21d15

3 files changed

Lines changed: 27 additions & 4 deletions

File tree

‎Lib/plistlib.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ def write(self, value):
699699
top_object = self._getrefnum(value)
700700
offset_table_offset = self._fp.tell()
701701
offset_size = _count_to_size(offset_table_offset)
702-
offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
702+
offset_format = f'>{num_objects}{_BINARY_FORMAT[offset_size]}'
703703
self._fp.write(struct.pack(offset_format, *self._object_offsets))
704704

705705
# Write trailer
@@ -846,7 +846,7 @@ def _write_object(self, value):
846846
refs = [self._getrefnum(o) for o in value]
847847
s = len(refs)
848848
self._write_size(0xA0, s)
849-
self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
849+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *refs))
850850

851851
elif isinstance(value, (dict, frozendict)):
852852
keyRefs, valRefs = [], []
@@ -862,8 +862,8 @@ def _write_object(self, value):
862862

863863
s = len(keyRefs)
864864
self._write_size(0xD0, s)
865-
self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
866-
self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
865+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *keyRefs))
866+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *valRefs))
867867

868868
else:
869869
raise TypeError(value)

‎Lib/test/test_plistlib.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import operator
44
import pickle
55
import struct
6+
import tracemalloc
67
import unittest
78
import plistlib
89
import os
@@ -1037,6 +1038,25 @@ def test_dump_duplicates(self):
10371038
data = plistlib.dumps([x]*1000, fmt=plistlib.FMT_BINARY)
10381039
self.assertLess(len(data), 1100, repr(data))
10391040

1041+
def test_dump_large_container_memory(self):
1042+
# Writing a large container should not leave memory proportional
1043+
# to its size allocated after the call returns.
1044+
if not tracemalloc.is_tracing():
1045+
tracemalloc.start()
1046+
self.addCleanup(tracemalloc.stop)
1047+
n = 20_000
1048+
for make in (lambda n: [True] * n,
1049+
lambda n: dict.fromkeys(map(str, range(n)), True)):
1050+
x = make(n)
1051+
with self.subTest(type=type(x).__name__):
1052+
# Warm up with a slightly smaller container, so that
1053+
# memory kept by free lists is not counted.
1054+
plistlib.dumps(make(n - 1), fmt=plistlib.FMT_BINARY)
1055+
before, _ = tracemalloc.get_traced_memory()
1056+
plistlib.dumps(x, fmt=plistlib.FMT_BINARY)
1057+
after, _ = tracemalloc.get_traced_memory()
1058+
self.assertLess(after - before, n)
1059+
10401060
def test_identity(self):
10411061
for x in (None, False, True, 12345, 123.45, 'abcde', b'abcde',
10421062
datetime.datetime(2004, 10, 26, 10, 33, 33),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`plistlib.dump` and :func:`plistlib.dumps` with
2+
:data:`~plistlib.FMT_BINARY` holding on to memory proportional to the size of
3+
large arrays and dictionaries after the call returns.

0 commit comments

Comments
 (0)