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"
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
top level object is a dictionary.
To write out and to parse a plist file, use the :func:`writePlist` and
:func:`readPlist` functions.
To write out and to parse a plist file, use the :func:`dump` and
:func:`load` functions.
To work with plist data in bytes objects, use :func:`writePlistToBytes`
and :func:`readPlistFromBytes`.
To work with plist data in bytes objects, use :func:`dumps`
and :func:`loads`.
Values can be strings, integers, floats, booleans, tuples, lists, dictionaries
(but only with string keys), :class:`Data` or :class:`datetime.datetime`
objects. String values (including dictionary keys) have to be unicode strings --
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.
(but only with string keys), :class:`Data`, :class:`bytes`, :class:`bytesarray`
or :class:`datetime.datetime` objects.
.. seealso::
@ -45,37 +40,145 @@ contain control characters.
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
binary) file object. Return the unpacked root object (which usually is a
Read a plist file. *fp* should be a readable and binary file object.
Return the unpacked root object (which usually is a
dictionary).
The XML data 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 *fmt* is the format of the file and the following values are valid:
* :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)
Write *rootObject* to a plist file. *pathOrFile* may either be a file name
or a (writable and binary) file object.
Write *rootObject* to an XML plist file. *pathOrFile* may be either a file name
or a (writable and binary) file object
A :exc:`TypeError` will be raised if the object is of an unsupported type or
a container that contains objects of unsupported types.
.. deprecated: 3.4 Use :func:`dump` instead.
.. function:: readPlistFromBytes(data)
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)
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)
@ -86,6 +189,24 @@ The following class is available:
It has one attribute, :attr:`data`, that can be used to retrieve the Python
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
--------
@ -103,13 +224,15 @@ Generating a plist::
aTrueValue = True,
aFalseValue = False,
),
someData = Data(b"<binary gunk>"),
someMoreData = Data(b"<lots of binary gunk>" * 10),
someData = b"<binary gunk>",
someMoreData = b"<lots of binary gunk>" * 10,
aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
)
writePlist(pl, fileName)
with open(fileName, 'wb') as fp:
dump(pl, fp)
Parsing a plist::
pl = readPlist(pathOrFile)
with open(fileName, 'rb') as fp:
pl = load(fp)
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 plistlib
import os
import datetime
import codecs
import binascii
import collections
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
TESTDATA = b"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aDate</key>
<date>2004-10-26T10:33:33Z</date>
<key>aDict</key>
<dict>
<key>aFalseValue</key>
<false/>
<key>aTrueValue</key>
<true/>
<key>aUnicodeValue</key>
<string>M\xc3\xa4ssig, Ma\xc3\x9f</string>
<key>anotherString</key>
<string>&lt;hello &amp; 'hi' there!&gt;</string>
<key>deeperDict</key>
<dict>
<key>a</key>
<integer>17</integer>
<key>b</key>
<real>32.5</real>
<key>c</key>
<array>
<integer>1</integer>
<integer>2</integer>
<string>text</string>
</array>
</dict>
</dict>
<key>aFloat</key>
<real>0.5</real>
<key>aList</key>
<array>
<string>A</string>
<string>B</string>
<integer>12</integer>
<real>32.5</real>
<array>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
</array>
</array>
<key>aString</key>
<string>Doodah</string>
<key>anEmptyDict</key>
<dict/>
<key>anEmptyList</key>
<array/>
<key>anInt</key>
<integer>728</integer>
<key>nestedData</key>
<array>
<data>
PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r
PgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5
IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi
aW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz
IG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID
PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==
</data>
</array>
<key>someData</key>
<data>
PGJpbmFyeSBndW5rPg==
</data>
<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
# The testdata is generated using Mac/Tools/plistlib_generate_testdata.py
# (which using PyObjC to control the Cocoa classes for generating plists)
TESTDATA={
plistlib.FMT_XML: binascii.a2b_base64(b'''
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU
WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO
IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w
LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+YURh
dGU8L2tleT4KCTxkYXRlPjIwMDQtMTAtMjZUMTA6MzM6MzNaPC9kYXRlPgoJ
PGtleT5hRGljdDwva2V5PgoJPGRpY3Q+CgkJPGtleT5hRmFsc2VWYWx1ZTwv
a2V5PgoJCTxmYWxzZS8+CgkJPGtleT5hVHJ1ZVZhbHVlPC9rZXk+CgkJPHRy
dWUvPgoJCTxrZXk+YVVuaWNvZGVWYWx1ZTwva2V5PgoJCTxzdHJpbmc+TcOk
c3NpZywgTWHDnzwvc3RyaW5nPgoJCTxrZXk+YW5vdGhlclN0cmluZzwva2V5
PgoJCTxzdHJpbmc+Jmx0O2hlbGxvICZhbXA7ICdoaScgdGhlcmUhJmd0Ozwv
c3RyaW5nPgoJCTxrZXk+ZGVlcGVyRGljdDwva2V5PgoJCTxkaWN0PgoJCQk8
a2V5PmE8L2tleT4KCQkJPGludGVnZXI+MTc8L2ludGVnZXI+CgkJCTxrZXk+
Yjwva2V5PgoJCQk8cmVhbD4zMi41PC9yZWFsPgoJCQk8a2V5PmM8L2tleT4K
CQkJPGFycmF5PgoJCQkJPGludGVnZXI+MTwvaW50ZWdlcj4KCQkJCTxpbnRl
Z2VyPjI8L2ludGVnZXI+CgkJCQk8c3RyaW5nPnRleHQ8L3N0cmluZz4KCQkJ
PC9hcnJheT4KCQk8L2RpY3Q+Cgk8L2RpY3Q+Cgk8a2V5PmFGbG9hdDwva2V5
PgoJPHJlYWw+MC41PC9yZWFsPgoJPGtleT5hTGlzdDwva2V5PgoJPGFycmF5
PgoJCTxzdHJpbmc+QTwvc3RyaW5nPgoJCTxzdHJpbmc+Qjwvc3RyaW5nPgoJ
CTxpbnRlZ2VyPjEyPC9pbnRlZ2VyPgoJCTxyZWFsPjMyLjU8L3JlYWw+CgkJ
PGFycmF5PgoJCQk8aW50ZWdlcj4xPC9pbnRlZ2VyPgoJCQk8aW50ZWdlcj4y
PC9pbnRlZ2VyPgoJCQk8aW50ZWdlcj4zPC9pbnRlZ2VyPgoJCTwvYXJyYXk+
Cgk8L2FycmF5PgoJPGtleT5hU3RyaW5nPC9rZXk+Cgk8c3RyaW5nPkRvb2Rh
aDwvc3RyaW5nPgoJPGtleT5hbkVtcHR5RGljdDwva2V5PgoJPGRpY3QvPgoJ
PGtleT5hbkVtcHR5TGlzdDwva2V5PgoJPGFycmF5Lz4KCTxrZXk+YW5JbnQ8
L2tleT4KCTxpbnRlZ2VyPjcyODwvaW50ZWdlcj4KCTxrZXk+bmVzdGVkRGF0
YTwva2V5PgoJPGFycmF5PgoJCTxkYXRhPgoJCVBHeHZkSE1nYjJZZ1ltbHVZ
WEo1SUdkMWJtcytBQUVDQXp4c2IzUnpJRzltSUdKcGJtRnllU0JuZFc1cgoJ
CVBnQUJBZ004Ykc5MGN5QnZaaUJpYVc1aGNua2daM1Z1YXo0QUFRSURQR3h2
ZEhNZ2IyWWdZbWx1WVhKNQoJCUlHZDFibXMrQUFFQ0F6eHNiM1J6SUc5bUlH
SnBibUZ5ZVNCbmRXNXJQZ0FCQWdNOGJHOTBjeUJ2WmlCaQoJCWFXNWhjbmtn
WjNWdWF6NEFBUUlEUEd4dmRITWdiMllnWW1sdVlYSjVJR2QxYm1zK0FBRUNB
enhzYjNSegoJCUlHOW1JR0pwYm1GeWVTQm5kVzVyUGdBQkFnTThiRzkwY3lC
dlppQmlhVzVoY25rZ1ozVnVhejRBQVFJRAoJCVBHeHZkSE1nYjJZZ1ltbHVZ
WEo1SUdkMWJtcytBQUVDQXc9PQoJCTwvZGF0YT4KCTwvYXJyYXk+Cgk8a2V5
PnNvbWVEYXRhPC9rZXk+Cgk8ZGF0YT4KCVBHSnBibUZ5ZVNCbmRXNXJQZz09
Cgk8L2RhdGE+Cgk8a2V5PnNvbWVNb3JlRGF0YTwva2V5PgoJPGRhdGE+CglQ
R3h2ZEhNZ2IyWWdZbWx1WVhKNUlHZDFibXMrQUFFQ0F6eHNiM1J6SUc5bUlH
SnBibUZ5ZVNCbmRXNXJQZ0FCQWdNOAoJYkc5MGN5QnZaaUJpYVc1aGNua2da
M1Z1YXo0QUFRSURQR3h2ZEhNZ2IyWWdZbWx1WVhKNUlHZDFibXMrQUFFQ0F6
eHMKCWIzUnpJRzltSUdKcGJtRnllU0JuZFc1clBnQUJBZ004Ykc5MGN5QnZa
aUJpYVc1aGNua2daM1Z1YXo0QUFRSURQR3h2CglkSE1nYjJZZ1ltbHVZWEo1
SUdkMWJtcytBQUVDQXp4c2IzUnpJRzltSUdKcGJtRnllU0JuZFc1clBnQUJB
Z004Ykc5MAoJY3lCdlppQmlhVzVoY25rZ1ozVnVhejRBQVFJRFBHeHZkSE1n
YjJZZ1ltbHVZWEo1SUdkMWJtcytBQUVDQXc9PQoJPC9kYXRhPgoJPGtleT7D
hWJlbnJhYTwva2V5PgoJPHN0cmluZz5UaGF0IHdhcyBhIHVuaWNvZGUga2V5
Ljwvc3RyaW5nPgo8L2RpY3Q+CjwvcGxpc3Q+Cg=='''),
plistlib.FMT_BINARY: binascii.a2b_base64(b'''
YnBsaXN0MDDcAQIDBAUGBwgJCgsMDQ4iIykqKywtLy4wVWFEYXRlVWFEaWN0
VmFGbG9hdFVhTGlzdFdhU3RyaW5nW2FuRW1wdHlEaWN0W2FuRW1wdHlMaXN0
VWFuSW50Wm5lc3RlZERhdGFYc29tZURhdGFcc29tZU1vcmVEYXRhZwDFAGIA
ZQBuAHIAYQBhM0GcuX30AAAA1Q8QERITFBUWFxhbYUZhbHNlVmFsdWVaYVRy
dWVWYWx1ZV1hVW5pY29kZVZhbHVlXWFub3RoZXJTdHJpbmdaZGVlcGVyRGlj
dAgJawBNAOQAcwBzAGkAZwAsACAATQBhAN9fEBU8aGVsbG8gJiAnaGknIHRo
ZXJlIT7TGRobHB0eUWFRYlFjEBEjQEBAAAAAAACjHyAhEAEQAlR0ZXh0Iz/g
AAAAAAAApSQlJh0nUUFRQhAMox8gKBADVkRvb2RhaNCgEQLYoS5PEPo8bG90
cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAEC
Azxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vu
az4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFy
eSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2Yg
YmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90
cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDTTxiaW5hcnkgZ3Vuaz5fEBdUaGF0IHdh
cyBhIHVuaWNvZGUga2V5LgAIACEAJwAtADQAOgBCAE4AWgBgAGsAdACBAJAA
mQCkALAAuwDJANcA4gDjAOQA+wETARoBHAEeASABIgErAS8BMQEzATgBQQFH
AUkBSwFNAVEBUwFaAVsBXAFfAWECXgJsAAAAAAAAAgEAAAAAAAAAMQAAAAAA
AAAAAAAAAAAAAoY='''),
}
class TestPlistlib(unittest.TestCase):
@ -99,7 +92,7 @@ class TestPlistlib(unittest.TestCase):
except:
pass
def _create(self):
def _create(self, fmt=None):
pl = dict(
aString="Doodah",
aList=["A", "B", 12, 32.5, [1, 2, 3]],
@ -112,9 +105,9 @@ class TestPlistlib(unittest.TestCase):
aFalseValue=False,
deeperDict=dict(a=17, b=32.5, c=[1, 2, "text"]),
),
someData = plistlib.Data(b"<binary gunk>"),
someMoreData = plistlib.Data(b"<lots of binary gunk>\0\1\2\3" * 10),
nestedData = [plistlib.Data(b"<lots of binary gunk>\0\1\2\3" * 10)],
someData = b"<binary gunk>",
someMoreData = 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),
anEmptyDict = dict(),
anEmptyList = list()
@ -129,49 +122,191 @@ class TestPlistlib(unittest.TestCase):
def test_io(self):
pl = self._create()
plistlib.writePlist(pl, support.TESTFN)
pl2 = plistlib.readPlist(support.TESTFN)
with open(support.TESTFN, 'wb') as fp:
plistlib.dump(pl, fp)
with open(support.TESTFN, 'rb') as fp:
pl2 = plistlib.load(fp)
self.assertEqual(dict(pl), dict(pl2))
self.assertRaises(AttributeError, plistlib.dump, pl, 'filename')
self.assertRaises(AttributeError, plistlib.load, 'filename')
def test_bytes(self):
pl = self._create()
data = plistlib.writePlistToBytes(pl)
pl2 = plistlib.readPlistFromBytes(data)
data = plistlib.dumps(pl)
pl2 = plistlib.loads(data)
self.assertNotIsInstance(pl, plistlib._InternalDict)
self.assertEqual(dict(pl), dict(pl2))
data2 = plistlib.writePlistToBytes(pl2)
data2 = plistlib.dumps(pl2)
self.assertEqual(data, data2)
def test_indentation_array(self):
data = [[[[[[[[{'test': plistlib.Data(b'aaaaaa')}]]]]]]]]
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data)
data = [[[[[[[[{'test': b'aaaaaa'}]]]]]]]]
self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_indentation_dict(self):
data = {'1': {'2': {'3': {'4': {'5': {'6': {'7': {'8': {'9': plistlib.Data(b'aaaaaa')}}}}}}}}}
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data)
data = {'1': {'2': {'3': {'4': {'5': {'6': {'7': {'8': {'9': b'aaaaaa'}}}}}}}}}
self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_indentation_dict_mix(self):
data = {'1': {'2': [{'3': [[[[[{'test': plistlib.Data(b'aaaaaa')}]]]]]}]}}
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data)
data = {'1': {'2': [{'3': [[[[[{'test': b'aaaaaa'}]]]]]}]}}
self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_appleformatting(self):
pl = plistlib.readPlistFromBytes(TESTDATA)
data = plistlib.writePlistToBytes(pl)
self.assertEqual(data, TESTDATA,
"generated data was not identical to Apple's output")
for use_builtin_types in (True, False):
for fmt in ALL_FORMATS:
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")
def test_appleformattingfromliteral(self):
pl = self._create()
pl2 = plistlib.readPlistFromBytes(TESTDATA)
self.assertEqual(dict(pl), dict(pl2),
"generated data was not identical to Apple's output")
self.maxDiff = None
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),
"generated data was not identical to Apple's output")
def test_bytesio(self):
from io import BytesIO
b = BytesIO()
pl = self._create()
plistlib.writePlist(pl, b)
pl2 = plistlib.readPlist(BytesIO(b.getvalue()))
self.assertEqual(dict(pl), dict(pl2))
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
b = BytesIO()
pl = self._create(fmt=fmt)
plistlib.dump(pl, b, fmt=fmt)
pl2 = plistlib.load(BytesIO(b.getvalue()))
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):
for i in range(128):
@ -179,25 +314,27 @@ class TestPlistlib(unittest.TestCase):
testString = "string containing %s" % c
if i >= 32 or c in "\r\n\t":
# \r, \n and \t are the only legal control chars in XML
plistlib.writePlistToBytes(testString)
plistlib.dumps(testString, fmt=plistlib.FMT_XML)
else:
self.assertRaises(ValueError,
plistlib.writePlistToBytes,
plistlib.dumps,
testString)
def test_nondictroot(self):
test1 = "abc"
test2 = [1, 2, 3, "abc"]
result1 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test1))
result2 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test2))
self.assertEqual(test1, result1)
self.assertEqual(test2, result2)
for fmt in ALL_FORMATS:
with self.subTest(fmt=fmt):
test1 = "abc"
test2 = [1, 2, 3, "abc"]
result1 = plistlib.loads(plistlib.dumps(test1, fmt=fmt))
result2 = plistlib.loads(plistlib.dumps(test2, fmt=fmt))
self.assertEqual(test1, result1)
self.assertEqual(test2, result2)
def test_invalidarray(self):
for i in ["<key>key inside an array</key>",
"<key>key inside an array2</key><real>3</real>",
"<true/><key>key inside an array3</key>"]:
self.assertRaises(ValueError, plistlib.readPlistFromBytes,
self.assertRaises(ValueError, plistlib.loads,
("<plist><array>%s</array></plist>"%i).encode())
def test_invaliddict(self):
@ -206,22 +343,130 @@ class TestPlistlib(unittest.TestCase):
"<string>missing key</string>",
"<key>k1</key><string>v1</string><real>5.3</real>"
"<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())
self.assertRaises(ValueError, plistlib.readPlistFromBytes,
self.assertRaises(ValueError, plistlib.loads,
("<plist><array><dict>%s</dict></array></plist>"%i).encode())
def test_invalidinteger(self):
self.assertRaises(ValueError, plistlib.readPlistFromBytes,
self.assertRaises(ValueError, plistlib.loads,
b"<plist><integer>not integer</integer></plist>")
def test_invalidreal(self):
self.assertRaises(ValueError, plistlib.readPlistFromBytes,
self.assertRaises(ValueError, plistlib.loads,
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():
support.run_unittest(TestPlistlib)
support.run_unittest(TestPlistlib, TestPlistlibDeprecated)
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
-------
- 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
big-endian platforms.