Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Lib/plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ def write(self, value):
top_object = self._getrefnum(value)
offset_table_offset = self._fp.tell()
offset_size = _count_to_size(offset_table_offset)
offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
offset_format = f'>{num_objects}{_BINARY_FORMAT[offset_size]}'
self._fp.write(struct.pack(offset_format, *self._object_offsets))

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

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

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

else:
raise TypeError(value)
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import operator
import pickle
import struct
import tracemalloc
import unittest
import plistlib
import os
Expand Down Expand Up @@ -1037,6 +1038,25 @@ def test_dump_duplicates(self):
data = plistlib.dumps([x]*1000, fmt=plistlib.FMT_BINARY)
self.assertLess(len(data), 1100, repr(data))

def test_dump_large_container_memory(self):
# Writing a large container should not leave memory proportional
# to its size allocated after the call returns.
if not tracemalloc.is_tracing():
tracemalloc.start()
self.addCleanup(tracemalloc.stop)
n = 20_000
for make in (lambda n: [True] * n,
lambda n: dict.fromkeys(map(str, range(n)), True)):
x = make(n)
with self.subTest(type=type(x).__name__):
# Warm up with a slightly smaller container, so that
# memory kept by free lists is not counted.
plistlib.dumps(make(n - 1), fmt=plistlib.FMT_BINARY)
before, _ = tracemalloc.get_traced_memory()
plistlib.dumps(x, fmt=plistlib.FMT_BINARY)
after, _ = tracemalloc.get_traced_memory()
self.assertLess(after - before, n)

def test_identity(self):
for x in (None, False, True, 12345, 123.45, 'abcde', b'abcde',
datetime.datetime(2004, 10, 26, 10, 33, 33),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`plistlib.dump` and :func:`plistlib.dumps` with
:data:`~plistlib.FMT_BINARY` holding on to memory proportional to the size of
large arrays and dictionaries after the call returns.
Loading