Skip to content

Commit 3630594

Browse files
authored
[3.14] gh-161106: Use repeat counts in plistlib binary writer struct formats (GH-161107) (#161137)
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 (cherry picked from commit 4b21d15)
1 parent 98a26e9 commit 3630594

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
@@ -706,7 +706,7 @@ def write(self, value):
706706
top_object = self._getrefnum(value)
707707
offset_table_offset = self._fp.tell()
708708
offset_size = _count_to_size(offset_table_offset)
709-
offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
709+
offset_format = f'>{num_objects}{_BINARY_FORMAT[offset_size]}'
710710
self._fp.write(struct.pack(offset_format, *self._object_offsets))
711711

712712
# Write trailer
@@ -853,7 +853,7 @@ def _write_object(self, value):
853853
refs = [self._getrefnum(o) for o in value]
854854
s = len(refs)
855855
self._write_size(0xA0, s)
856-
self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
856+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *refs))
857857

858858
elif isinstance(value, dict):
859859
keyRefs, valRefs = [], []
@@ -869,8 +869,8 @@ def _write_object(self, value):
869869

870870
s = len(keyRefs)
871871
self._write_size(0xD0, s)
872-
self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
873-
self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
872+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *keyRefs))
873+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *valRefs))
874874

875875
else:
876876
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
@@ -1018,6 +1019,25 @@ def test_dump_duplicates(self):
10181019
data = plistlib.dumps([x]*1000, fmt=plistlib.FMT_BINARY)
10191020
self.assertLess(len(data), 1100, repr(data))
10201021

1022+
def test_dump_large_container_memory(self):
1023+
# Writing a large container should not leave memory proportional
1024+
# to its size allocated after the call returns.
1025+
if not tracemalloc.is_tracing():
1026+
tracemalloc.start()
1027+
self.addCleanup(tracemalloc.stop)
1028+
n = 20_000
1029+
for make in (lambda n: [True] * n,
1030+
lambda n: dict.fromkeys(map(str, range(n)), True)):
1031+
x = make(n)
1032+
with self.subTest(type=type(x).__name__):
1033+
# Warm up with a slightly smaller container, so that
1034+
# memory kept by free lists is not counted.
1035+
plistlib.dumps(make(n - 1), fmt=plistlib.FMT_BINARY)
1036+
before, _ = tracemalloc.get_traced_memory()
1037+
plistlib.dumps(x, fmt=plistlib.FMT_BINARY)
1038+
after, _ = tracemalloc.get_traced_memory()
1039+
self.assertLess(after - before, n)
1040+
10211041
def test_identity(self):
10221042
for x in (None, False, True, 12345, 123.45, 'abcde', b'abcde',
10231043
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)