Issue #789290: make sure that hash(2**63) == hash(2.**63) on 64-bit
[python.git] / Lib / plistlib.py
blob31d1e75fdf815be21e497493eea2826996b78b07
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
65 import warnings
68 def readPlist(pathOrFile):
69 """Read a .plist file. 'pathOrFile' may either be a file name or a
70 (readable) file object. Return the unpacked root object (which
71 usually is a dictionary).
72 """
73 didOpen = 0
74 if isinstance(pathOrFile, (str, unicode)):
75 pathOrFile = open(pathOrFile)
76 didOpen = 1
77 p = PlistParser()
78 rootObject = p.parse(pathOrFile)
79 if didOpen:
80 pathOrFile.close()
81 return rootObject
84 def writePlist(rootObject, pathOrFile):
85 """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a
86 file name or a (writable) file object.
87 """
88 didOpen = 0
89 if isinstance(pathOrFile, (str, unicode)):
90 pathOrFile = open(pathOrFile, "w")
91 didOpen = 1
92 writer = PlistWriter(pathOrFile)
93 writer.writeln("<plist version=\"1.0\">")
94 writer.writeValue(rootObject)
95 writer.writeln("</plist>")
96 if didOpen:
97 pathOrFile.close()
100 def readPlistFromString(data):
101 """Read a plist data from a string. Return the root object.
103 return readPlist(StringIO(data))
106 def writePlistToString(rootObject):
107 """Return 'rootObject' as a plist-formatted string.
109 f = StringIO()
110 writePlist(rootObject, f)
111 return f.getvalue()
114 def readPlistFromResource(path, restype='plst', resid=0):
115 """Read plst resource from the resource fork of path.
117 warnings.warnpy3k("In 3.x, readPlistFromResource is removed.")
118 from Carbon.File import FSRef, FSGetResourceForkName
119 from Carbon.Files import fsRdPerm
120 from Carbon import Res
121 fsRef = FSRef(path)
122 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdPerm)
123 Res.UseResFile(resNum)
124 plistData = Res.Get1Resource(restype, resid).data
125 Res.CloseResFile(resNum)
126 return readPlistFromString(plistData)
129 def writePlistToResource(rootObject, path, restype='plst', resid=0):
130 """Write 'rootObject' as a plst resource to the resource fork of path.
132 warnings.warnpy3k("In 3.x, writePlistToResource is removed.")
133 from Carbon.File import FSRef, FSGetResourceForkName
134 from Carbon.Files import fsRdWrPerm
135 from Carbon import Res
136 plistData = writePlistToString(rootObject)
137 fsRef = FSRef(path)
138 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdWrPerm)
139 Res.UseResFile(resNum)
140 try:
141 Res.Get1Resource(restype, resid).RemoveResource()
142 except Res.Error:
143 pass
144 res = Res.Resource(plistData)
145 res.AddResource(restype, resid, '')
146 res.WriteResource()
147 Res.CloseResFile(resNum)
150 class DumbXMLWriter:
152 def __init__(self, file, indentLevel=0, indent="\t"):
153 self.file = file
154 self.stack = []
155 self.indentLevel = indentLevel
156 self.indent = indent
158 def beginElement(self, element):
159 self.stack.append(element)
160 self.writeln("<%s>" % element)
161 self.indentLevel += 1
163 def endElement(self, element):
164 assert self.indentLevel > 0
165 assert self.stack.pop() == element
166 self.indentLevel -= 1
167 self.writeln("</%s>" % element)
169 def simpleElement(self, element, value=None):
170 if value is not None:
171 value = _escapeAndEncode(value)
172 self.writeln("<%s>%s</%s>" % (element, value, element))
173 else:
174 self.writeln("<%s/>" % element)
176 def writeln(self, line):
177 if line:
178 self.file.write(self.indentLevel * self.indent + line + "\n")
179 else:
180 self.file.write("\n")
183 # Contents should conform to a subset of ISO 8601
184 # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with
185 # a loss of precision)
186 _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")
188 def _dateFromString(s):
189 order = ('year', 'month', 'day', 'hour', 'minute', 'second')
190 gd = _dateParser.match(s).groupdict()
191 lst = []
192 for key in order:
193 val = gd[key]
194 if val is None:
195 break
196 lst.append(int(val))
197 return datetime.datetime(*lst)
199 def _dateToString(d):
200 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
201 d.year, d.month, d.day,
202 d.hour, d.minute, d.second
206 # Regex to find any control chars, except for \t \n and \r
207 _controlCharPat = re.compile(
208 r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
209 r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
211 def _escapeAndEncode(text):
212 m = _controlCharPat.search(text)
213 if m is not None:
214 raise ValueError("strings can't contains control characters; "
215 "use plistlib.Data instead")
216 text = text.replace("\r\n", "\n") # convert DOS line endings
217 text = text.replace("\r", "\n") # convert Mac line endings
218 text = text.replace("&", "&amp;") # escape '&'
219 text = text.replace("<", "&lt;") # escape '<'
220 text = text.replace(">", "&gt;") # escape '>'
221 return text.encode("utf-8") # encode as UTF-8
224 PLISTHEADER = """\
225 <?xml version="1.0" encoding="UTF-8"?>
226 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
229 class PlistWriter(DumbXMLWriter):
231 def __init__(self, file, indentLevel=0, indent="\t", writeHeader=1):
232 if writeHeader:
233 file.write(PLISTHEADER)
234 DumbXMLWriter.__init__(self, file, indentLevel, indent)
236 def writeValue(self, value):
237 if isinstance(value, (str, unicode)):
238 self.simpleElement("string", value)
239 elif isinstance(value, bool):
240 # must switch for bool before int, as bool is a
241 # subclass of int...
242 if value:
243 self.simpleElement("true")
244 else:
245 self.simpleElement("false")
246 elif isinstance(value, (int, long)):
247 self.simpleElement("integer", "%d" % value)
248 elif isinstance(value, float):
249 self.simpleElement("real", repr(value))
250 elif isinstance(value, dict):
251 self.writeDict(value)
252 elif isinstance(value, Data):
253 self.writeData(value)
254 elif isinstance(value, datetime.datetime):
255 self.simpleElement("date", _dateToString(value))
256 elif isinstance(value, (tuple, list)):
257 self.writeArray(value)
258 else:
259 raise TypeError("unsuported type: %s" % type(value))
261 def writeData(self, data):
262 self.beginElement("data")
263 self.indentLevel -= 1
264 maxlinelength = 76 - len(self.indent.replace("\t", " " * 8) *
265 self.indentLevel)
266 for line in data.asBase64(maxlinelength).split("\n"):
267 if line:
268 self.writeln(line)
269 self.indentLevel += 1
270 self.endElement("data")
272 def writeDict(self, d):
273 self.beginElement("dict")
274 items = d.items()
275 items.sort()
276 for key, value in items:
277 if not isinstance(key, (str, unicode)):
278 raise TypeError("keys must be strings")
279 self.simpleElement("key", key)
280 self.writeValue(value)
281 self.endElement("dict")
283 def writeArray(self, array):
284 self.beginElement("array")
285 for value in array:
286 self.writeValue(value)
287 self.endElement("array")
290 class _InternalDict(dict):
292 # This class is needed while Dict is scheduled for deprecation:
293 # we only need to warn when a *user* instantiates Dict or when
294 # the "attribute notation for dict keys" is used.
296 def __getattr__(self, attr):
297 try:
298 value = self[attr]
299 except KeyError:
300 raise AttributeError, attr
301 from warnings import warn
302 warn("Attribute access from plist dicts is deprecated, use d[key] "
303 "notation instead", PendingDeprecationWarning)
304 return value
306 def __setattr__(self, attr, value):
307 from warnings import warn
308 warn("Attribute access from plist dicts is deprecated, use d[key] "
309 "notation instead", PendingDeprecationWarning)
310 self[attr] = value
312 def __delattr__(self, attr):
313 try:
314 del self[attr]
315 except KeyError:
316 raise AttributeError, attr
317 from warnings import warn
318 warn("Attribute access from plist dicts is deprecated, use d[key] "
319 "notation instead", PendingDeprecationWarning)
321 class Dict(_InternalDict):
323 def __init__(self, **kwargs):
324 from warnings import warn
325 warn("The plistlib.Dict class is deprecated, use builtin dict instead",
326 PendingDeprecationWarning)
327 super(Dict, self).__init__(**kwargs)
330 class Plist(_InternalDict):
332 """This class has been deprecated. Use readPlist() and writePlist()
333 functions instead, together with regular dict objects.
336 def __init__(self, **kwargs):
337 from warnings import warn
338 warn("The Plist class is deprecated, use the readPlist() and "
339 "writePlist() functions instead", PendingDeprecationWarning)
340 super(Plist, self).__init__(**kwargs)
342 def fromFile(cls, pathOrFile):
343 """Deprecated. Use the readPlist() function instead."""
344 rootObject = readPlist(pathOrFile)
345 plist = cls()
346 plist.update(rootObject)
347 return plist
348 fromFile = classmethod(fromFile)
350 def write(self, pathOrFile):
351 """Deprecated. Use the writePlist() function instead."""
352 writePlist(self, pathOrFile)
355 def _encodeBase64(s, maxlinelength=76):
356 # copied from base64.encodestring(), with added maxlinelength argument
357 maxbinsize = (maxlinelength//4)*3
358 pieces = []
359 for i in range(0, len(s), maxbinsize):
360 chunk = s[i : i + maxbinsize]
361 pieces.append(binascii.b2a_base64(chunk))
362 return "".join(pieces)
364 class Data:
366 """Wrapper for binary data."""
368 def __init__(self, data):
369 self.data = data
371 def fromBase64(cls, data):
372 # base64.decodestring just calls binascii.a2b_base64;
373 # it seems overkill to use both base64 and binascii.
374 return cls(binascii.a2b_base64(data))
375 fromBase64 = classmethod(fromBase64)
377 def asBase64(self, maxlinelength=76):
378 return _encodeBase64(self.data, maxlinelength)
380 def __cmp__(self, other):
381 if isinstance(other, self.__class__):
382 return cmp(self.data, other.data)
383 elif isinstance(other, str):
384 return cmp(self.data, other)
385 else:
386 return cmp(id(self), id(other))
388 def __repr__(self):
389 return "%s(%s)" % (self.__class__.__name__, repr(self.data))
392 class PlistParser:
394 def __init__(self):
395 self.stack = []
396 self.currentKey = None
397 self.root = None
399 def parse(self, fileobj):
400 from xml.parsers.expat import ParserCreate
401 parser = ParserCreate()
402 parser.StartElementHandler = self.handleBeginElement
403 parser.EndElementHandler = self.handleEndElement
404 parser.CharacterDataHandler = self.handleData
405 parser.ParseFile(fileobj)
406 return self.root
408 def handleBeginElement(self, element, attrs):
409 self.data = []
410 handler = getattr(self, "begin_" + element, None)
411 if handler is not None:
412 handler(attrs)
414 def handleEndElement(self, element):
415 handler = getattr(self, "end_" + element, None)
416 if handler is not None:
417 handler()
419 def handleData(self, data):
420 self.data.append(data)
422 def addObject(self, value):
423 if self.currentKey is not None:
424 self.stack[-1][self.currentKey] = value
425 self.currentKey = None
426 elif not self.stack:
427 # this is the root object
428 self.root = value
429 else:
430 self.stack[-1].append(value)
432 def getData(self):
433 data = "".join(self.data)
434 try:
435 data = data.encode("ascii")
436 except UnicodeError:
437 pass
438 self.data = []
439 return data
441 # element handlers
443 def begin_dict(self, attrs):
444 d = _InternalDict()
445 self.addObject(d)
446 self.stack.append(d)
447 def end_dict(self):
448 self.stack.pop()
450 def end_key(self):
451 self.currentKey = self.getData()
453 def begin_array(self, attrs):
454 a = []
455 self.addObject(a)
456 self.stack.append(a)
457 def end_array(self):
458 self.stack.pop()
460 def end_true(self):
461 self.addObject(True)
462 def end_false(self):
463 self.addObject(False)
464 def end_integer(self):
465 self.addObject(int(self.getData()))
466 def end_real(self):
467 self.addObject(float(self.getData()))
468 def end_string(self):
469 self.addObject(self.getData())
470 def end_data(self):
471 self.addObject(Data.fromBase64(self.getData()))
472 def end_date(self):
473 self.addObject(_dateFromString(self.getData()))