]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-141510: Support `frozendict` in `plistlib` (#145590)
authorHugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Tue, 31 Mar 2026 12:45:23 +0000 (15:45 +0300)
committerGitHub <noreply@github.com>
Tue, 31 Mar 2026 12:45:23 +0000 (15:45 +0300)
Co-authored-by: Victor Stinner <vstinner@python.org>
Doc/library/plistlib.rst
Doc/whatsnew/3.15.rst
Lib/plistlib.py
Lib/test/test_plistlib.py
Misc/NEWS.d/next/Library/2026-03-21-16-03-16.gh-issue-141510.tKptA7.rst [new file with mode: 0644]

index fa15cd4267eef4928da56dd0bdab14dd4170ab72..72140e41675c3507f81da00531f85ce3560ea721 100644 (file)
@@ -18,7 +18,7 @@ and XML plist files.
 
 The property list (``.plist``) file format is a simple serialization supporting
 basic object types, like dictionaries, lists, numbers and strings.  Usually the
-top level object is a dictionary.
+top level object is a dictionary or a frozen dictionary.
 
 To write out and to parse a plist file, use the :func:`dump` and
 :func:`load` functions.
index 462482c80122eaf144227450a7ec7453888b3f97..97937892de3a6e2935473404e4b93db4e6081ffc 100644 (file)
@@ -217,7 +217,8 @@ For example::
 
 The following standard library modules have been updated to accept
 :class:`!frozendict`: :mod:`copy`, :mod:`decimal`, :mod:`json`, :mod:`marshal`,
-:mod:`pickle`, :mod:`pprint` and :mod:`xml.etree.ElementTree`.
+:mod:`plistlib` (only for serialization), :mod:`pickle`, :mod:`pprint` and
+:mod:`xml.etree.ElementTree`.
 
 :func:`eval` and :func:`exec` accept :class:`!frozendict` for *globals*, and
 :func:`type` and :meth:`str.maketrans` accept :class:`!frozendict` for *dict*.
index 01c7aa96261abecd1683c5adf333006638255192..93f3ef5e38af84329e6573432667f582a8c3dfa2 100644 (file)
@@ -2,7 +2,7 @@ r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
 
 The property list (.plist) file format is a simple XML pickle supporting
 basic object types, like dictionaries, lists, numbers and strings.
-Usually the top level object is a dictionary.
+Usually the top level object is a dictionary or a frozen dictionary.
 
 To write out a plist file, use the dump(value, file)
 function. 'value' is the top level object, 'file' is
@@ -357,7 +357,7 @@ class _PlistWriter(_DumbXMLWriter):
         elif isinstance(value, float):
             self.simple_element("real", repr(value))
 
-        elif isinstance(value, dict):
+        elif isinstance(value, (dict, frozendict)):
             self.write_dict(value)
 
         elif isinstance(value, (bytes, bytearray)):
@@ -715,7 +715,7 @@ class _BinaryPlistWriter (object):
             self._objidtable[id(value)] = refnum
 
         # And finally recurse into containers
-        if isinstance(value, dict):
+        if isinstance(value, (dict, frozendict)):
             keys = []
             values = []
             items = value.items()
@@ -836,7 +836,7 @@ class _BinaryPlistWriter (object):
             self._write_size(0xA0, s)
             self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
 
-        elif isinstance(value, dict):
+        elif isinstance(value, (dict, frozendict)):
             keyRefs, valRefs = [], []
 
             if self._sort_keys:
@@ -869,18 +869,18 @@ def _is_fmt_binary(header):
 # Generic bits
 #
 
-_FORMATS={
-    FMT_XML: dict(
+_FORMATS=frozendict({
+    FMT_XML: frozendict(
         detect=_is_fmt_xml,
         parser=_PlistParser,
         writer=_PlistWriter,
     ),
-    FMT_BINARY: dict(
+    FMT_BINARY: frozendict(
         detect=_is_fmt_binary,
         parser=_BinaryPlistParser,
         writer=_BinaryPlistWriter,
     )
-}
+})
 
 
 def load(fp, *, fmt=None, dict_type=dict, aware_datetime=False):
index d9216be4d956588146ea036bf8d61734e2c9073b..b9c261310bb5670a165d3a3257f3834974e99550 100644 (file)
@@ -792,6 +792,25 @@ class TestPlistlib(unittest.TestCase):
                 })
                 self.assertIsNot(pl2['first'], pl2['second'])
 
+    def test_frozendict(self):
+        pl = frozendict(
+            aString="Doodah",
+            anInt=728,
+            aDict=frozendict(
+                anotherString="hello",
+                aTrueValue=True,
+            ),
+            aList=["A", "B", 12],
+        )
+
+        for fmt in ALL_FORMATS:
+            with self.subTest(fmt=fmt):
+                data = plistlib.dumps(pl, fmt=fmt)
+                pl2 = plistlib.loads(data)
+                self.assertEqual(pl2, dict(pl))
+                self.assertIsInstance(pl2, dict)
+                self.assertIsInstance(pl2['aDict'], dict)
+
     def test_controlcharacters(self):
         for i in range(128):
             c = chr(i)
diff --git a/Misc/NEWS.d/next/Library/2026-03-21-16-03-16.gh-issue-141510.tKptA7.rst b/Misc/NEWS.d/next/Library/2026-03-21-16-03-16.gh-issue-141510.tKptA7.rst
new file mode 100644 (file)
index 0000000..19c30f1
--- /dev/null
@@ -0,0 +1,2 @@
+Support :class:`frozendict` in :mod:`plistlib`, for serialization only.
+Patch by Hugo van Kemenade.