diff --git a/Doc/library/plistlib.rst b/Doc/library/plistlib.rst
--- a/Doc/library/plistlib.rst
+++ b/Doc/library/plistlib.rst
@@ -45,35 +45,114 @@
This module defines the following functions:
-.. function:: readPlist(pathOrFile)
+.. function:: readPlist(pathOrFile, \*, fmt=None, bytes_for_data=False, 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
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
-.. function:: writePlist(rootObject, pathOrFile)
+ * :data:`FMT_XML`: XML file format
+
+ * :data:`FMT_BINARY`: Binary plist format
+
+ If *bytes_for_data* is false (the default) binary data will be returned as instances
+ of :class:`Data`, otherwise it is returned as instances of :class:`bytes`.
+
+ 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 "xml1" 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.
+
+ .. versionchanged:: 3.4
+ Added the the *fmt*, *bytes_for_data* and *dict_type* arguments.
+
+
+.. function:: writePlist(rootObject, pathOrFile, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False)
Write *rootObject* to a plist file. *pathOrFile* may either be a file name
or a (writable and 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 the *fmt*, *sort_keys* and *skipkeys* arguments.
-.. function:: readPlistFromBytes(data)
+
+.. function:: readPlistFromBytes(data, \*, fmt=None, bytes_for_data=False, dict_type=dict)
Read a plist data from a bytes object. Return the root object.
+ The *fmt* is the format of the data and the following values are valid:
-.. function:: writePlistToBytes(rootObject)
+ * :data:`None`: Autodetect the file format
+
+ * :data:`FMT_XML`: XML file format
+
+ * :data:`FMT_BINARY`: Binary plist format
+
+ If *bytes_for_data* is false (the default) binary data will be returned as instances
+ of :class:`Data`, otherwise it is returned as instances of :class:`bytes`.
+
+ 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).
+
+ 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 the *fmt*, *bytes_for_data* and *dict_type* arguments.
+
+
+.. function:: writePlistToBytes(rootObject, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False)
Return *rootObject* as a plist-formatted bytes 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.
+
+ .. versionchanged:: 3.4
+ Added the the *fmt*, *sort_keys* and *skipkeys* arguments.
+
The following class is available:
@@ -86,6 +165,21 @@
It has one attribute, :attr:`data`, that can be used to retrieve the Python
bytes object stored in it.
+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
--------
diff --git a/Lib/plistlib.py b/Lib/plistlib.py
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -4,8 +4,8 @@
basic object types, like dictionaries, lists, numbers and strings.
Usually the top level object is a dictionary.
-To write out a plist file, use the writePlist(rootObject, pathOrFile)
-function. 'rootObject' is the top level object, 'pathOrFile' is a
+To write out a plist file, use the writePlist(value, pathOrFile)
+function. 'value' is the top level object, 'pathOrFile' is a
filename or a (writable) file object.
To parse a plist from a file, use the readPlist(pathOrFile) function,
@@ -48,71 +48,496 @@
pl = readPlist(pathOrFile)
print pl["aKey"]
"""
-
-
__all__ = [
"readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
- "Plist", "Data", "Dict"
+ "Plist", "Data", "Dict", "FMT_XML", "FMT_BINARY",
]
# Note: the Plist and Dict classes have been deprecated.
import binascii
+import codecs
import datetime
from io import BytesIO
+import os
import re
+import struct
+import itertools
+from xml.parsers.expat import ParserCreate
+FMT_XML="xml1"
+FMT_BINARY="binary1"
-def readPlist(pathOrFile):
- """Read a .plist file. 'pathOrFile' may either be a file name or a
- (readable) file object. Return the unpacked root object (which
- usually is a dictionary).
+class InvalidFileException(ValueError):
+ def __str__(self):
+ return "Invalid file"
+ def __unicode__(self):
+ return "Invalid file"
+
+class _InternalDict(dict):
+
+ # This class is needed while Dict is scheduled for deprecation:
+ # we only need to warn when a *user* instantiates Dict or when
+ # the "attribute notation for dict keys" is used.
+
+ def __getattr__(self, attr):
+ try:
+ value = self[attr]
+ except KeyError:
+ raise AttributeError(attr)
+ from warnings import warn
+ warn("Attribute access from plist dicts is deprecated, use d[key] "
+ "notation instead", DeprecationWarning, 2)
+ return value
+
+ def __setattr__(self, attr, value):
+ from warnings import warn
+ warn("Attribute access from plist dicts is deprecated, use d[key] "
+ "notation instead", DeprecationWarning, 2)
+ self[attr] = value
+
+ def __delattr__(self, attr):
+ try:
+ del self[attr]
+ except KeyError:
+ raise AttributeError(attr)
+ from warnings import warn
+ warn("Attribute access from plist dicts is deprecated, use d[key] "
+ "notation instead", DeprecationWarning, 2)
+
+class _PlistParser:
+ def __init__(self, bytes_for_data, dict_type):
+ self.stack = []
+ self.current_key = None
+ self.root = None
+ self._bytes_for_data = bytes_for_data
+ self._dict_type = dict_type
+
+ def parse(self, fileobj):
+ self.parser = ParserCreate()
+ self.parser.StartElementHandler = self.handleBeginElement
+ self.parser.EndElementHandler = self.handleEndElement
+ self.parser.CharacterDataHandler = self.handleData
+ self.parser.ParseFile(fileobj)
+ return self.root
+
+ def handleBeginElement(self, element, attrs):
+ self.data = []
+ handler = getattr(self, "begin_" + element, None)
+ if handler is not None:
+ handler(attrs)
+
+ def handleEndElement(self, element):
+ handler = getattr(self, "end_" + element, None)
+ if handler is not None:
+ handler()
+
+ def handleData(self, data):
+ self.data.append(data)
+
+ def addObject(self, value):
+ if self.current_key is not None:
+ if not isinstance(self.stack[-1], type({})):
+ raise ValueError("unexpected element at line %d" %
+ self.parser.CurrentLineNumber)
+ self.stack[-1][self.current_key] = value
+ self.current_key = None
+ elif not self.stack:
+ # this is the root object
+ self.root = value
+ else:
+ if not isinstance(self.stack[-1], type([])):
+ raise ValueError("unexpected element at line %d" %
+ self.parser.CurrentLineNumber)
+ self.stack[-1].append(value)
+
+ def getData(self):
+ data = ''.join(self.data)
+ self.data = []
+ return data
+
+ # element handlers
+
+ def begin_dict(self, attrs):
+ d = self._dict_type()
+ self.addObject(d)
+ self.stack.append(d)
+
+ def end_dict(self):
+ if self.current_key:
+ raise ValueError("missing value for key '%s' at line %d" %
+ (self.current_key,self.parser.CurrentLineNumber))
+ self.stack.pop()
+
+ def end_key(self):
+ if self.current_key or not isinstance(self.stack[-1], type({})):
+ raise ValueError("unexpected key at line %d" %
+ self.parser.CurrentLineNumber)
+ self.current_key = self.getData()
+
+ def begin_array(self, attrs):
+ a = []
+ self.addObject(a)
+ self.stack.append(a)
+
+ def end_array(self):
+ self.stack.pop()
+
+ def end_true(self):
+ self.addObject(True)
+
+ def end_false(self):
+ self.addObject(False)
+
+ def end_integer(self):
+ self.addObject(int(self.getData()))
+
+ def end_real(self):
+ self.addObject(float(self.getData()))
+
+ def end_string(self):
+ self.addObject(self.getData())
+
+ def end_data(self):
+ if self._bytes_for_data:
+ self.addObject(binascii.a2b_base64(self.getData().encode("utf-8")))
+ else:
+ self.addObject(Data.fromBase64(self.getData().encode("utf-8")))
+
+ def end_date(self):
+ self.addObject(_date_from_string(self.getData()))
+
+
+class _BinaryPlistParser(object):
"""
- didOpen = False
- try:
- if isinstance(pathOrFile, str):
- pathOrFile = open(pathOrFile, 'rb')
- didOpen = True
- p = PlistParser()
- rootObject = p.parse(pathOrFile)
- finally:
- if didOpen:
- pathOrFile.close()
- return rootObject
+ Read or write a binary plist file, following the description of the binary format.
+ Raise InvalidFileException in case of error, otherwise return the root object, as usual
+ see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c
+ """
+ def __init__(self, bytes_for_data, dict_type):
+ self._bytes_for_data = bytes_for_data
+ self._dict_type = dict_type
-def writePlist(rootObject, pathOrFile):
- """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a
- file name or a (writable) file object.
- """
- didOpen = False
- try:
- if isinstance(pathOrFile, str):
- pathOrFile = open(pathOrFile, 'wb')
- didOpen = True
- writer = PlistWriter(pathOrFile)
- writer.writeln("")
- writer.writeValue(rootObject)
- writer.writeln("")
- finally:
- if didOpen:
- pathOrFile.close()
+ def parse(self, fp):
+ return self.read(fp)
+ def read(self, fp):
+ # File format:
+ # HEADER
+ # object...
+ # refid->offset...
+ # TRAILER
+ self._fp = fp
+ self._fp.seek(-32, os.SEEK_END) # go to the trailer
+ trailer = self._fp.read(32)
+ if len(trailer) != 32:
+ return InvalidFileException()
+ offset_size, self._ref_size, num_objects, top_object, offset_table_offset = struct.unpack('>6xBBQQQ', trailer)
+ self._fp.seek(offset_table_offset)
+ offset_format = '>' + {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[offset_size] * num_objects
+ self._ref_format = {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[self._ref_size]
+ self._object_offsets = struct.unpack(offset_format, self._fp.read(offset_size * num_objects))
+ return self._read_object(self._object_offsets[top_object])
-def readPlistFromBytes(data):
- """Read a plist data from a bytes object. Return the root object.
- """
- return readPlist(BytesIO(data))
+ def _get_size(self, tokenL):
+ """ return the size of the next object."""
+ if tokenL == 0xF:
+ m = int.from_bytes(self._fp.read(1), byteorder='big') & 0x3
+ # {log2(bit_number): (bit_number, pattern to use with struct.unpack)}
+ int_format = {0: (1, '>B'), 1: (2, '>H'), 2: (4, '>L'), 3: (8, '>Q'), }
+ s, f = int_format[m]
+ return struct.unpack(f, self._fp.read(s))[0]
+ return tokenL
-def writePlistToBytes(rootObject):
- """Return 'rootObject' as a plist-formatted bytes object.
- """
- f = BytesIO()
- writePlist(rootObject, f)
- return f.getvalue()
+ def _read_refs(self, n):
+ return struct.unpack('>' + self._ref_format * n, self._fp.read(n * self._ref_size))
+ def _read_object(self, offset):
+ """ read the object at offset. May recursively read sub-objects (content of an array/dict/set) """
+ self._fp.seek(offset)
+ token = self._fp.read(1)[0]
+ tokenH, tokenL = token & 0xF0, token & 0x0F #high and low parts
-class DumbXMLWriter:
+ if token == 0x00:
+ return None
+
+ elif token == 0x08:
+ return False
+
+ elif token == 0x09:
+ return True
+
+ # The referenced source code also mentions URL (0x0c, 0x0d) and UUID (0x0e),
+ # but neither can be generated using the Cocoa libraries.
+
+ elif token == 0x0f:
+ return b''
+
+ elif tokenH == 0x10: #int
+ return int.from_bytes(self._fp.read(1 << tokenL), 'big')
+
+ elif tokenH == 0x20 and tokenL == 0x02: #real
+ return struct.unpack('>f', self._fp.read(4))[0]
+
+ elif tokenH == 0x20 and tokenL == 0x03: #real
+ return struct.unpack('>d', self._fp.read(8))[0]
+
+ elif tokenH == 0x30 and tokenL == 0x03: #date
+ f = struct.unpack('>d', self._fp.read(8))[0]
+ # timestamp 0 of binary plists corresponds to 1/1/2001 (year of Mac OS X 10.0), instead of 1/1/1970.
+ return datetime.datetime.utcfromtimestamp(f + (31 * 365 + 8) * 86400)
+
+ elif tokenH == 0x40: #data
+ s = self._get_size(tokenL)
+ if self._bytes_for_data:
+ return self._fp.read(s)
+ else:
+ return Data(self._fp.read(s))
+
+ elif tokenH == 0x50: #ascii string
+ s = self._get_size(tokenL)
+ result = self._fp.read(s).decode('ascii')
+ return result
+
+ elif tokenH == 0x60: #unicode string
+ s = self._get_size(tokenL)
+ return self._fp.read(s * 2).decode('utf-16be')
+
+ # tokenH == 0x80 is documented as 'UID' and appears to be used for keyed-archiving,
+ # not in plists.
+ elif tokenH == 0xA0: #array
+ s = self._get_size(tokenL)
+ obj_refs = self._read_refs(s)
+ return [self._read_object(self._object_offsets[x]) for x in obj_refs]
+
+ # tokenH == 0xB0 is documented as 'ordset', but is not actually implemented
+ # in the Apple reference code.
+
+ # tokenH == 0xC0 is documented as 'set', but sets cannot be used in
+ # plists.
+
+ elif tokenH == 0xD0: #dict
+ s = self._get_size(tokenL)
+ key_refs = self._read_refs(s)
+ obj_refs = self._read_refs(s)
+ result = self._dict_type()
+ for k, o in zip(key_refs, obj_refs):
+ result[self._read_object(self._object_offsets[k])] = self._read_object(self._object_offsets[o])
+ return result
+
+ raise InvalidFileException()
+
+class _BinaryPlistWriter (object):
+ def __init__(self, fp, sort_keys, skipkeys):
+ self._fp = fp
+ self._sort_keys = sort_keys
+ self._skipkeys = skipkeys
+
+ def write(self, value):
+
+ # Flattened object list:
+ self._objlist = []
+
+ # Mappings from object->objectid
+ # First dict has (type(object), object) as the key,
+ # second dict is used when object is not hashable and
+ # has id(object) as the key.
+ self._objtable = {}
+ self._objidtable = {}
+
+ # Create list of all objects in the plist
+ self._flatten(value)
+
+ # Size of object references in serialized containers
+ # depends on the number of objects in the plist.
+ num_objects = len(self._objlist)
+ self._object_offsets = [0]*num_objects
+ if num_objects < 1 << 8:
+ self._ref_size = 1
+ elif num_objects < 1 << 16:
+ self._ref_size = 2
+ elif num_objects < 1 << 32:
+ self._ref_size = 4
+ else:
+ self._ref_size = 8
+
+ self._ref_format = {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[self._ref_size]
+
+ # Write file header
+ self._fp.write(b'bplist00')
+
+ # Write object list
+ for obj in self._objlist:
+ self._write_object(obj)
+
+ # Write refnum->object offset table
+ top_object = self._getrefnum(value)
+ offset_table_offset = self._fp.tell()
+ if offset_table_offset < 1 << 8:
+ offset_size = 1
+ elif offset_table_offset < 1 << 16:
+ offset_size = 2
+ elif offset_table_offset < 1 << 32:
+ offset_size = 4
+ else:
+ offset_size = 8
+
+ offset_format = '>' + {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[offset_size] * num_objects
+ self._fp.write(struct.pack(offset_format, *self._object_offsets))
+
+ # Write trailer
+ sortVersion = 0
+ trailer = sortVersion, offset_size, self._ref_size, num_objects, top_object, offset_table_offset
+ self._fp.write(struct.pack('>5xBBBQQQ', *trailer))
+
+ def _flatten(self, value):
+ # First check if the object is in the object table, not used for
+ # containers to ensure that two subcontainers with the same contents
+ # will be serialized as distinct values.
+ if isinstance(value, (str, int, float, datetime.datetime, bytes)):
+ if (type(value), value) in self._objtable:
+ return
+
+ elif isinstance(value, Data):
+ if (type(value.data), value.data) in self._objtable:
+ return
+
+ # Add to objectreference map
+ refnum = len(self._objlist)
+ self._objlist.append(value)
+ try:
+ if isinstance(value, Data):
+ self._objtable[(type(value.data), value.data)] = refnum
+ else:
+ self._objtable[(type(value), value)] = refnum
+ except TypeError:
+ self._objidtable[id(value)] = refnum
+
+ # And finally recurse into containers
+ if isinstance(value, dict):
+ keys = []
+ values = []
+ if self._sort_keys:
+ for k, v in sorted(value.items()):
+ if self._skipkeys and not isinstance(k, str):
+ continue
+ keys.append(k)
+ values.append(v)
+ else:
+ for k, v in value.items():
+ if self._skipkeys and not isinstance(k, str):
+ continue
+ keys.append(k)
+ values.append(v)
+
+ for o in itertools.chain(keys, values):
+ self._flatten(o)
+
+ elif isinstance(value, (list, tuple)):
+ for o in value:
+ self._flatten(o)
+
+ def _getrefnum(self, value):
+ try:
+ if isinstance(value, Data):
+ return self._objtable[(type(value.data), value.data)]
+ else:
+ return self._objtable[(type(value), value)]
+ except TypeError:
+ return self._objidtable[id(value)]
+
+ def _write_size(self, token, size):
+ if size < 15:
+ self._fp.write(struct.pack('>B', token | size))
+ elif size < 1 << 8:
+ self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))
+ elif size < 1 << 16:
+ self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))
+ elif size < 1 << 32:
+ self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))
+ else:
+ self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))
+
+ def _write_object(self, value):
+ ref = self._getrefnum(value)
+ self._object_offsets[ref] = self._fp.tell()
+ if value == None:
+ self._fp.write(b'\x00')
+
+ elif isinstance(value, bool) and value == False:
+ self._fp.write(b'\x08')
+
+ elif isinstance(value, bool) and value == True:
+ self._fp.write(b'\x09')
+
+ elif isinstance(value, int):
+ if value < 1 << 8:
+ self._fp.write(struct.pack('>BB', 0x10, value))
+ elif value < 1 << 16:
+ self._fp.write(struct.pack('>BH', 0x11, value))
+ elif value < 1 << 32:
+ self._fp.write(struct.pack('>BL', 0x12, value))
+ else:
+ self._fp.write(struct.pack('>BQ', 0x13, value))
+
+ elif isinstance(value, float):
+ self._fp.write(struct.pack('>Bd', 0x23, value))
+
+ elif isinstance(value, datetime.datetime):
+ f = (value - datetime.datetime(2001, 1, 1)).total_seconds()
+ self._fp.write(struct.pack('>Bd', 0x33, f))
+
+ elif isinstance(value, Data):
+ self._write_size(0x40, len(value.data))
+ self._fp.write(value.data)
+
+ elif isinstance(value, bytes):
+ self._write_size(0x40, len(value))
+ self._fp.write(value)
+
+ elif isinstance(value, str):
+ try:
+ t = value.encode('ascii')
+ self._write_size(0x50, len(value))
+ except UnicodeEncodeError:
+ t = value.encode('utf-16be')
+ self._write_size(0x60, len(value))
+ self._fp.write(t)
+
+ elif isinstance(value, list) or isinstance(value, tuple):
+ refs = [ self._getrefnum(o) for o in value ]
+ s = len(refs)
+ self._write_size(0xA0, s)
+ self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
+
+ elif isinstance(value, dict):
+ keyRefs, valRefs = [], []
+
+ before = (self._fp.tell() == 8)
+
+ if self._sort_keys:
+ rootItems = sorted(value.items())
+ else:
+ rootItems = value.items()
+
+ for k, v in rootItems:
+ if not isinstance(k, str):
+ if self._skipkeys:
+ continue
+ raise TypeError("keys must be strings")
+ keyRefs.append(self._getrefnum(k))
+ valRefs.append(self._getrefnum(v))
+
+ s = len(keyRefs)
+ self._write_size(0xD0, s)
+ self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
+ self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
+
+ else:
+ raise InvalidFileException()
+
+class _DumbXMLWriter:
def __init__(self, file, indentLevel=0, indent="\t"):
self.file = file
self.stack = []
@@ -146,59 +571,19 @@
self.file.write(line)
self.file.write(b'\n')
+class _PlistWriter(_DumbXMLWriter):
-# Contents should conform to a subset of ISO 8601
-# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with
-# a loss of precision)
-_dateParser = re.compile(r"(?P\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z", re.ASCII)
-
-def _dateFromString(s):
- order = ('year', 'month', 'day', 'hour', 'minute', 'second')
- gd = _dateParser.match(s).groupdict()
- lst = []
- for key in order:
- val = gd[key]
- if val is None:
- break
- lst.append(int(val))
- return datetime.datetime(*lst)
-
-def _dateToString(d):
- return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
- d.year, d.month, d.day,
- d.hour, d.minute, d.second
- )
-
-
-# Regex to find any control chars, except for \t \n and \r
-_controlCharPat = re.compile(
- r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
- r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
-
-def _escape(text):
- m = _controlCharPat.search(text)
- if m is not None:
- raise ValueError("strings can't contains control characters; "
- "use plistlib.Data instead")
- text = text.replace("\r\n", "\n") # convert DOS line endings
- text = text.replace("\r", "\n") # convert Mac line endings
- text = text.replace("&", "&") # escape '&'
- text = text.replace("<", "<") # escape '<'
- text = text.replace(">", ">") # escape '>'
- return text
-
-
-PLISTHEADER = b"""\
-
-
-"""
-
-class PlistWriter(DumbXMLWriter):
-
- def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1):
+ def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1, sort_keys=True, skipkeys=False):
if writeHeader:
file.write(PLISTHEADER)
- DumbXMLWriter.__init__(self, file, indentLevel, indent)
+ _DumbXMLWriter.__init__(self, file, indentLevel, indent)
+ self._sort_keys = sort_keys
+ self._skipkeys = skipkeys
+
+ def write(self, value):
+ self.writeln("")
+ self.writeValue(value)
+ self.writeln("")
def writeValue(self, value):
if isinstance(value, str):
@@ -218,19 +603,24 @@
self.writeDict(value)
elif isinstance(value, Data):
self.writeData(value)
+ elif isinstance(value, bytes):
+ self.writeBytes(value)
elif isinstance(value, datetime.datetime):
- self.simpleElement("date", _dateToString(value))
+ self.simpleElement("date", _date_to_string(value))
elif isinstance(value, (tuple, list)):
self.writeArray(value)
else:
raise TypeError("unsupported type: %s" % type(value))
def writeData(self, data):
+ self.writeBytes(data.data)
+
+ def writeBytes(self, data):
self.beginElement("data")
self.indentLevel -= 1
maxlinelength = max(16, 76 - len(self.indent.replace(b"\t", b" " * 8) *
self.indentLevel))
- for line in data.asBase64(maxlinelength).split(b"\n"):
+ for line in _encode_base64(data, maxlinelength).split(b"\n"):
if line:
self.writeln(line)
self.indentLevel += 1
@@ -239,9 +629,15 @@
def writeDict(self, d):
if d:
self.beginElement("dict")
- items = sorted(d.items())
+ if self._sort_keys:
+ items = sorted(d.items())
+ else:
+ items = d.items()
+
for key, value in items:
if not isinstance(key, str):
+ if self._skipkeys:
+ continue
raise TypeError("keys must be strings")
self.simpleElement("key", key)
self.writeValue(value)
@@ -258,37 +654,149 @@
else:
self.simpleElement("array")
+def _is_fmt_xml(header):
+ header = header[:7]
+ for bom in b'', codecs.BOM_UTF8, codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE, codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE:
+ for start in b'', b'\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z", re.ASCII)
+
+def _date_from_string(s):
+ order = ('year', 'month', 'day', 'hour', 'minute', 'second')
+ gd = _dateParser.match(s).groupdict()
+ lst = []
+ for key in order:
+ val = gd[key]
+ if val is None:
+ break
+ lst.append(int(val))
+ return datetime.datetime(*lst)
+
+def _date_to_string(d):
+ return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
+ d.year, d.month, d.day,
+ d.hour, d.minute, d.second
+ )
+
+
+# Regex to find any control chars, except for \t \n and \r
+_controlCharPat = re.compile(
+ r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
+ r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
+
+def _escape(text):
+ m = _controlCharPat.search(text)
+ if m is not None:
+ raise ValueError("strings can't contains control characters; "
+ "use plistlib.Data instead")
+ text = text.replace("\r\n", "\n") # convert DOS line endings
+ text = text.replace("\r", "\n") # convert Mac line endings
+ text = text.replace("&", "&") # escape '&'
+ text = text.replace("<", "<") # escape '<'
+ text = text.replace(">", ">") # escape '>'
+ return text
+
+
+PLISTHEADER = b"""\
+
+
+"""
+
+
+
class Dict(_InternalDict):
@@ -313,9 +821,9 @@
def fromFile(cls, pathOrFile):
"""Deprecated. Use the readPlist() function instead."""
- rootObject = readPlist(pathOrFile)
+ value = readPlist(pathOrFile)
plist = cls()
- plist.update(rootObject)
+ plist.update(value)
return plist
fromFile = classmethod(fromFile)
@@ -324,7 +832,7 @@
writePlist(self, pathOrFile)
-def _encodeBase64(s, maxlinelength=76):
+def _encode_base64(s, maxlinelength=76):
# copied from base64.encodebytes(), with added maxlinelength argument
maxbinsize = (maxlinelength//4)*3
pieces = []
@@ -349,7 +857,7 @@
return cls(binascii.a2b_base64(data))
def asBase64(self, maxlinelength=76):
- return _encodeBase64(self.data, maxlinelength)
+ return _encode_base64(self.data, maxlinelength)
def __eq__(self, other):
if isinstance(other, self.__class__):
@@ -362,93 +870,24 @@
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
-class PlistParser:
- def __init__(self):
- self.stack = []
- self.currentKey = None
- self.root = None
+# simular interface as the pickle and json modules:
- def parse(self, fileobj):
- from xml.parsers.expat import ParserCreate
- self.parser = ParserCreate()
- self.parser.StartElementHandler = self.handleBeginElement
- self.parser.EndElementHandler = self.handleEndElement
- self.parser.CharacterDataHandler = self.handleData
- self.parser.ParseFile(fileobj)
- return self.root
+def dump(value, fp, *, skipkeys=False, sort_keys=True):
+ if not hasattr(fp, 'write'):
+ raise TypeError("fp must be a file-like object")
- def handleBeginElement(self, element, attrs):
- self.data = []
- handler = getattr(self, "begin_" + element, None)
- if handler is not None:
- handler(attrs)
+ writePlist(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys)
- def handleEndElement(self, element):
- handler = getattr(self, "end_" + element, None)
- if handler is not None:
- handler()
+def dumps(value, *, skipkeys=False, sort_keys=True):
+ fp = BytesIO()
+ return dump(value, fp, skipkeys=skipkeys, sort_keys=sort_keys)
- def handleData(self, data):
- self.data.append(data)
+def load(fp, *, fmt=None, dict_type=dict):
+ if not hasattr(fp, 'read'):
+ raise TypeError("fp must be a file-like object")
+ readPlist(fp, fmt=fmt, data_as_bytes=True, dict_type=dict_type)
- def addObject(self, value):
- if self.currentKey is not None:
- if not isinstance(self.stack[-1], type({})):
- raise ValueError("unexpected element at line %d" %
- self.parser.CurrentLineNumber)
- self.stack[-1][self.currentKey] = value
- self.currentKey = None
- elif not self.stack:
- # this is the root object
- self.root = value
- else:
- if not isinstance(self.stack[-1], type([])):
- raise ValueError("unexpected element at line %d" %
- self.parser.CurrentLineNumber)
- self.stack[-1].append(value)
-
- def getData(self):
- data = ''.join(self.data)
- self.data = []
- return data
-
- # element handlers
-
- def begin_dict(self, attrs):
- d = _InternalDict()
- self.addObject(d)
- self.stack.append(d)
- def end_dict(self):
- if self.currentKey:
- raise ValueError("missing value for key '%s' at line %d" %
- (self.currentKey,self.parser.CurrentLineNumber))
- self.stack.pop()
-
- def end_key(self):
- if self.currentKey or not isinstance(self.stack[-1], type({})):
- raise ValueError("unexpected key at line %d" %
- self.parser.CurrentLineNumber)
- self.currentKey = self.getData()
-
- def begin_array(self, attrs):
- a = []
- self.addObject(a)
- self.stack.append(a)
- def end_array(self):
- self.stack.pop()
-
- def end_true(self):
- self.addObject(True)
- def end_false(self):
- self.addObject(False)
- def end_integer(self):
- self.addObject(int(self.getData()))
- def end_real(self):
- self.addObject(float(self.getData()))
- def end_string(self):
- self.addObject(self.getData())
- def end_data(self):
- self.addObject(Data.fromBase64(self.getData().encode("utf-8")))
- def end_date(self):
- self.addObject(_dateFromString(self.getData()))
+def loads(value, *, fmt=None):
+ fp = BytesIO(value)
+ return load(fp, fmt=fmt)
diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
--- a/Lib/test/test_plistlib.py
+++ b/Lib/test/test_plistlib.py
@@ -4,92 +4,19 @@
import plistlib
import os
import datetime
+import collections
from test import support
+from io import BytesIO
-# This test data was generated through Cocoa's NSDictionary class
-TESTDATA = b"""
-
-
-
- aDate
- 2004-10-26T10:33:33Z
- aDict
-
- aFalseValue
-
- aTrueValue
-
- aUnicodeValue
- M\xc3\xa4ssig, Ma\xc3\x9f
- anotherString
- <hello & 'hi' there!>
- deeperDict
-
- a
- 17
- b
- 32.5
- c
-
- 1
- 2
- text
-
-
-
- aFloat
- 0.5
- aList
-
- A
- B
- 12
- 32.5
-
- 1
- 2
- 3
-
-
- aString
- Doodah
- anEmptyDict
-
- anEmptyList
-
- anInt
- 728
- nestedData
-
-
- PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r
- PgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5
- IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi
- aW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz
- IG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID
- PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==
-
-
- someData
-
- PGJpbmFyeSBndW5rPg==
-
- someMoreData
-
- PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8
- bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs
- b3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv
- dHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90
- cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==
-
- \xc3\x85benraa
- That was a unicode key.
-
-
-""".replace(b" " * 8, b"\t") # Apple as well as plistlib.py output hard tabs
+ALL_FORMATS=(plistlib.FMT_XML, plistlib.FMT_BINARY)
+# 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: b'\n\n\n\n\taDate\n\t2004-10-26T10:33:33Z\n\taDict\n\t\n\t\taFalseValue\n\t\t\n\t\taTrueValue\n\t\t\n\t\taUnicodeValue\n\t\tM\xc3\xa4ssig, Ma\xc3\x9f\n\t\tanotherString\n\t\t<hello & \'hi\' there!>\n\t\tdeeperDict\n\t\t\n\t\t\ta\n\t\t\t17\n\t\t\tb\n\t\t\t32.5\n\t\t\tc\n\t\t\t\n\t\t\t\t1\n\t\t\t\t2\n\t\t\t\ttext\n\t\t\t\n\t\t\n\t\n\taFloat\n\t0.5\n\taList\n\t\n\t\tA\n\t\tB\n\t\t12\n\t\t32.5\n\t\t\n\t\t\t1\n\t\t\t2\n\t\t\t3\n\t\t\n\t\n\taString\n\tDoodah\n\tanEmptyDict\n\t\n\tanEmptyList\n\t\n\tanInt\n\t728\n\tnestedData\n\t\n\t\t\n\t\tPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r\n\t\tPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5\n\t\tIGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi\n\t\taW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz\n\t\tIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID\n\t\tPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==\n\t\t\n\t\n\tsomeData\n\t\n\tPGJpbmFyeSBndW5rPg==\n\t\n\tsomeMoreData\n\t\n\tPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8\n\tbG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs\n\tb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv\n\tdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90\n\tcyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==\n\t\n\t\xc3\x85benraa\n\tThat was a unicode key.\n\n\n',
+ plistlib.FMT_BINARY: b'bplist00\xdc\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e"#)*+,-/.0UaDateUaDictVaFloatUaListWaString[anEmptyDict[anEmptyListUanIntZnestedDataXsomeData\\someMoreDatag\x00\xc5\x00b\x00e\x00n\x00r\x00a\x00a3A\x9c\xb9}\xf4\x00\x00\x00\xd5\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18[aFalseValueZaTrueValue]aUnicodeValue]anotherStringZdeeperDict\x08\tk\x00M\x00\xe4\x00s\x00s\x00i\x00g\x00,\x00 \x00M\x00a\x00\xdf_\x10\x15\xd3\x19\x1a\x1b\x1c\x1d\x1eQaQbQc\x10\x11#@@@\x00\x00\x00\x00\x00\xa3\x1f !\x10\x01\x10\x02Ttext#?\xe0\x00\x00\x00\x00\x00\x00\xa5$%&\x1d\'QAQB\x10\x0c\xa3\x1f (\x10\x03VDoodah\xd0\xa0\x11\x02\xd8\xa1.O\x10\xfa\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03M_\x10\x17That was a unicode key.\x00\x08\x00!\x00\'\x00-\x004\x00:\x00B\x00N\x00Z\x00`\x00k\x00t\x00\x81\x00\x90\x00\x99\x00\xa4\x00\xb0\x00\xbb\x00\xc9\x00\xd7\x00\xe2\x00\xe3\x00\xe4\x00\xfb\x01\x13\x01\x1a\x01\x1c\x01\x1e\x01 \x01"\x01+\x01/\x011\x013\x018\x01A\x01G\x01I\x01K\x01M\x01Q\x01S\x01Z\x01[\x01\\\x01_\x01a\x02^\x02l\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x86',
+}
class TestPlistlib(unittest.TestCase):
@@ -99,7 +26,7 @@
except:
pass
- def _create(self):
+ def _create(self, fmt=None):
pl = dict(
aString="Doodah",
aList=["A", "B", 12, 32.5, [1, 2, 3]],
@@ -154,24 +81,149 @@
self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(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 bytes_for_data in (True, False):
+ for fmt in ALL_FORMATS:
+ with self.subTest(fmt=fmt, bytes_for_data=bytes_for_data):
+ pl = plistlib.readPlistFromBytes(TESTDATA[fmt], bytes_for_data=bytes_for_data)
+ data = plistlib.writePlistToBytes(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.readPlistFromBytes(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.writePlist(pl, b, fmt=fmt)
+ pl2 = plistlib.readPlist(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.writePlist(pl, b, fmt=fmt, sort_keys=sort_keys)
+ pl2 = plistlib.readPlist(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.writePlistToBytes(pl, fmt=fmt, sort_keys=sort_keys)
+ pl2 = plistlib.readPlistFromBytes(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.writePlistToBytes, pl, fmt=fmt)
+
+ b = BytesIO()
+ self.assertRaises(TypeError, plistlib.writePlist, 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.writePlistToBytes(pl, fmt=fmt, skipkeys=True, sort_keys=False)
+ pl2 = plistlib.readPlistFromBytes(data)
+ self.assertEqual(pl2, {'snake': 'aWord'})
+
+ fp = BytesIO()
+ data = plistlib.writePlist(pl, fp, fmt=fmt, skipkeys=True, sort_keys=False)
+ pl2 = plistlib.readPlistFromBytes(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.writePlistToBytes(pl, fmt=fmt)
+ pl2 = plistlib.readPlistFromBytes(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.writePlistToBytes(pl, fmt=fmt)
+ pl2 = plistlib.readPlistFromBytes(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.writePlistToBytes(pl, fmt=fmt)
+ pl2 = plistlib.readPlistFromBytes(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,19 +231,21 @@
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.writePlistToBytes(testString, fmt=plistlib.FMT_XML)
else:
self.assertRaises(ValueError,
plistlib.writePlistToBytes,
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.readPlistFromBytes(plistlib.writePlistToBytes(test1, fmt=fmt))
+ result2 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test2, fmt=fmt))
+ self.assertEqual(test1, result1)
+ self.assertEqual(test2, result2)
def test_invalidarray(self):
for i in ["key inside an array",