https://github.com/python/cpython/commit/7f7ff4c99d1f53424be8c323c50f3623c3488ba1 commit: 7f7ff4c99d1f53424be8c323c50f3623c3488ba1 branch: main author: ankhikarmakar <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-09-18T10:56:30+03:00 summary:
gh-145856: Fix plistlib.dump() with skipkeys=True and sort_keys=True (GH-150109) A dict with non-string keys raised TypeError, because the keys were sorted before skipping non-string keys. Co-authored-by: VanshAgarwal24036 <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> files: A Misc/NEWS.d/next/Library/2026-05-20-14-00-00.gh-issue-145856.Pl5kZx.rst M Lib/plistlib.py M Lib/test/test_plistlib.py diff --git a/Lib/plistlib.py b/Lib/plistlib.py index 93f3ef5e38af84..822fbcf17b16d0 100644 --- a/Lib/plistlib.py +++ b/Lib/plistlib.py @@ -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: @@ -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: @@ -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: @@ -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: diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py index b9c261310bb567..fd6036c033ffb8 100644 --- a/Lib/test/test_plistlib.py +++ b/Lib/test/test_plistlib.py @@ -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 = { diff --git a/Misc/NEWS.d/next/Library/2026-05-20-14-00-00.gh-issue-145856.Pl5kZx.rst b/Misc/NEWS.d/next/Library/2026-05-20-14-00-00.gh-issue-145856.Pl5kZx.rst new file mode 100644 index 00000000000000..9f8cc32fd81c88 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-20-14-00-00.gh-issue-145856.Pl5kZx.rst @@ -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`. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
