Skip to content
Open
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
36 changes: 22 additions & 14 deletions Lib/plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ def _date_to_string(d, aware_datetime):
d.hour, d.minute, d.second
)

def _dict_items(d, sort_keys, skipkeys):
"""Return the (key, value) pairs of a dict, sorted if needed.

Sorting fails for keys of different types, so non-string keys are
removed or reported before sorting.
"""
items = d.items()
if sort_keys:
if skipkeys:
items = [item for item in items if isinstance(item[0], str)]
items.sort()
else:
for key in d:
if not isinstance(key, str):
raise TypeError("keys must be strings")
items = sorted(items)
return items


def _escape(text):
m = _controlCharPat.search(text)
if m is not None:
Expand Down Expand Up @@ -388,11 +407,7 @@ def write_bytes(self, data):
def write_dict(self, d):
if d:
self.begin_element("dict")
if self._sort_keys:
items = sorted(d.items())
else:
items = d.items()

items = _dict_items(d, self._sort_keys, self._skipkeys)
for key, value in items:
if not isinstance(key, str):
if self._skipkeys:
Expand Down Expand Up @@ -718,10 +733,7 @@ def _flatten(self, value):
if isinstance(value, (dict, frozendict)):
keys = []
values = []
items = value.items()
if self._sort_keys:
items = sorted(items)

items = _dict_items(value, self._sort_keys, self._skipkeys)
for k, v in items:
if not isinstance(k, str):
if self._skipkeys:
Expand Down Expand Up @@ -839,11 +851,7 @@ def _write_object(self, value):
elif isinstance(value, (dict, frozendict)):
keyRefs, valRefs = [], []

if self._sort_keys:
rootItems = sorted(value.items())
else:
rootItems = value.items()

rootItems = _dict_items(value, self._sort_keys, self._skipkeys)
for k, v in rootItems:
if not isinstance(k, str):
if self._skipkeys:
Expand Down
36 changes: 25 additions & 11 deletions Lib/test/test_plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,20 +722,34 @@ def test_skipkeys(self):
'snake': 'aWord',
}

for fmt in ALL_FORMATS:
for sort_keys in (False, True):
with self.subTest(fmt=fmt, sort_keys=sort_keys):
data = plistlib.dumps(
pl, fmt=fmt, skipkeys=True, sort_keys=sort_keys)

pl2 = plistlib.loads(data)
self.assertEqual(pl2, {'snake': 'aWord'})

fp = BytesIO()
plistlib.dump(
pl, fp, fmt=fmt, skipkeys=True, sort_keys=sort_keys)
data = fp.getvalue()
pl2 = plistlib.loads(fp.getvalue())
self.assertEqual(pl2, {'snake': 'aWord'})

def test_skipkeys_with_sort_keys_mixed_types(self):
# gh-145856: skipkeys=True + sort_keys=True with mixed key types
# used to raise TypeError because the sort ran before the filter.
pl = {1: 'a', 'z': 'b', 'a': 'c'}

for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
data = plistlib.dumps(
pl, fmt=fmt, skipkeys=True, sort_keys=False)

pl2 = plistlib.loads(data)
self.assertEqual(pl2, {'snake': 'aWord'})

fp = BytesIO()
plistlib.dump(
pl, fp, fmt=fmt, skipkeys=True, sort_keys=False)
data = fp.getvalue()
pl2 = plistlib.loads(fp.getvalue())
self.assertEqual(pl2, {'snake': 'aWord'})
pl, fmt=fmt, skipkeys=True, sort_keys=True)
pl2 = plistlib.loads(data, dict_type=collections.OrderedDict)
self.assertEqual(dict(pl2), {'z': 'b', 'a': 'c'})
self.assertEqual(list(pl2.keys()), ['a', 'z'])

def test_tuple_members(self):
pl = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :func:`plistlib.dumps` and :func:`plistlib.dump` so that ``skipkeys=True``
together with ``sort_keys=True`` correctly drops non-string keys when the
dictionary contains a mix of string and non-string keys. Previously the sort
ran before the filter and raised :exc:`TypeError`.
Loading