Issue #14455: plistlib now supports binary plists and has an updated API.

This patch adds support for binary plists on OSX to plistlib (based
on a patch by 'dpounces').

The patch also cleans up the API for the plistlib module.
This commit is contained in:
Ronald Oussoren 2013-11-21 15:46:49 +01:00
parent 8455723cfb
commit c5cf797342
5 changed files with 1446 additions and 426 deletions

View File

@ -16,26 +16,21 @@
-------------- --------------
This module provides an interface for reading and writing the "property list" This module provides an interface for reading and writing the "property list"
XML files used mainly by Mac OS X. files used mainly by Mac OS X and supports both binary and XML plist files.
The property list (``.plist``) file format is a simple XML pickle supporting The property list (``.plist``) file format is a simple serialization supporting
basic object types, like dictionaries, lists, numbers and strings. Usually the basic object types, like dictionaries, lists, numbers and strings. Usually the
top level object is a dictionary. top level object is a dictionary.
To write out and to parse a plist file, use the :func:`writePlist` and To write out and to parse a plist file, use the :func:`dump` and
:func:`readPlist` functions. :func:`load` functions.
To work with plist data in bytes objects, use :func:`writePlistToBytes` To work with plist data in bytes objects, use :func:`dumps`
and :func:`readPlistFromBytes`. and :func:`loads`.
Values can be strings, integers, floats, booleans, tuples, lists, dictionaries Values can be strings, integers, floats, booleans, tuples, lists, dictionaries
(but only with string keys), :class:`Data` or :class:`datetime.datetime` (but only with string keys), :class:`Data`, :class:`bytes`, :class:`bytesarray`
objects. String values (including dictionary keys) have to be unicode strings -- or :class:`datetime.datetime` objects.
they will be written out as UTF-8.
The ``<data>`` plist type is supported through the :class:`Data` class. This is
a thin wrapper around a Python bytes object. Use :class:`Data` if your strings
contain control characters.
.. seealso:: .. seealso::
@ -45,37 +40,145 @@ contain control characters.
This module defines the following functions: This module defines the following functions:
.. function:: readPlist(pathOrFile) .. function:: load(fp, \*, fmt=None, use_builtin_types=True, dict_type=dict)
Read a plist file. *pathOrFile* may either be a file name or a (readable and Read a plist file. *fp* should be a readable and binary file object.
binary) file object. Return the unpacked root object (which usually is a Return the unpacked root object (which usually is a
dictionary). dictionary).
The XML data is parsed using the Expat parser from :mod:`xml.parsers.expat` The *fmt* is the format of the file and the following values are valid:
-- see its documentation for possible exceptions on ill-formed XML.
Unknown elements will simply be ignored by the plist parser. * :data:`None`: Autodetect the file format
* :data:`FMT_XML`: XML file format
* :data:`FMT_BINARY`: Binary plist format
If *use_builtin_types* is True (the default) binary data will be returned
as instances of :class:`bytes`, otherwise it is returned as instances of
:class:`Data`.
The *dict_type* is the type used for dictionaries that are read from the
plist file. The exact structure of the plist can be recovered by using
:class:`collections.OrderedDict` (although the order of keys shouldn't be
important in plist files).
XML data for the :data:`FMT_XML` format is parsed using the Expat parser
from :mod:`xml.parsers.expat` -- see its documentation for possible
exceptions on ill-formed XML. Unknown elements will simply be ignored
by the plist parser.
The parser for the binary format raises :exc:`InvalidFileException`
when the file cannot be parsed.
.. versionadded:: 3.4
.. function:: loads(data, \*, fmt=None, use_builtin_types=True, dict_type=dict)
Load a plist from a bytes object. See :func:`load` for an explanation of
the keyword arguments.
.. function:: dump(value, fp, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False)
Write *value* to a plist file. *Fp* should be a writable, binary
file object.
The *fmt* argument specifies the format of the plist file and can be
one of the following values:
* :data:`FMT_XML`: XML formatted plist file
* :data:`FMT_BINARY`: Binary formatted plist file
When *sort_keys* is true (the default) the keys for dictionaries will be
written to the plist in sorted order, otherwise they will be written in
the iteration order of the dictionary.
When *skipkeys* is false (the default) the function raises :exc:`TypeError`
when a key of a dictionary is not a string, otherwise such keys are skipped.
A :exc:`TypeError` will be raised if the object is of an unsupported type or
a container that contains objects of unsupported types.
.. versionchanged:: 3.4
Added the *fmt*, *sort_keys* and *skipkeys* arguments.
.. function:: dumps(value, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False)
Return *value* as a plist-formatted bytes object. See
the documentation for :func:`dump` for an explanation of the keyword
arguments of this function.
The following functions are deprecated:
.. function:: readPlist(pathOrFile)
Read a plist file. *pathOrFile* may be either a file name or a (readable
and binary) file object. Returns the unpacked root object (which usually
is a dictionary).
This function calls :func:`load` to do the actual work, the the documentation
of :func:`that function <load>` for an explanation of the keyword arguments.
.. note::
Dict values in the result have a ``__getattr__`` method that defers
to ``__getitem_``. This means that you can use attribute access to
access items of these dictionaries.
.. deprecated: 3.4 Use :func:`load` instead.
.. function:: writePlist(rootObject, pathOrFile) .. function:: writePlist(rootObject, pathOrFile)
Write *rootObject* to a plist file. *pathOrFile* may either be a file name Write *rootObject* to an XML plist file. *pathOrFile* may be either a file name
or a (writable and binary) file object. or a (writable and binary) file object
A :exc:`TypeError` will be raised if the object is of an unsupported type or .. deprecated: 3.4 Use :func:`dump` instead.
a container that contains objects of unsupported types.
.. function:: readPlistFromBytes(data) .. function:: readPlistFromBytes(data)
Read a plist data from a bytes object. Return the root object. Read a plist data from a bytes object. Return the root object.
See :func:`load` for a description of the keyword arguments.
.. note::
Dict values in the result have a ``__getattr__`` method that defers
to ``__getitem_``. This means that you can use attribute access to
access items of these dictionaries.
.. deprecated:: 3.4 Use :func:`loads` instead.
.. function:: writePlistToBytes(rootObject) .. function:: writePlistToBytes(rootObject)
Return *rootObject* as a plist-formatted bytes object. Return *rootObject* as an XML plist-formatted bytes object.
.. deprecated:: 3.4 Use :func:`dumps` instead.
.. versionchanged:: 3.4
Added the *fmt*, *sort_keys* and *skipkeys* arguments.
The following class is available: The following classes are available:
.. class:: Dict([dict]):
Return an extended mapping object with the same value as dictionary
*dict*.
This class is a subclass of :class:`dict` where attribute access can
be used to access items. That is, ``aDict.key`` is the same as
``aDict['key']`` for getting, setting and deleting items in the mapping.
.. deprecated:: 3.0
.. class:: Data(data) .. class:: Data(data)
@ -86,6 +189,24 @@ The following class is available:
It has one attribute, :attr:`data`, that can be used to retrieve the Python It has one attribute, :attr:`data`, that can be used to retrieve the Python
bytes object stored in it. bytes object stored in it.
.. deprecated:: 3.4 Use a :class:`bytes` object instead
The following constants are avaiable:
.. data:: FMT_XML
The XML format for plist files.
.. versionadded:: 3.4
.. data:: FMT_BINARY
The binary format for plist files
.. versionadded:: 3.4
Examples Examples
-------- --------
@ -103,13 +224,15 @@ Generating a plist::
aTrueValue = True, aTrueValue = True,
aFalseValue = False, aFalseValue = False,
), ),
someData = Data(b"<binary gunk>"), someData = b"<binary gunk>",
someMoreData = Data(b"<lots of binary gunk>" * 10), someMoreData = b"<lots of binary gunk>" * 10,
aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())), aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
) )
writePlist(pl, fileName) with open(fileName, 'wb') as fp:
dump(pl, fp)
Parsing a plist:: Parsing a plist::
pl = readPlist(pathOrFile) with open(fileName, 'rb') as fp:
pl = load(fp)
print(pl["aKey"]) print(pl["aKey"])

File diff suppressed because it is too large Load Diff

View File

@ -1,94 +1,87 @@
# Copyright (C) 2003 Python Software Foundation # Copyright (C) 2003-2013 Python Software Foundation
import unittest import unittest
import plistlib import plistlib
import os import os
import datetime import datetime
import codecs
import binascii
import collections
from test import support from test import support
from io import BytesIO
ALL_FORMATS=(plistlib.FMT_XML, plistlib.FMT_BINARY)
# This test data was generated through Cocoa's NSDictionary class # The testdata is generated using Mac/Tools/plistlib_generate_testdata.py
TESTDATA = b"""<?xml version="1.0" encoding="UTF-8"?> # (which using PyObjC to control the Cocoa classes for generating plists)
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \ TESTDATA={
"http://www.apple.com/DTDs/PropertyList-1.0.dtd"> plistlib.FMT_XML: binascii.a2b_base64(b'''
<plist version="1.0"> PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU
<dict> WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO
<key>aDate</key> IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w
<date>2004-10-26T10:33:33Z</date> LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+YURh
<key>aDict</key> dGU8L2tleT4KCTxkYXRlPjIwMDQtMTAtMjZUMTA6MzM6MzNaPC9kYXRlPgoJ
<dict> PGtleT5hRGljdDwva2V5PgoJPGRpY3Q+CgkJPGtleT5hRmFsc2VWYWx1ZTwv
<key>aFalseValue</key> a2V5PgoJCTxmYWxzZS8+CgkJPGtleT5hVHJ1ZVZhbHVlPC9rZXk+CgkJPHRy
<false/> dWUvPgoJCTxrZXk+YVVuaWNvZGVWYWx1ZTwva2V5PgoJCTxzdHJpbmc+TcOk
<key>aTrueValue</key> c3NpZywgTWHDnzwvc3RyaW5nPgoJCTxrZXk+YW5vdGhlclN0cmluZzwva2V5
<true/> PgoJCTxzdHJpbmc+Jmx0O2hlbGxvICZhbXA7ICdoaScgdGhlcmUhJmd0Ozwv
<key>aUnicodeValue</key> c3RyaW5nPgoJCTxrZXk+ZGVlcGVyRGljdDwva2V5PgoJCTxkaWN0PgoJCQk8
<string>M\xc3\xa4ssig, Ma\xc3\x9f</string> a2V5PmE8L2tleT4KCQkJPGludGVnZXI+MTc8L2ludGVnZXI+CgkJCTxrZXk+
<key>anotherString</key> Yjwva2V5PgoJCQk8cmVhbD4zMi41PC9yZWFsPgoJCQk8a2V5PmM8L2tleT4K
<string>&lt;hello &amp; 'hi' there!&gt;</string> CQkJPGFycmF5PgoJCQkJPGludGVnZXI+MTwvaW50ZWdlcj4KCQkJCTxpbnRl
<key>deeperDict</key> Z2VyPjI8L2ludGVnZXI+CgkJCQk8c3RyaW5nPnRleHQ8L3N0cmluZz4KCQkJ
<dict> PC9hcnJheT4KCQk8L2RpY3Q+Cgk8L2RpY3Q+Cgk8a2V5PmFGbG9hdDwva2V5
<key>a</key> PgoJPHJlYWw+MC41PC9yZWFsPgoJPGtleT5hTGlzdDwva2V5PgoJPGFycmF5
<integer>17</integer> PgoJCTxzdHJpbmc+QTwvc3RyaW5nPgoJCTxzdHJpbmc+Qjwvc3RyaW5nPgoJ
<key>b</key> CTxpbnRlZ2VyPjEyPC9pbnRlZ2VyPgoJCTxyZWFsPjMyLjU8L3JlYWw+CgkJ
<real>32.5</real> PGFycmF5PgoJCQk8aW50ZWdlcj4xPC9pbnRlZ2VyPgoJCQk8aW50ZWdlcj4y
<key>c</key> PC9pbnRlZ2VyPgoJCQk8aW50ZWdlcj4zPC9pbnRlZ2VyPgoJCTwvYXJyYXk+
<array> Cgk8L2FycmF5PgoJPGtleT5hU3RyaW5nPC9rZXk+Cgk8c3RyaW5nPkRvb2Rh
<integer>1</integer> aDwvc3RyaW5nPgoJPGtleT5hbkVtcHR5RGljdDwva2V5PgoJPGRpY3QvPgoJ
<integer>2</integer> PGtleT5hbkVtcHR5TGlzdDwva2V5PgoJPGFycmF5Lz4KCTxrZXk+YW5JbnQ8
<string>text</string> L2tleT4KCTxpbnRlZ2VyPjcyODwvaW50ZWdlcj4KCTxrZXk+bmVzdGVkRGF0
</array> YTwva2V5PgoJPGFycmF5PgoJCTxkYXRhPgoJCVBHeHZkSE1nYjJZZ1ltbHVZ
</dict> WEo1SUdkMWJtcytBQUVDQXp4c2IzUnpJRzltSUdKcGJtRnllU0JuZFc1cgoJ
</dict> CVBnQUJBZ004Ykc5MGN5QnZaaUJpYVc1aGNua2daM1Z1YXo0QUFRSURQR3h2
<key>aFloat</key> ZEhNZ2IyWWdZbWx1WVhKNQoJCUlHZDFibXMrQUFFQ0F6eHNiM1J6SUc5bUlH
<real>0.5</real> SnBibUZ5ZVNCbmRXNXJQZ0FCQWdNOGJHOTBjeUJ2WmlCaQoJCWFXNWhjbmtn
<key>aList</key> WjNWdWF6NEFBUUlEUEd4dmRITWdiMllnWW1sdVlYSjVJR2QxYm1zK0FBRUNB
<array> enhzYjNSegoJCUlHOW1JR0pwYm1GeWVTQm5kVzVyUGdBQkFnTThiRzkwY3lC
<string>A</string> dlppQmlhVzVoY25rZ1ozVnVhejRBQVFJRAoJCVBHeHZkSE1nYjJZZ1ltbHVZ
<string>B</string> WEo1SUdkMWJtcytBQUVDQXc9PQoJCTwvZGF0YT4KCTwvYXJyYXk+Cgk8a2V5
<integer>12</integer> PnNvbWVEYXRhPC9rZXk+Cgk8ZGF0YT4KCVBHSnBibUZ5ZVNCbmRXNXJQZz09
<real>32.5</real> Cgk8L2RhdGE+Cgk8a2V5PnNvbWVNb3JlRGF0YTwva2V5PgoJPGRhdGE+CglQ
<array> R3h2ZEhNZ2IyWWdZbWx1WVhKNUlHZDFibXMrQUFFQ0F6eHNiM1J6SUc5bUlH
<integer>1</integer> SnBibUZ5ZVNCbmRXNXJQZ0FCQWdNOAoJYkc5MGN5QnZaaUJpYVc1aGNua2da
<integer>2</integer> M1Z1YXo0QUFRSURQR3h2ZEhNZ2IyWWdZbWx1WVhKNUlHZDFibXMrQUFFQ0F6
<integer>3</integer> eHMKCWIzUnpJRzltSUdKcGJtRnllU0JuZFc1clBnQUJBZ004Ykc5MGN5QnZa
</array> aUJpYVc1aGNua2daM1Z1YXo0QUFRSURQR3h2CglkSE1nYjJZZ1ltbHVZWEo1
</array> SUdkMWJtcytBQUVDQXp4c2IzUnpJRzltSUdKcGJtRnllU0JuZFc1clBnQUJB
<key>aString</key> Z004Ykc5MAoJY3lCdlppQmlhVzVoY25rZ1ozVnVhejRBQVFJRFBHeHZkSE1n
<string>Doodah</string> YjJZZ1ltbHVZWEo1SUdkMWJtcytBQUVDQXc9PQoJPC9kYXRhPgoJPGtleT7D
<key>anEmptyDict</key> hWJlbnJhYTwva2V5PgoJPHN0cmluZz5UaGF0IHdhcyBhIHVuaWNvZGUga2V5
<dict/> Ljwvc3RyaW5nPgo8L2RpY3Q+CjwvcGxpc3Q+Cg=='''),
<key>anEmptyList</key> plistlib.FMT_BINARY: binascii.a2b_base64(b'''
<array/> YnBsaXN0MDDcAQIDBAUGBwgJCgsMDQ4iIykqKywtLy4wVWFEYXRlVWFEaWN0
<key>anInt</key> VmFGbG9hdFVhTGlzdFdhU3RyaW5nW2FuRW1wdHlEaWN0W2FuRW1wdHlMaXN0
<integer>728</integer> VWFuSW50Wm5lc3RlZERhdGFYc29tZURhdGFcc29tZU1vcmVEYXRhZwDFAGIA
<key>nestedData</key> ZQBuAHIAYQBhM0GcuX30AAAA1Q8QERITFBUWFxhbYUZhbHNlVmFsdWVaYVRy
<array> dWVWYWx1ZV1hVW5pY29kZVZhbHVlXWFub3RoZXJTdHJpbmdaZGVlcGVyRGlj
<data> dAgJawBNAOQAcwBzAGkAZwAsACAATQBhAN9fEBU8aGVsbG8gJiAnaGknIHRo
PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r ZXJlIT7TGRobHB0eUWFRYlFjEBEjQEBAAAAAAACjHyAhEAEQAlR0ZXh0Iz/g
PgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5 AAAAAAAApSQlJh0nUUFRQhAMox8gKBADVkRvb2RhaNCgEQLYoS5PEPo8bG90
IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAEC
aW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz Azxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vu
IG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID az4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFy
PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw== eSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2Yg
</data> YmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90
</array> cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDTTxiaW5hcnkgZ3Vuaz5fEBdUaGF0IHdh
<key>someData</key> cyBhIHVuaWNvZGUga2V5LgAIACEAJwAtADQAOgBCAE4AWgBgAGsAdACBAJAA
<data> mQCkALAAuwDJANcA4gDjAOQA+wETARoBHAEeASABIgErAS8BMQEzATgBQQFH
PGJpbmFyeSBndW5rPg== AUkBSwFNAVEBUwFaAVsBXAFfAWECXgJsAAAAAAAAAgEAAAAAAAAAMQAAAAAA
</data> AAAAAAAAAAAAAoY='''),
<key>someMoreData</key> }
<data>
PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8
bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs
b3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv
dHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90
cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==
</data>
<key>\xc3\x85benraa</key>
<string>That was a unicode key.</string>
</dict>
</plist>
""".replace(b" " * 8, b"\t") # Apple as well as plistlib.py output hard tabs
class TestPlistlib(unittest.TestCase): class TestPlistlib(unittest.TestCase):
@ -99,7 +92,7 @@ class TestPlistlib(unittest.TestCase):
except: except:
pass pass
def _create(self): def _create(self, fmt=None):
pl = dict( pl = dict(
aString="Doodah", aString="Doodah",
aList=["A", "B", 12, 32.5, [1, 2, 3]], aList=["A", "B", 12, 32.5, [1, 2, 3]],
@ -112,9 +105,9 @@ class TestPlistlib(unittest.TestCase):
aFalseValue=False, aFalseValue=False,
deeperDict=dict(a=17, b=32.5, c=[1, 2, "text"]), deeperDict=dict(a=17, b=32.5, c=[1, 2, "text"]),
), ),
someData = plistlib.Data(b"<binary gunk>"), someData = b"<binary gunk>",
someMoreData = plistlib.Data(b"<lots of binary gunk>\0\1\2\3" * 10), someMoreData = b"<lots of binary gunk>\0\1\2\3" * 10,
nestedData = [plistlib.Data(b"<lots of binary gunk>\0\1\2\3" * 10)], nestedData = [b"<lots of binary gunk>\0\1\2\3" * 10],
aDate = datetime.datetime(2004, 10, 26, 10, 33, 33), aDate = datetime.datetime(2004, 10, 26, 10, 33, 33),
anEmptyDict = dict(), anEmptyDict = dict(),
anEmptyList = list() anEmptyList = list()
@ -129,67 +122,211 @@ class TestPlistlib(unittest.TestCase):
def test_io(self): def test_io(self):
pl = self._create() pl = self._create()
plistlib.writePlist(pl, support.TESTFN) with open(support.TESTFN, 'wb') as fp:
pl2 = plistlib.readPlist(support.TESTFN) plistlib.dump(pl, fp)
with open(support.TESTFN, 'rb') as fp:
pl2 = plistlib.load(fp)
self.assertEqual(dict(pl), dict(pl2)) self.assertEqual(dict(pl), dict(pl2))
self.assertRaises(AttributeError, plistlib.dump, pl, 'filename')
self.assertRaises(AttributeError, plistlib.load, 'filename')
def test_bytes(self): def test_bytes(self):
pl = self._create() pl = self._create()
data = plistlib.writePlistToBytes(pl) data = plistlib.dumps(pl)
pl2 = plistlib.readPlistFromBytes(data) pl2 = plistlib.loads(data)
self.assertNotIsInstance(pl, plistlib._InternalDict)
self.assertEqual(dict(pl), dict(pl2)) self.assertEqual(dict(pl), dict(pl2))
data2 = plistlib.writePlistToBytes(pl2) data2 = plistlib.dumps(pl2)
self.assertEqual(data, data2) self.assertEqual(data, data2)
def test_indentation_array(self): def test_indentation_array(self):
data = [[[[[[[[{'test': plistlib.Data(b'aaaaaa')}]]]]]]]] data = [[[[[[[[{'test': b'aaaaaa'}]]]]]]]]
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data) self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_indentation_dict(self): def test_indentation_dict(self):
data = {'1': {'2': {'3': {'4': {'5': {'6': {'7': {'8': {'9': plistlib.Data(b'aaaaaa')}}}}}}}}} data = {'1': {'2': {'3': {'4': {'5': {'6': {'7': {'8': {'9': b'aaaaaa'}}}}}}}}}
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data) self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_indentation_dict_mix(self): def test_indentation_dict_mix(self):
data = {'1': {'2': [{'3': [[[[[{'test': plistlib.Data(b'aaaaaa')}]]]]]}]}} data = {'1': {'2': [{'3': [[[[[{'test': b'aaaaaa'}]]]]]}]}}
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data) self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_appleformatting(self): def test_appleformatting(self):
pl = plistlib.readPlistFromBytes(TESTDATA) for use_builtin_types in (True, False):
data = plistlib.writePlistToBytes(pl) for fmt in ALL_FORMATS:
self.assertEqual(data, TESTDATA, with self.subTest(fmt=fmt, use_builtin_types=use_builtin_types):
pl = plistlib.loads(TESTDATA[fmt],
use_builtin_types=use_builtin_types)
data = plistlib.dumps(pl, fmt=fmt)
self.assertEqual(data, TESTDATA[fmt],
"generated data was not identical to Apple's output") "generated data was not identical to Apple's output")
def test_appleformattingfromliteral(self): def test_appleformattingfromliteral(self):
pl = self._create() self.maxDiff = None
pl2 = plistlib.readPlistFromBytes(TESTDATA) for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
pl = self._create(fmt=fmt)
pl2 = plistlib.loads(TESTDATA[fmt])
self.assertEqual(dict(pl), dict(pl2), self.assertEqual(dict(pl), dict(pl2),
"generated data was not identical to Apple's output") "generated data was not identical to Apple's output")
def test_bytesio(self): def test_bytesio(self):
from io import BytesIO for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
b = BytesIO() b = BytesIO()
pl = self._create() pl = self._create(fmt=fmt)
plistlib.writePlist(pl, b) plistlib.dump(pl, b, fmt=fmt)
pl2 = plistlib.readPlist(BytesIO(b.getvalue())) pl2 = plistlib.load(BytesIO(b.getvalue()))
self.assertEqual(dict(pl), dict(pl2)) self.assertEqual(dict(pl), dict(pl2))
def test_keysort_bytesio(self):
pl = collections.OrderedDict()
pl['b'] = 1
pl['a'] = 2
pl['c'] = 3
for fmt in ALL_FORMATS:
for sort_keys in (False, True):
with self.subTest(fmt=fmt, sort_keys=sort_keys):
b = BytesIO()
plistlib.dump(pl, b, fmt=fmt, sort_keys=sort_keys)
pl2 = plistlib.load(BytesIO(b.getvalue()),
dict_type=collections.OrderedDict)
self.assertEqual(dict(pl), dict(pl2))
if sort_keys:
self.assertEqual(list(pl2.keys()), ['a', 'b', 'c'])
else:
self.assertEqual(list(pl2.keys()), ['b', 'a', 'c'])
def test_keysort(self):
pl = collections.OrderedDict()
pl['b'] = 1
pl['a'] = 2
pl['c'] = 3
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, sort_keys=sort_keys)
pl2 = plistlib.loads(data, dict_type=collections.OrderedDict)
self.assertEqual(dict(pl), dict(pl2))
if sort_keys:
self.assertEqual(list(pl2.keys()), ['a', 'b', 'c'])
else:
self.assertEqual(list(pl2.keys()), ['b', 'a', 'c'])
def test_keys_no_string(self):
pl = { 42: 'aNumber' }
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
self.assertRaises(TypeError, plistlib.dumps, pl, fmt=fmt)
b = BytesIO()
self.assertRaises(TypeError, plistlib.dump, pl, b, fmt=fmt)
def test_skipkeys(self):
pl = {
42: 'aNumber',
'snake': 'aWord',
}
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'})
def test_tuple_members(self):
pl = {
'first': (1, 2),
'second': (1, 2),
'third': (3, 4),
}
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
data = plistlib.dumps(pl, fmt=fmt)
pl2 = plistlib.loads(data)
self.assertEqual(pl2, {
'first': [1, 2],
'second': [1, 2],
'third': [3, 4],
})
self.assertIsNot(pl2['first'], pl2['second'])
def test_list_members(self):
pl = {
'first': [1, 2],
'second': [1, 2],
'third': [3, 4],
}
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
data = plistlib.dumps(pl, fmt=fmt)
pl2 = plistlib.loads(data)
self.assertEqual(pl2, {
'first': [1, 2],
'second': [1, 2],
'third': [3, 4],
})
self.assertIsNot(pl2['first'], pl2['second'])
def test_dict_members(self):
pl = {
'first': {'a': 1},
'second': {'a': 1},
'third': {'b': 2 },
}
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
data = plistlib.dumps(pl, fmt=fmt)
pl2 = plistlib.loads(data)
self.assertEqual(pl2, {
'first': {'a': 1},
'second': {'a': 1},
'third': {'b': 2 },
})
self.assertIsNot(pl2['first'], pl2['second'])
def test_controlcharacters(self): def test_controlcharacters(self):
for i in range(128): for i in range(128):
c = chr(i) c = chr(i)
testString = "string containing %s" % c testString = "string containing %s" % c
if i >= 32 or c in "\r\n\t": if i >= 32 or c in "\r\n\t":
# \r, \n and \t are the only legal control chars in XML # \r, \n and \t are the only legal control chars in XML
plistlib.writePlistToBytes(testString) plistlib.dumps(testString, fmt=plistlib.FMT_XML)
else: else:
self.assertRaises(ValueError, self.assertRaises(ValueError,
plistlib.writePlistToBytes, plistlib.dumps,
testString) testString)
def test_nondictroot(self): def test_nondictroot(self):
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
test1 = "abc" test1 = "abc"
test2 = [1, 2, 3, "abc"] test2 = [1, 2, 3, "abc"]
result1 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test1)) result1 = plistlib.loads(plistlib.dumps(test1, fmt=fmt))
result2 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test2)) result2 = plistlib.loads(plistlib.dumps(test2, fmt=fmt))
self.assertEqual(test1, result1) self.assertEqual(test1, result1)
self.assertEqual(test2, result2) self.assertEqual(test2, result2)
@ -197,7 +334,7 @@ class TestPlistlib(unittest.TestCase):
for i in ["<key>key inside an array</key>", for i in ["<key>key inside an array</key>",
"<key>key inside an array2</key><real>3</real>", "<key>key inside an array2</key><real>3</real>",
"<true/><key>key inside an array3</key>"]: "<true/><key>key inside an array3</key>"]:
self.assertRaises(ValueError, plistlib.readPlistFromBytes, self.assertRaises(ValueError, plistlib.loads,
("<plist><array>%s</array></plist>"%i).encode()) ("<plist><array>%s</array></plist>"%i).encode())
def test_invaliddict(self): def test_invaliddict(self):
@ -206,22 +343,130 @@ class TestPlistlib(unittest.TestCase):
"<string>missing key</string>", "<string>missing key</string>",
"<key>k1</key><string>v1</string><real>5.3</real>" "<key>k1</key><string>v1</string><real>5.3</real>"
"<key>k1</key><key>k2</key><string>double key</string>"]: "<key>k1</key><key>k2</key><string>double key</string>"]:
self.assertRaises(ValueError, plistlib.readPlistFromBytes, self.assertRaises(ValueError, plistlib.loads,
("<plist><dict>%s</dict></plist>"%i).encode()) ("<plist><dict>%s</dict></plist>"%i).encode())
self.assertRaises(ValueError, plistlib.readPlistFromBytes, self.assertRaises(ValueError, plistlib.loads,
("<plist><array><dict>%s</dict></array></plist>"%i).encode()) ("<plist><array><dict>%s</dict></array></plist>"%i).encode())
def test_invalidinteger(self): def test_invalidinteger(self):
self.assertRaises(ValueError, plistlib.readPlistFromBytes, self.assertRaises(ValueError, plistlib.loads,
b"<plist><integer>not integer</integer></plist>") b"<plist><integer>not integer</integer></plist>")
def test_invalidreal(self): def test_invalidreal(self):
self.assertRaises(ValueError, plistlib.readPlistFromBytes, self.assertRaises(ValueError, plistlib.loads,
b"<plist><integer>not real</integer></plist>") b"<plist><integer>not real</integer></plist>")
def test_xml_encodings(self):
base = TESTDATA[plistlib.FMT_XML]
for xml_encoding, encoding, bom in [
(b'utf-8', 'utf-8', codecs.BOM_UTF8),
(b'utf-16', 'utf-16-le', codecs.BOM_UTF16_LE),
(b'utf-16', 'utf-16-be', codecs.BOM_UTF16_BE),
# Expat does not support UTF-32
#(b'utf-32', 'utf-32-le', codecs.BOM_UTF32_LE),
#(b'utf-32', 'utf-32-be', codecs.BOM_UTF32_BE),
]:
pl = self._create(fmt=plistlib.FMT_XML)
with self.subTest(encoding=encoding):
data = base.replace(b'UTF-8', xml_encoding)
data = bom + data.decode('utf-8').encode(encoding)
pl2 = plistlib.loads(data)
self.assertEqual(dict(pl), dict(pl2))
class TestPlistlibDeprecated(unittest.TestCase):
def test_io_deprecated(self):
pl_in = {
'key': 42,
'sub': {
'key': 9,
'alt': 'value',
'data': b'buffer',
}
}
pl_out = plistlib._InternalDict({
'key': 42,
'sub': plistlib._InternalDict({
'key': 9,
'alt': 'value',
'data': plistlib.Data(b'buffer'),
})
})
self.addCleanup(support.unlink, support.TESTFN)
with self.assertWarns(DeprecationWarning):
plistlib.writePlist(pl_in, support.TESTFN)
with self.assertWarns(DeprecationWarning):
pl2 = plistlib.readPlist(support.TESTFN)
self.assertEqual(pl_out, pl2)
os.unlink(support.TESTFN)
with open(support.TESTFN, 'wb') as fp:
with self.assertWarns(DeprecationWarning):
plistlib.writePlist(pl_in, fp)
with open(support.TESTFN, 'rb') as fp:
with self.assertWarns(DeprecationWarning):
pl2 = plistlib.readPlist(fp)
self.assertEqual(pl_out, pl2)
def test_bytes_deprecated(self):
pl = {
'key': 42,
'sub': {
'key': 9,
'alt': 'value',
'data': b'buffer',
}
}
with self.assertWarns(DeprecationWarning):
data = plistlib.writePlistToBytes(pl)
with self.assertWarns(DeprecationWarning):
pl2 = plistlib.readPlistFromBytes(data)
self.assertIsInstance(pl2, plistlib._InternalDict)
self.assertEqual(pl2, plistlib._InternalDict(
key=42,
sub=plistlib._InternalDict(
key=9,
alt='value',
data=plistlib.Data(b'buffer'),
)
))
with self.assertWarns(DeprecationWarning):
data2 = plistlib.writePlistToBytes(pl2)
self.assertEqual(data, data2)
def test_dataobject_deprecated(self):
in_data = { 'key': plistlib.Data(b'hello') }
out_data = { 'key': b'hello' }
buf = plistlib.dumps(in_data)
cur = plistlib.loads(buf)
self.assertEqual(cur, out_data)
self.assertNotEqual(cur, in_data)
cur = plistlib.loads(buf, use_builtin_types=False)
self.assertNotEqual(cur, out_data)
self.assertEqual(cur, in_data)
with self.assertWarns(DeprecationWarning):
cur = plistlib.readPlistFromBytes(buf)
self.assertNotEqual(cur, out_data)
self.assertEqual(cur, in_data)
def test_main(): def test_main():
support.run_unittest(TestPlistlib) support.run_unittest(TestPlistlib, TestPlistlibDeprecated)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,94 @@
#!/usr/bin/env python3
from Cocoa import NSMutableDictionary, NSMutableArray, NSString, NSDate
from Cocoa import NSPropertyListSerialization, NSPropertyListOpenStepFormat
from Cocoa import NSPropertyListXMLFormat_v1_0, NSPropertyListBinaryFormat_v1_0
from Cocoa import CFUUIDCreateFromString, NSNull, NSUUID, CFPropertyListCreateData
from Cocoa import NSURL
import datetime
from collections import OrderedDict
import binascii
FORMATS=[
# ('openstep', NSPropertyListOpenStepFormat),
('plistlib.FMT_XML', NSPropertyListXMLFormat_v1_0),
('plistlib.FMT_BINARY', NSPropertyListBinaryFormat_v1_0),
]
def nsstr(value):
return NSString.alloc().initWithString_(value)
def main():
pl = OrderedDict()
seconds = datetime.datetime(2004, 10, 26, 10, 33, 33, tzinfo=datetime.timezone(datetime.timedelta(0))).timestamp()
pl[nsstr('aDate')] = NSDate.dateWithTimeIntervalSince1970_(seconds)
pl[nsstr('aDict')] = d = OrderedDict()
d[nsstr('aFalseValue')] = False
d[nsstr('aTrueValue')] = True
d[nsstr('aUnicodeValue')] = "M\xe4ssig, Ma\xdf"
d[nsstr('anotherString')] = "<hello & 'hi' there!>"
d[nsstr('deeperDict')] = dd = OrderedDict()
dd[nsstr('a')] = 17
dd[nsstr('b')] = 32.5
dd[nsstr('c')] = a = NSMutableArray.alloc().init()
a.append(1)
a.append(2)
a.append(nsstr('text'))
pl[nsstr('aFloat')] = 0.5
pl[nsstr('aList')] = a = NSMutableArray.alloc().init()
a.append(nsstr('A'))
a.append(nsstr('B'))
a.append(12)
a.append(32.5)
aa = NSMutableArray.alloc().init()
a.append(aa)
aa.append(1)
aa.append(2)
aa.append(3)
pl[nsstr('aString')] = nsstr('Doodah')
pl[nsstr('anEmptyDict')] = NSMutableDictionary.alloc().init()
pl[nsstr('anEmptyList')] = NSMutableArray.alloc().init()
pl[nsstr('anInt')] = 728
pl[nsstr('nestedData')] = a = NSMutableArray.alloc().init()
a.append(b'''<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03''')
pl[nsstr('someData')] = b'<binary gunk>'
pl[nsstr('someMoreData')] = b'''<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03<lots of binary gunk>\x00\x01\x02\x03'''
pl[nsstr('\xc5benraa')] = nsstr("That was a unicode key.")
print("TESTDATA={")
for fmt_name, fmt_key in FORMATS:
data, error = NSPropertyListSerialization.dataWithPropertyList_format_options_error_(
pl, fmt_key, 0, None)
if data is None:
print("Cannot serialize", fmt_name, error)
else:
print(" %s: binascii.a2b_base64(b'''\n %s'''),"%(fmt_name, _encode_base64(bytes(data)).decode('ascii')[:-1]))
print("}")
print()
def _encode_base64(s, maxlinelength=60):
maxbinsize = (maxlinelength//4)*3
pieces = []
for i in range(0, len(s), maxbinsize):
chunk = s[i : i + maxbinsize]
pieces.append(binascii.b2a_base64(chunk))
return b' '.join(pieces)
main()

View File

@ -59,6 +59,8 @@ Core and Builtins
Library Library
------- -------
- Issue #14455: plistlib now supports binary plists and has an updated API.
- Issue #19633: Fixed writing not compressed 16- and 32-bit wave files on - Issue #19633: Fixed writing not compressed 16- and 32-bit wave files on
big-endian platforms. big-endian platforms.