Enable doctest running for several other documents.
[python.git] / Lib / plistlib.py
blob0ce533333430d1141951d6a215a0991ab3f684d9
1 """plistlib.py -- a tool to generate and parse MacOSX .plist files.
3 The PropertyList (.plist) file format is a simple XML pickle supporting
4 basic object types, like dictionaries, lists, numbers and strings.
5 Usually the top level object is a dictionary.
7 To write out a plist file, use the writePlist(rootObject, pathOrFile)
8 function. 'rootObject' is the top level object, 'pathOrFile' is a
9 filename or a (writable) file object.
11 To parse a plist from a file, use the readPlist(pathOrFile) function,
12 with a file name or a (readable) file object as the only argument. It
13 returns the top level object (again, usually a dictionary).
15 To work with plist data in strings, you can use readPlistFromString()
16 and writePlistToString().
18 Values can be strings, integers, floats, booleans, tuples, lists,
19 dictionaries, Data or datetime.datetime objects. String values (including
20 dictionary keys) may be unicode strings -- they will be written out as
21 UTF-8.
23 The <data> plist type is supported through the Data class. This is a
24 thin wrapper around a Python string.
26 Generate Plist example:
28 pl = dict(
29 aString="Doodah",
30 aList=["A", "B", 12, 32.1, [1, 2, 3]],
31 aFloat=0.1,
32 anInt=728,
33 aDict=dict(
34 anotherString="<hello & hi there!>",
35 aUnicodeValue=u'M\xe4ssig, Ma\xdf',
36 aTrueValue=True,
37 aFalseValue=False,
39 someData=Data("<binary gunk>"),
40 someMoreData=Data("<lots of binary gunk>" * 10),
41 aDate=datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
43 # unicode keys are possible, but a little awkward to use:
44 pl[u'\xc5benraa'] = "That was a unicode key."
45 writePlist(pl, fileName)
47 Parse Plist example:
49 pl = readPlist(pathOrFile)
50 print pl["aKey"]
51 """
54 __all__ = [
55 "readPlist", "writePlist", "readPlistFromString", "writePlistToString",
56 "readPlistFromResource", "writePlistToResource",
57 "Plist", "Data", "Dict"
59 # Note: the Plist and Dict classes have been deprecated.
61 import binascii
62 import datetime
63 from cStringIO import StringIO
64 import re
67 def readPlist(pathOrFile):
68 """Read a .plist file. 'pathOrFile' may either be a file name or a
69 (readable) file object. Return the unpacked root object (which
70 usually is a dictionary).
71 """
72 didOpen = 0
73 if isinstance(pathOrFile, (str, unicode)):
74 pathOrFile = open(pathOrFile)
75 didOpen = 1
76 p = PlistParser()
77 rootObject = p.parse(pathOrFile)
78 if didOpen:
79 pathOrFile.close()
80 return rootObject
83 def writePlist(rootObject, pathOrFile):
84 """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a
85 file name or a (writable) file object.
86 """
87 didOpen = 0
88 if isinstance(pathOrFile, (str, unicode)):
89 pathOrFile = open(pathOrFile, "w")
90 didOpen = 1
91 writer = PlistWriter(pathOrFile)
92 writer.writeln("<plist version=\"1.0\">")
93 writer.writeValue(rootObject)
94 writer.writeln("</plist>")
95 if didOpen:
96 pathOrFile.close()
99 def readPlistFromString(data):
100 """Read a plist data from a string. Return the root object.
102 return readPlist(StringIO(data))
105 def writePlistToString(rootObject):
106 """Return 'rootObject' as a plist-formatted string.
108 f = StringIO()
109 writePlist(rootObject, f)
110 return f.getvalue()
113 def readPlistFromResource(path, restype='plst', resid=0):
114 """Read plst resource from the resource fork of path.
116 from Carbon.File import FSRef, FSGetResourceForkName
117 from Carbon.Files import fsRdPerm
118 from Carbon import Res
119 fsRef = FSRef(path)
120 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdPerm)
121 Res.UseResFile(resNum)
122 plistData = Res.Get1Resource(restype, resid).data
123 Res.CloseResFile(resNum)
124 return readPlistFromString(plistData)
127 def writePlistToResource(rootObject, path, restype='plst', resid=0):
128 """Write 'rootObject' as a plst resource to the resource fork of path.
130 from Carbon.File import FSRef, FSGetResourceForkName
131 from Carbon.Files import fsRdWrPerm
132 from Carbon import Res
133 plistData = writePlistToString(rootObject)
134 fsRef = FSRef(path)
135 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdWrPerm)
136 Res.UseResFile(resNum)
137 try:
138 Res.Get1Resource(restype, resid).RemoveResource()
139 except Res.Error:
140 pass
141 res = Res.Resource(plistData)
142 res.AddResource(restype, resid, '')
143 res.WriteResource()
144 Res.CloseResFile(resNum)
147 class DumbXMLWriter:
149 def __init__(self, file, indentLevel=0, indent="\t"):
150 self.file = file
151 self.stack = []
152 self.indentLevel = indentLevel
153 self.indent = indent
155 def beginElement(self, element):
156 self.stack.append(element)
157 self.writeln("<%s>" % element)
158 self.indentLevel += 1
160 def endElement(self, element):
161 assert self.indentLevel > 0
162 assert self.stack.pop() == element
163 self.indentLevel -= 1
164 self.writeln("</%s>" % element)
166 def simpleElement(self, element, value=None):
167 if value is not None:
168 value = _escapeAndEncode(value)
169 self.writeln("<%s>%s</%s>" % (element, value, element))
170 else:
171 self.writeln("<%s/>" % element)
173 def writeln(self, line):
174 if line:
175 self.file.write(self.indentLevel * self.indent + line + "\n")
176 else:
177 self.file.write("\n")
180 # Contents should conform to a subset of ISO 8601
181 # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with
182 # a loss of precision)
183 _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")
185 def _dateFromString(s):
186 order = ('year', 'month', 'day', 'hour', 'minute', 'second')
187 gd = _dateParser.match(s).groupdict()
188 lst = []
189 for key in order:
190 val = gd[key]
191 if val is None:
192 break
193 lst.append(int(val))
194 return datetime.datetime(*lst)
196 def _dateToString(d):
197 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
198 d.year, d.month, d.day,
199 d.hour, d.minute, d.second
203 # Regex to find any control chars, except for \t \n and \r
204 _controlCharPat = re.compile(
205 r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
206 r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
208 def _escapeAndEncode(text):
209 m = _controlCharPat.search(text)
210 if m is not None:
211 raise ValueError("strings can't contains control characters; "
212 "use plistlib.Data instead")
213 text = text.replace("\r\n", "\n") # convert DOS line endings
214 text = text.replace("\r", "\n") # convert Mac line endings
215 text = text.replace("&", "&amp;") # escape '&'
216 text = text.replace("<", "&lt;") # escape '<'
217 text = text.replace(">", "&gt;") # escape '>'
218 return text.encode("utf-8") # encode as UTF-8
221 PLISTHEADER = """\
222 <?xml version="1.0" encoding="UTF-8"?>
223 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
226 class PlistWriter(DumbXMLWriter):
228 def __init__(self, file, indentLevel=0, indent="\t", writeHeader=1):
229 if writeHeader:
230 file.write(PLISTHEADER)
231 DumbXMLWriter.__init__(self, file, indentLevel, indent)
233 def writeValue(self, value):
234 if isinstance(value, (str, unicode)):
235 self.simpleElement("string", value)
236 elif isinstance(value, bool):
237 # must switch for bool before int, as bool is a
238 # subclass of int...
239 if value:
240 self.simpleElement("true")
241 else:
242 self.simpleElement("false")
243 elif isinstance(value, (int, long)):
244 self.simpleElement("integer", "%d" % value)
245 elif isinstance(value, float):
246 self.simpleElement("real", repr(value))
247 elif isinstance(value, dict):
248 self.writeDict(value)
249 elif isinstance(value, Data):
250 self.writeData(value)
251 elif isinstance(value, datetime.datetime):
252 self.simpleElement("date", _dateToString(value))
253 elif isinstance(value, (tuple, list)):
254 self.writeArray(value)
255 else:
256 raise TypeError("unsuported type: %s" % type(value))
258 def writeData(self, data):
259 self.beginElement("data")
260 self.indentLevel -= 1
261 maxlinelength = 76 - len(self.indent.replace("\t", " " * 8) *
262 self.indentLevel)
263 for line in data.asBase64(maxlinelength).split("\n"):
264 if line:
265 self.writeln(line)
266 self.indentLevel += 1
267 self.endElement("data")
269 def writeDict(self, d):
270 self.beginElement("dict")
271 items = d.items()
272 items.sort()
273 for key, value in items:
274 if not isinstance(key, (str, unicode)):
275 raise TypeError("keys must be strings")
276 self.simpleElement("key", key)
277 self.writeValue(value)
278 self.endElement("dict")
280 def writeArray(self, array):
281 self.beginElement("array")
282 for value in array:
283 self.writeValue(value)
284 self.endElement("array")
287 class _InternalDict(dict):
289 # This class is needed while Dict is scheduled for deprecation:
290 # we only need to warn when a *user* instantiates Dict or when
291 # the "attribute notation for dict keys" is used.
293 def __getattr__(self, attr):
294 try:
295 value = self[attr]
296 except KeyError:
297 raise AttributeError, attr
298 from warnings import warn
299 warn("Attribute access from plist dicts is deprecated, use d[key] "
300 "notation instead", PendingDeprecationWarning)
301 return value
303 def __setattr__(self, attr, value):
304 from warnings import warn
305 warn("Attribute access from plist dicts is deprecated, use d[key] "
306 "notation instead", PendingDeprecationWarning)
307 self[attr] = value
309 def __delattr__(self, attr):
310 try:
311 del self[attr]
312 except KeyError:
313 raise AttributeError, attr
314 from warnings import warn
315 warn("Attribute access from plist dicts is deprecated, use d[key] "
316 "notation instead", PendingDeprecationWarning)
318 class Dict(_InternalDict):
320 def __init__(self, **kwargs):
321 from warnings import warn
322 warn("The plistlib.Dict class is deprecated, use builtin dict instead",
323 PendingDeprecationWarning)
324 super(Dict, self).__init__(**kwargs)
327 class Plist(_InternalDict):
329 """This class has been deprecated. Use readPlist() and writePlist()
330 functions instead, together with regular dict objects.
333 def __init__(self, **kwargs):
334 from warnings import warn
335 warn("The Plist class is deprecated, use the readPlist() and "
336 "writePlist() functions instead", PendingDeprecationWarning)
337 super(Plist, self).__init__(**kwargs)
339 def fromFile(cls, pathOrFile):
340 """Deprecated. Use the readPlist() function instead."""
341 rootObject = readPlist(pathOrFile)
342 plist = cls()
343 plist.update(rootObject)
344 return plist
345 fromFile = classmethod(fromFile)
347 def write(self, pathOrFile):
348 """Deprecated. Use the writePlist() function instead."""
349 writePlist(self, pathOrFile)
352 def _encodeBase64(s, maxlinelength=76):
353 # copied from base64.encodestring(), with added maxlinelength argument
354 maxbinsize = (maxlinelength//4)*3
355 pieces = []
356 for i in range(0, len(s), maxbinsize):
357 chunk = s[i : i + maxbinsize]
358 pieces.append(binascii.b2a_base64(chunk))
359 return "".join(pieces)
361 class Data:
363 """Wrapper for binary data."""
365 def __init__(self, data):
366 self.data = data
368 def fromBase64(cls, data):
369 # base64.decodestring just calls binascii.a2b_base64;
370 # it seems overkill to use both base64 and binascii.
371 return cls(binascii.a2b_base64(data))
372 fromBase64 = classmethod(fromBase64)
374 def asBase64(self, maxlinelength=76):
375 return _encodeBase64(self.data, maxlinelength)
377 def __cmp__(self, other):
378 if isinstance(other, self.__class__):
379 return cmp(self.data, other.data)
380 elif isinstance(other, str):
381 return cmp(self.data, other)
382 else:
383 return cmp(id(self), id(other))
385 def __repr__(self):
386 return "%s(%s)" % (self.__class__.__name__, repr(self.data))
389 class PlistParser:
391 def __init__(self):
392 self.stack = []
393 self.currentKey = None
394 self.root = None
396 def parse(self, fileobj):
397 from xml.parsers.expat import ParserCreate
398 parser = ParserCreate()
399 parser.StartElementHandler = self.handleBeginElement
400 parser.EndElementHandler = self.handleEndElement
401 parser.CharacterDataHandler = self.handleData
402 parser.ParseFile(fileobj)
403 return self.root
405 def handleBeginElement(self, element, attrs):
406 self.data = []
407 handler = getattr(self, "begin_" + element, None)
408 if handler is not None:
409 handler(attrs)
411 def handleEndElement(self, element):
412 handler = getattr(self, "end_" + element, None)
413 if handler is not None:
414 handler()
416 def handleData(self, data):
417 self.data.append(data)
419 def addObject(self, value):
420 if self.currentKey is not None:
421 self.stack[-1][self.currentKey] = value
422 self.currentKey = None
423 elif not self.stack:
424 # this is the root object
425 self.root = value
426 else:
427 self.stack[-1].append(value)
429 def getData(self):
430 data = "".join(self.data)
431 try:
432 data = data.encode("ascii")
433 except UnicodeError:
434 pass
435 self.data = []
436 return data
438 # element handlers
440 def begin_dict(self, attrs):
441 d = _InternalDict()
442 self.addObject(d)
443 self.stack.append(d)
444 def end_dict(self):
445 self.stack.pop()
447 def end_key(self):
448 self.currentKey = self.getData()
450 def begin_array(self, attrs):
451 a = []
452 self.addObject(a)
453 self.stack.append(a)
454 def end_array(self):
455 self.stack.pop()
457 def end_true(self):
458 self.addObject(True)
459 def end_false(self):
460 self.addObject(False)
461 def end_integer(self):
462 self.addObject(int(self.getData()))
463 def end_real(self):
464 self.addObject(float(self.getData()))
465 def end_string(self):
466 self.addObject(self.getData())
467 def end_data(self):
468 self.addObject(Data.fromBase64(self.getData()))
469 def end_date(self):
470 self.addObject(_dateFromString(self.getData()))