r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
The property list (.plist) file format is a simple XML pickle supporting
basic object types, like dictionaries, lists, numbers and strings.
Usually the top level object is a dictionary.
To write out a plist file, use the dump(value, file)
function. 'value' is the top level object, 'file' is
a (writable) file object.
To parse a plist from a file, use the load(file) function,
with a (readable) file object as the only argument. It
returns the top level object (again, usually a dictionary).
To work with plist data in bytes objects, you can use loads()
Values can be strings, integers, floats, booleans, tuples, lists,
dictionaries (but only with string keys), Data, bytes, bytearray, or
datetime.datetime objects.
aList = ["A", "B", 12, 32.1, [1, 2, 3]],
anotherString = "<hello & hi there!>",
aUnicodeValue = "M\xe4ssig, Ma\xdf",
someData = b"<binary gunk>",
someMoreData = b"<lots of binary gunk>" * 10,
aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
with open(fileName, 'wb') as fp:
with open(fileName, 'rb') as fp:
"readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
"Plist", "Data", "Dict", "InvalidFileException", "FMT_XML", "FMT_BINARY",
"load", "dump", "loads", "dumps"
from warnings import warn
from xml.parsers.expat import ParserCreate
PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
globals().update(PlistFormat.__members__)
# Deprecated functionality
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):
raise AttributeError(attr)
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", DeprecationWarning, 2)
def __setattr__(self, attr, value):
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", DeprecationWarning, 2)
def __delattr__(self, attr):
raise AttributeError(attr)
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", DeprecationWarning, 2)
class Dict(_InternalDict):
def __init__(self, **kwargs):
warn("The plistlib.Dict class is deprecated, use builtin dict instead",
super().__init__(**kwargs)
@contextlib.contextmanager
def _maybe_open(pathOrFile, mode):
if isinstance(pathOrFile, str):
with open(pathOrFile, mode) as fp:
class Plist(_InternalDict):
"""This class has been deprecated. Use dump() and load()
functions instead, together with regular dict objects.
def __init__(self, **kwargs):
warn("The Plist class is deprecated, use the load() and "
"dump() functions instead", DeprecationWarning, 2)
super().__init__(**kwargs)
def fromFile(cls, pathOrFile):
"""Deprecated. Use the load() function instead."""
with _maybe_open(pathOrFile, 'rb') as fp:
def write(self, pathOrFile):
"""Deprecated. Use the dump() function instead."""
with _maybe_open(pathOrFile, 'wb') as fp:
def readPlist(pathOrFile):
Read a .plist from a path or file. pathOrFile should either
be a file name, or a readable binary file object.
This function is deprecated, use load instead.
warn("The readPlist function is deprecated, use load() instead",
with _maybe_open(pathOrFile, 'rb') as fp:
return load(fp, fmt=None, use_builtin_types=False,
def writePlist(value, pathOrFile):
Write 'value' to a .plist file. 'pathOrFile' may either be a
file name or a (writable) file object.
This function is deprecated, use dump instead.
warn("The writePlist function is deprecated, use dump() instead",
with _maybe_open(pathOrFile, 'wb') as fp:
dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False)
def readPlistFromBytes(data):
Read a plist data from a bytes object. Return the root object.
This function is deprecated, use loads instead.
warn("The readPlistFromBytes function is deprecated, use loads() instead",
return load(BytesIO(data), fmt=None, use_builtin_types=False,
def writePlistToBytes(value):
Return 'value' as a plist-formatted bytes object.
This function is deprecated, use dumps instead.
warn("The writePlistToBytes function is deprecated, use dumps() instead",
dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
This class is deprecated, use a bytes object instead.
def __init__(self, data):
if not isinstance(data, bytes):
raise TypeError("data must be as bytes")
def fromBase64(cls, data):
# base64.decodebytes just calls binascii.a2b_base64;
# it seems overkill to use both base64 and binascii.
return cls(_decode_base64(data))
def asBase64(self, maxlinelength=76):
return _encode_base64(self.data, maxlinelength)
if isinstance(other, self.__class__):
return self.data == other.data
elif isinstance(other, bytes):
return self.data == other
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
# End of deprecated functionality
<?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">
# 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 _encode_base64(s, maxlinelength=76):
# copied from base64.encodebytes(), with added maxlinelength argument
maxbinsize = (maxlinelength//4)*3
for i in range(0, len(s), maxbinsize):
chunk = s[i : i + maxbinsize]
pieces.append(binascii.b2a_base64(chunk))
return binascii.a2b_base64(s.encode("utf-8"))
return binascii.a2b_base64(s)
# 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<year>\d\d\d\d)(?:-(?P<month>\d\d)(?:-(?P<day>\d\d)(?:T(?P<hour>\d\d)(?::(?P<minute>\d\d)(?::(?P<second>\d\d))?)?)?)?)?Z", re.ASCII)
def _date_from_string(s):
order = ('year', 'month', 'day', 'hour', 'minute', 'second')
gd = _dateParser.match(s).groupdict()
return datetime.datetime(*lst)
return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
d.hour, d.minute, d.second
m = _controlCharPat.search(text)
raise ValueError("strings can't contains control characters; "
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 '>'
def __init__(self, use_builtin_types, dict_type):
self._use_builtin_types = use_builtin_types
self._dict_type = dict_type
def parse(self, fileobj):
self.parser = ParserCreate()
self.parser.StartElementHandler = self.handle_begin_element
self.parser.EndElementHandler = self.handle_end_element
self.parser.CharacterDataHandler = self.handle_data
self.parser.ParseFile(fileobj)
def handle_begin_element(self, element, attrs):
handler = getattr(self, "begin_" + element, None)
def handle_end_element(self, element):
handler = getattr(self, "end_" + element, None)
def handle_data(self, data):
def add_object(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
# this is the root object
if not isinstance(self.stack[-1], type([])):
raise ValueError("unexpected element at line %d" %
self.parser.CurrentLineNumber)
self.stack[-1].append(value)
data = ''.join(self.data)
def begin_dict(self, attrs):
raise ValueError("missing value for key '%s' at line %d" %
(self.current_key,self.parser.CurrentLineNumber))
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.get_data()
def begin_array(self, attrs):
self.add_object(int(self.get_data()))
self.add_object(float(self.get_data()))
self.add_object(self.get_data())
if self._use_builtin_types:
self.add_object(_decode_base64(self.get_data()))
self.add_object(Data.fromBase64(self.get_data()))
self.add_object(_date_from_string(self.get_data()))
def __init__(self, file, indent_level=0, indent="\t"):
self._indent_level = indent_level
def begin_element(self, element):
self.stack.append(element)
self.writeln("<%s>" % element)
def end_element(self, element):
assert self._indent_level > 0
assert self.stack.pop() == element
self.writeln("</%s>" % element)
def simple_element(self, element, value=None):
self.writeln("<%s>%s</%s>" % (element, value, element))
self.writeln("<%s/>" % element)
# plist has fixed encoding of utf-8
# XXX: is this test needed?
if isinstance(line, str):
line = line.encode('utf-8')
self.file.write(self._indent_level * self.indent)
class _PlistWriter(_DumbXMLWriter):
self, file, indent_level=0, indent=b"\t", writeHeader=1,
sort_keys=True, skipkeys=False):
_DumbXMLWriter.__init__(self, file, indent_level, indent)
self._sort_keys = sort_keys
self._skipkeys = skipkeys
self.writeln("<plist version=\"1.0\">")
def write_value(self, value):
if isinstance(value, str):
self.simple_element("string", value)
self.simple_element("true")
self.simple_element("false")
elif isinstance(value, int):
if -1 << 63 <= value < 1 << 64:
self.simple_element("integer", "%d" % value)
raise OverflowError(value)
elif isinstance(value, float):
self.simple_element("real", repr(value))
elif isinstance(value, dict):
elif isinstance(value, Data):
elif isinstance(value, (bytes, bytearray)):
elif isinstance(value, datetime.datetime):
self.simple_element("date", _date_to_string(value))