_make_boundary(): Fix for SF bug #745478, broken boundary calculation
[python/dscho.git] / Lib / plat-mac / plistlib.py
blob40e2675afbeb1d8d62bc9fa37e057b6f6b77b807
1 """plistlib.py -- a tool to generate and parse MacOSX .plist files.
3 The main class in this module is Plist. It takes a set of arbitrary
4 keyword arguments, which will be the top level elements of the plist
5 dictionary. After instantiation you can add more elements by assigning
6 new attributes to the Plist instance.
8 To write out a plist file, call the write() method of the Plist
9 instance with a filename or a file object.
11 To parse a plist from a file, use the Plist.fromFile(pathOrFile)
12 classmethod, with a file name or a file object as the only argument.
13 (Warning: you need pyexpat installed for this to work, ie. it doesn't
14 work with a vanilla Python 2.2 as shipped with MacOS X.2.)
16 Values can be strings, integers, floats, booleans, tuples, lists,
17 dictionaries, Data or Date objects. String values (including dictionary
18 keys) may be unicode strings -- they will be written out as UTF-8.
20 For convenience, this module exports a class named Dict(), which
21 allows you to easily construct (nested) dicts using keyword arguments.
22 But regular dicts work, too.
24 To support Boolean values in plists with Python < 2.3, "bool", "True"
25 and "False" are exported. Use these symbols from this module if you
26 want to be compatible with Python 2.2.x (strongly recommended).
28 The <data> plist type is supported through the Data class. This is a
29 thin wrapper around a Python string.
31 The <date> plist data has (limited) support through the Date class.
32 (Warning: Dates are only supported if the PyXML package is installed.)
34 Generate Plist example:
36 pl = Plist(
37 aString="Doodah",
38 aList=["A", "B", 12, 32.1, [1, 2, 3]],
39 aFloat = 0.1,
40 anInt = 728,
41 aDict=Dict(
42 anotherString="<hello & hi there!>",
43 aUnicodeValue=u'M\xe4ssig, Ma\xdf',
44 aTrueValue=True,
45 aFalseValue=False,
47 someData = Data("<binary gunk>"),
48 someMoreData = Data("<lots of binary gunk>" * 10),
49 aDate = Date(time.mktime(time.gmtime())),
51 # unicode keys are possible, but a little awkward to use:
52 pl[u'\xc5benraa'] = "That was a unicode key."
53 pl.write(fileName)
55 Parse Plist example:
57 pl = Plist.fromFile(pathOrFile)
58 print pl.aKey
61 """
63 # written by Just van Rossum (just@letterror.com), 2002-11-19
66 __all__ = ["Plist", "Data", "Date", "Dict", "False", "True", "bool"]
69 INDENT = "\t"
72 class DumbXMLWriter:
74 def __init__(self, file):
75 self.file = file
76 self.stack = []
77 self.indentLevel = 0
79 def beginElement(self, element):
80 self.stack.append(element)
81 self.writeln("<%s>" % element)
82 self.indentLevel += 1
84 def endElement(self, element):
85 assert self.indentLevel > 0
86 assert self.stack.pop() == element
87 self.indentLevel -= 1
88 self.writeln("</%s>" % element)
90 def simpleElement(self, element, value=None):
91 if value:
92 value = _encode(value)
93 self.writeln("<%s>%s</%s>" % (element, value, element))
94 else:
95 self.writeln("<%s/>" % element)
97 def writeln(self, line):
98 if line:
99 self.file.write(self.indentLevel * INDENT + line + "\n")
100 else:
101 self.file.write("\n")
104 def _encode(text):
105 text = text.replace("&", "&amp;")
106 text = text.replace("<", "&lt;")
107 return text.encode("utf-8")
110 PLISTHEADER = """\
111 <?xml version="1.0" encoding="UTF-8"?>
112 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
115 class PlistWriter(DumbXMLWriter):
117 def __init__(self, file):
118 file.write(PLISTHEADER)
119 DumbXMLWriter.__init__(self, file)
121 def writeValue(self, value):
122 if isinstance(value, (str, unicode)):
123 self.simpleElement("string", value)
124 elif isinstance(value, bool):
125 # must switch for bool before int, as bool is a
126 # subclass of int...
127 if value:
128 self.simpleElement("true")
129 else:
130 self.simpleElement("false")
131 elif isinstance(value, int):
132 self.simpleElement("integer", str(value))
133 elif isinstance(value, float):
134 # should perhaps use repr() for better precision?
135 self.simpleElement("real", str(value))
136 elif isinstance(value, (dict, Dict)):
137 self.writeDict(value)
138 elif isinstance(value, Data):
139 self.writeData(value)
140 elif isinstance(value, Date):
141 self.simpleElement("date", value.toString())
142 elif isinstance(value, (tuple, list)):
143 self.writeArray(value)
144 else:
145 assert 0, "unsuported type: %s" % type(value)
147 def writeData(self, data):
148 self.beginElement("data")
149 for line in data.asBase64().split("\n"):
150 if line:
151 self.writeln(line)
152 self.endElement("data")
154 def writeDict(self, d):
155 self.beginElement("dict")
156 items = d.items()
157 items.sort()
158 for key, value in items:
159 assert isinstance(key, (str, unicode)), "keys must be strings"
160 self.simpleElement("key", key)
161 self.writeValue(value)
162 self.endElement("dict")
164 def writeArray(self, array):
165 self.beginElement("array")
166 for value in array:
167 self.writeValue(value)
168 self.endElement("array")
171 class Dict:
173 """Dict wrapper for convenient access of values through attributes."""
175 def __init__(self, **kwargs):
176 self.__dict__.update(kwargs)
178 def __cmp__(self, other):
179 if isinstance(other, self.__class__):
180 return cmp(self.__dict__, other.__dict__)
181 elif isinstance(other, dict):
182 return cmp(self.__dict__, other)
183 else:
184 return cmp(id(self), id(other))
186 def __str__(self):
187 return "%s(**%s)" % (self.__class__.__name__, self.__dict__)
188 __repr__ = __str__
190 def copy(self):
191 return self.__class__(**self.__dict__)
193 def __getattr__(self, attr):
194 """Delegate everything else to the dict object."""
195 return getattr(self.__dict__, attr)
198 class Plist(Dict):
200 """The main Plist object. Basically a dict (the toplevel object
201 of a plist is a dict) with two additional methods to read from
202 and write to files.
205 def fromFile(cls, pathOrFile):
206 didOpen = 0
207 if not hasattr(pathOrFile, "write"):
208 pathOrFile = open(pathOrFile)
209 didOpen = 1
210 p = PlistParser()
211 plist = p.parse(pathOrFile)
212 if didOpen:
213 pathOrFile.close()
214 return plist
215 fromFile = classmethod(fromFile)
217 def write(self, pathOrFile):
218 if not hasattr(pathOrFile, "write"):
219 pathOrFile = open(pathOrFile, "w")
220 didOpen = 1
221 else:
222 didOpen = 0
224 writer = PlistWriter(pathOrFile)
225 writer.writeln("<plist version=\"1.0\">")
226 writer.writeDict(self.__dict__)
227 writer.writeln("</plist>")
229 if didOpen:
230 pathOrFile.close()
233 class Data:
235 """Wrapper for binary data."""
237 def __init__(self, data):
238 self.data = data
240 def fromBase64(cls, data):
241 import base64
242 return cls(base64.decodestring(data))
243 fromBase64 = classmethod(fromBase64)
245 def asBase64(self):
246 import base64
247 return base64.encodestring(self.data)
249 def __cmp__(self, other):
250 if isinstance(other, self.__class__):
251 return cmp(self.data, other.data)
252 elif isinstance(other, str):
253 return cmp(self.data, other)
254 else:
255 return cmp(id(self), id(other))
257 def __repr__(self):
258 return "%s(%s)" % (self.__class__.__name__, repr(self.data))
261 class Date:
263 """Primitive date wrapper, uses time floats internally, is agnostic
264 about time zones.
267 def __init__(self, date):
268 if isinstance(date, str):
269 from xml.utils.iso8601 import parse
270 date = parse(date)
271 self.date = date
273 def toString(self):
274 from xml.utils.iso8601 import tostring
275 return tostring(self.date)
277 def __cmp__(self, other):
278 if isinstance(other, self.__class__):
279 return cmp(self.date, other.date)
280 elif isinstance(other, (int, float)):
281 return cmp(self.date, other)
282 else:
283 return cmp(id(self), id(other))
285 def __repr__(self):
286 return "%s(%s)" % (self.__class__.__name__, repr(self.toString()))
289 class PlistParser:
291 def __init__(self):
292 self.stack = []
293 self.currentKey = None
294 self.root = None
296 def parse(self, file):
297 from xml.parsers.expat import ParserCreate
298 parser = ParserCreate()
299 parser.StartElementHandler = self.handleBeginElement
300 parser.EndElementHandler = self.handleEndElement
301 parser.CharacterDataHandler = self.handleData
302 parser.ParseFile(file)
303 return self.root
305 def handleBeginElement(self, element, attrs):
306 self.data = []
307 handler = getattr(self, "begin_" + element, None)
308 if handler is not None:
309 handler(attrs)
311 def handleEndElement(self, element):
312 handler = getattr(self, "end_" + element, None)
313 if handler is not None:
314 handler()
316 def handleData(self, data):
317 self.data.append(data)
319 def addObject(self, value):
320 if self.currentKey is not None:
321 self.stack[-1][self.currentKey] = value
322 self.currentKey = None
323 elif not self.stack:
324 # this is the root object
325 assert self.root is value
326 else:
327 self.stack[-1].append(value)
329 def getData(self):
330 data = "".join(self.data)
331 try:
332 data = data.encode("ascii")
333 except UnicodeError:
334 pass
335 self.data = []
336 return data
338 # element handlers
340 def begin_dict(self, attrs):
341 if self.root is None:
342 self.root = d = Plist()
343 else:
344 d = Dict()
345 self.addObject(d)
346 self.stack.append(d)
347 def end_dict(self):
348 self.stack.pop()
350 def end_key(self):
351 self.currentKey = self.getData()
353 def begin_array(self, attrs):
354 a = []
355 self.addObject(a)
356 self.stack.append(a)
357 def end_array(self):
358 self.stack.pop()
360 def end_true(self):
361 self.addObject(True)
362 def end_false(self):
363 self.addObject(False)
364 def end_integer(self):
365 self.addObject(int(self.getData()))
366 def end_real(self):
367 self.addObject(float(self.getData()))
368 def end_string(self):
369 self.addObject(self.getData())
370 def end_data(self):
371 self.addObject(Data.fromBase64(self.getData()))
372 def end_date(self):
373 self.addObject(Date(self.getData()))
376 # cruft to support booleans in Python <= 2.3
377 import sys
378 if sys.version_info[:2] < (2, 3):
379 # Python 2.2 and earlier: no booleans
380 # Python 2.2.x: booleans are ints
381 class bool(int):
382 """Imitation of the Python 2.3 bool object."""
383 def __new__(cls, value):
384 return int.__new__(cls, not not value)
385 def __repr__(self):
386 if self:
387 return "True"
388 else:
389 return "False"
390 True = bool(1)
391 False = bool(0)
392 else:
393 # Bind the boolean builtins to local names
394 True = True
395 False = False
396 bool = bool
399 if __name__ == "__main__":
400 from StringIO import StringIO
401 import time
402 if len(sys.argv) == 1:
403 pl = Plist(
404 aString="Doodah",
405 aList=["A", "B", 12, 32.1, [1, 2, 3]],
406 aFloat = 0.1,
407 anInt = 728,
408 aDict=Dict(
409 anotherString="<hello & hi there!>",
410 aUnicodeValue=u'M\xe4ssig, Ma\xdf',
411 aTrueValue=True,
412 aFalseValue=False,
414 someData = Data("<binary gunk>"),
415 someMoreData = Data("<lots of binary gunk>" * 10),
416 aDate = Date(time.mktime(time.gmtime())),
418 elif len(sys.argv) == 2:
419 pl = Plist.fromFile(sys.argv[1])
420 else:
421 print "Too many arguments: at most 1 plist file can be given."
422 sys.exit(1)
424 # unicode keys are possible, but a little awkward to use:
425 pl[u'\xc5benraa'] = "That was a unicode key."
426 f = StringIO()
427 pl.write(f)
428 xml = f.getvalue()
429 print xml
430 f.seek(0)
431 pl2 = Plist.fromFile(f)
432 assert pl == pl2
433 f = StringIO()
434 pl2.write(f)
435 assert xml == f.getvalue()
436 #print repr(pl2)