Skip to content

Commit 9be8728

Browse files
committed
gh-145856: Fix plistlib.dumps() skipkeys behavior with mixed key types
When both skipkeys=True and sort_keys=True were passed to plistlib.dump()/dumps(), the sort ran before the non-string keys were filtered out, raising TypeError on dictionaries with mixed key types. Filter non-string keys before sorting in all three write_dict sites (XML, binary pass 1, binary pass 2).
1 parent ec9ce3e commit 9be8728

3 files changed

Lines changed: 40 additions & 23 deletions

File tree

Lib/plistlib.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -388,15 +388,14 @@ def write_bytes(self, data):
388388
def write_dict(self, d):
389389
if d:
390390
self.begin_element("dict")
391+
items = d.items()
392+
if self._skipkeys:
393+
items = [(k, v) for k, v in items if isinstance(k, str)]
391394
if self._sort_keys:
392-
items = sorted(d.items())
393-
else:
394-
items = d.items()
395+
items = sorted(items)
395396

396397
for key, value in items:
397398
if not isinstance(key, str):
398-
if self._skipkeys:
399-
continue
400399
raise TypeError("keys must be strings")
401400
self.simple_element("key", key)
402401
self.write_value(value)
@@ -719,13 +718,13 @@ def _flatten(self, value):
719718
keys = []
720719
values = []
721720
items = value.items()
721+
if self._skipkeys:
722+
items = [(k, v) for k, v in items if isinstance(k, str)]
722723
if self._sort_keys:
723724
items = sorted(items)
724725

725726
for k, v in items:
726727
if not isinstance(k, str):
727-
if self._skipkeys:
728-
continue
729728
raise TypeError("keys must be strings")
730729
keys.append(k)
731730
values.append(v)
@@ -839,15 +838,15 @@ def _write_object(self, value):
839838
elif isinstance(value, (dict, frozendict)):
840839
keyRefs, valRefs = [], []
841840

841+
rootItems = value.items()
842+
if self._skipkeys:
843+
rootItems = [(k, v) for k, v in rootItems
844+
if isinstance(k, str)]
842845
if self._sort_keys:
843-
rootItems = sorted(value.items())
844-
else:
845-
rootItems = value.items()
846+
rootItems = sorted(rootItems)
846847

847848
for k, v in rootItems:
848849
if not isinstance(k, str):
849-
if self._skipkeys:
850-
continue
851850
raise TypeError("keys must be strings")
852851
keyRefs.append(self._getrefnum(k))
853852
valRefs.append(self._getrefnum(v))

Lib/test/test_plistlib.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -722,20 +722,34 @@ def test_skipkeys(self):
722722
'snake': 'aWord',
723723
}
724724

725+
for fmt in ALL_FORMATS:
726+
for sort_keys in (False, True):
727+
with self.subTest(fmt=fmt, sort_keys=sort_keys):
728+
data = plistlib.dumps(
729+
pl, fmt=fmt, skipkeys=True, sort_keys=sort_keys)
730+
731+
pl2 = plistlib.loads(data)
732+
self.assertEqual(pl2, {'snake': 'aWord'})
733+
734+
fp = BytesIO()
735+
plistlib.dump(
736+
pl, fp, fmt=fmt, skipkeys=True, sort_keys=sort_keys)
737+
data = fp.getvalue()
738+
pl2 = plistlib.loads(fp.getvalue())
739+
self.assertEqual(pl2, {'snake': 'aWord'})
740+
741+
def test_skipkeys_with_sort_keys_mixed_types(self):
742+
# gh-145856: skipkeys=True + sort_keys=True with mixed key types
743+
# used to raise TypeError because the sort ran before the filter.
744+
pl = {1: 'a', 'z': 'b', 'a': 'c'}
745+
725746
for fmt in ALL_FORMATS:
726747
with self.subTest(fmt=fmt):
727748
data = plistlib.dumps(
728-
pl, fmt=fmt, skipkeys=True, sort_keys=False)
729-
730-
pl2 = plistlib.loads(data)
731-
self.assertEqual(pl2, {'snake': 'aWord'})
732-
733-
fp = BytesIO()
734-
plistlib.dump(
735-
pl, fp, fmt=fmt, skipkeys=True, sort_keys=False)
736-
data = fp.getvalue()
737-
pl2 = plistlib.loads(fp.getvalue())
738-
self.assertEqual(pl2, {'snake': 'aWord'})
749+
pl, fmt=fmt, skipkeys=True, sort_keys=True)
750+
pl2 = plistlib.loads(data, dict_type=collections.OrderedDict)
751+
self.assertEqual(dict(pl2), {'z': 'b', 'a': 'c'})
752+
self.assertEqual(list(pl2.keys()), ['a', 'z'])
739753

740754
def test_tuple_members(self):
741755
pl = {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`plistlib.dumps` and :func:`plistlib.dump` so that ``skipkeys=True``
2+
together with ``sort_keys=True`` correctly drops non-string keys when the
3+
dictionary contains a mix of string and non-string keys. Previously the sort
4+
ran before the filter and raised :exc:`TypeError`.

0 commit comments

Comments
 (0)