]> granicus.if.org Git - python/commitdiff
Pure Python implementation of a plist generator/parser.
authorJust van Rossum <just@letterror.com>
Tue, 19 Nov 2002 22:01:02 +0000 (22:01 +0000)
committerJust van Rossum <just@letterror.com>
Tue, 19 Nov 2002 22:01:02 +0000 (22:01 +0000)
Mac/Lib/plistlib.py [new file with mode: 0644]

diff --git a/Mac/Lib/plistlib.py b/Mac/Lib/plistlib.py
new file mode 100644 (file)
index 0000000..9413fd1
--- /dev/null
@@ -0,0 +1,433 @@
+"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
+
+The main class in this module is Plist. It takes a set of arbitrary
+keyword arguments, which will be the top level elements of the plist
+dictionary. After instantiation you can add more elements by assigning
+new attributes to the Plist instance.
+
+To write out a plist file, call the write() method of the Plist
+instance with a filename or a file object.
+
+To parse a plist from a file, use the Plist.fromFile(pathOrFile)
+classmethod, with a file name or a file object as the only argument.
+(Warning: you need pyexpat installed for this to work, ie. it doesn't
+work with a vanilla Python 2.2 as shipped with MacOS X.2.)
+
+Values can be strings, integers, floats, booleans, tuples, lists,
+dictionaries, Data or Date objects. String values (including dictionary
+keys) may be unicode strings -- they will be written out as UTF-8.
+
+For convenience, this module exports a class named Dict(), which
+allows you to easily construct (nested) dicts using keyword arguments.
+But regular dicts work, too.
+
+To support Boolean values in plists with Python < 2.3, "bool", "True"
+and "False" are exported. Use these symbols from this module if you
+want to be compatible with Python 2.2.x (strongly recommended).
+
+The <data> plist type is supported through the Data class. This is a
+thin wrapper around a Python string.
+
+The <date> plist data has (limited) support through the Date class.
+(Warning: Dates are only supported if the PyXML package is installed.)
+
+Generate Plist example:
+
+       pl = Plist(
+                       Foo="Doodah",
+                       aList=["A", "B", 12, 32.1, [1, 2, 3]],
+                       aFloat = 0.1,
+                       anInt = 728,
+                       aDict=Dict(
+                               aString="<hello & hi there!>",
+                               SomeUnicodeValue=u'M\xe4ssig, Ma\xdf',
+                               someTrueValue=True,
+                               someFalseValue=False,
+                       ),
+                       someData = Data("hello there!"),
+                       someMoreData = Data("hello there! " * 10),
+                       aDate = Date(time.mktime(time.gmtime())),
+       )
+       # unicode keys are possible, but a little awkward to use:
+       pl[u'\xc5benraa'] = "That was a unicode key."
+       pl.write(fileName)
+
+Parse Plist example:
+
+       pl = Plist.fromFile(pathOrFile)
+       print pl.aKey
+
+
+"""
+
+# written by Just van Rossum (just@letterror.com), 2002-11-19
+
+
+__all__ = ["Plist", "Data", "Date", "Dict", "False", "True", "bool"]
+
+
+INDENT = "\t"
+
+
+class DumbXMLWriter:
+
+       def __init__(self, file):
+               self.file = file
+               self.stack = []
+               self.indentLevel = 0
+
+       def beginElement(self, element):
+               self.stack.append(element)
+               element = _encode(element)
+               self.writeln("<%s>" % element)
+               self.indentLevel += 1
+
+       def endElement(self, element):
+               assert self.indentLevel > 0
+               assert self.stack.pop() == element
+               self.indentLevel -= 1
+               self.writeln("</%s>" % element)
+
+       def simpleElement(self, element, value=None):
+               if value:
+                       element = _encode(element)
+                       value = _encode(value)
+                       self.writeln("<%s>%s</%s>" % (element, value, element))
+               else:
+                       self.writeln("<%s/>" % element)
+
+       def writeln(self, line):
+               if line:
+                       self.file.write(self.indentLevel * INDENT + line + "\n")
+               else:
+                       self.file.write("\n")
+
+
+def _encode(text):
+       text = text.replace("&", "&amp;")
+       text = text.replace("<", "&lt;")
+       return text.encode("utf-8")
+
+
+PLISTHEADER = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+"""
+
+class PlistWriter(DumbXMLWriter):
+
+       def __init__(self, file):
+               file.write(PLISTHEADER)
+               DumbXMLWriter.__init__(self, file)
+
+       def writeValue(self, value):
+               if isinstance(value, (str, unicode)):
+                       self.simpleElement("string", value)
+               elif isinstance(value, bool):
+                       # must switch for bool before int, as bool is a
+                       # subclass of int...
+                       if value:
+                               self.simpleElement("true")
+                       else:
+                               self.simpleElement("false")
+               elif isinstance(value, int):
+                       self.simpleElement("integer", str(value))
+               elif isinstance(value, float):
+                       # should perhaps use repr() for better precision?
+                       self.simpleElement("real", str(value))
+               elif isinstance(value, (dict, Dict)):
+                       self.writeDict(value)
+               elif isinstance(value, Data):
+                       self.writeData(value)
+               elif isinstance(value, Date):
+                       self.simpleElement("date", value.toString())
+               elif isinstance(value, (tuple, list)):
+                       self.writeArray(value)
+               else:
+                       assert 0, "unsuported type: %s" % type(value)
+
+       def writeData(self, data):
+               self.beginElement("data")
+               for line in data.asBase64().split("\n"):
+                       if line:
+                               self.writeln(line.strip())
+               self.endElement("data")
+
+       def writeDict(self, d):
+               self.beginElement("dict")
+               items = d.items()
+               items.sort()
+               for key, value in items:
+                       assert isinstance(key, (str, unicode)), "keys must be strings"
+                       self.simpleElement("key", key)
+                       self.writeValue(value)
+               self.endElement("dict")
+
+       def writeArray(self, array):
+               self.beginElement("array")
+               for value in array:
+                       self.writeValue(value)
+               self.endElement("array")
+
+
+class Dict:
+
+       """Dict wrapper for convenient acces of values through attributes."""
+
+       def __init__(self, **args):
+               self.__dict__.update(args)
+
+       def __cmp__(self, other):
+               if isinstance(other, self.__class__):
+                       return cmp(self.__dict__, other.__dict__)
+               elif isinstance(other, dict):
+                       return cmp(self.__dict__, other)
+               else:
+                       return cmp(id(self), id(other))
+
+       def __str__(self):
+               return "%s(**%s)" % (self.__class__.__name__, self.__dict__)
+       __repr__ = __str__
+
+       def __getattr__(self, attr):
+               """Delegate everything else to the dict object."""
+               return getattr(self.__dict__, attr)
+
+
+class Plist(Dict):
+
+       """The main Plist object. Basically a dict (the toplevel object of
+       a plist is a dict) with one additional method: write()."""
+
+       def fromFile(cls, pathOrFile):
+               didOpen = 0
+               if not hasattr(pathOrFile, "write"):
+                       pathOrFile = open(pathOrFile)
+                       didOpen = 1
+               p = PlistParser()
+               plist = p.parse(pathOrFile)
+               if didOpen:
+                       pathOrFile.close()
+               return plist
+       fromFile = classmethod(fromFile)
+
+       def write(self, pathOrFile):
+               if not hasattr(pathOrFile, "write"):
+                       pathOrFile = open(pathOrFile, "w")
+                       didOpen = 1
+               else:
+                       didOpen = 0
+
+               writer = PlistWriter(pathOrFile)
+               writer.writeln("<plist version=\"1.0\">")
+               writer.writeDict(self.__dict__)
+               writer.writeln("</plist>")
+
+               if didOpen:
+                       pathOrFile.close()
+
+
+class Data:
+
+       """Wrapper for binary data."""
+
+       def __init__(self, data):
+               self.data = data
+
+       def fromBase64(cls, data):
+               import base64
+               return cls(base64.decodestring(data))
+       fromBase64 = classmethod(fromBase64)
+
+       def asBase64(self):
+               import base64
+               return base64.encodestring(self.data)
+
+       def __cmp__(self, other):
+               if isinstance(other, self.__class__):
+                       return cmp(self.data, other.data)
+               elif isinstance(other, str):
+                       return cmp(self.data, other)
+               else:
+                       return cmp(id(self), id(other))
+
+       def __repr__(self):
+               return "%s(%s)" % (self.__class__.__name__, repr(self.data))
+
+
+class Date:
+
+       """Primitive date wrapper, uses time floats internally, is agnostic
+       about time zones.
+       """
+
+       def __init__(self, date):
+               if isinstance(date, str):
+                       from xml.utils.iso8601 import parse
+                       date = parse(date)
+               self.date = date
+
+       def toString(self):
+               from xml.utils.iso8601 import tostring
+               return tostring(self.date)
+
+       def __cmp__(self, other):
+               if isinstance(other, self.__class__):
+                       return cmp(self.date, other.date)
+               elif isinstance(other, (int, float)):
+                       return cmp(self.date, other)
+               else:
+                       return cmp(id(self), id(other))
+
+       def __repr__(self):
+               return "%s(%s)" % (self.__class__.__name__, repr(self.toString()))
+
+
+class PlistParser:
+
+       def __init__(self):
+               self.stack = []
+               self.currentKey = None
+               self.root = None
+
+       def parse(self, file):
+               from xml.parsers.expat import ParserCreate
+               parser = ParserCreate()
+               parser.StartElementHandler = self.handleBeginElement
+               parser.EndElementHandler = self.handleEndElement
+               parser.CharacterDataHandler = self.handleData
+               parser.ParseFile(file)
+               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.currentKey is not None:
+                       self.stack[-1][self.currentKey] = value
+                       self.currentKey = None
+               elif not self.stack:
+                       # this is the root object
+                       assert self.root is value
+               else:
+                       self.stack[-1].append(value)
+
+       def getData(self):
+               data = "".join(self.data)
+               try:
+                       data = data.encode("ascii")
+               except UnicodeError:
+                       pass
+               self.data = []
+               return data
+
+       # element handlers
+
+       def begin_dict(self, attrs):
+               if self.root is None:
+                       self.root = d = Plist()
+               else:
+                       d = Dict()
+               self.addObject(d)
+               self.stack.append(d)
+       def end_dict(self):
+               self.stack.pop()
+
+       def end_key(self):
+               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()))
+       def end_date(self):
+               self.addObject(Date(self.getData()))
+
+
+# cruft to support booleans in Python <= 2.3
+import sys
+if sys.version_info[:2] < (2, 3):
+       # Python 2.2 and earlier: no booleans
+       # Python 2.2.x: booleans are ints
+       class bool(int):
+               """Imitation of the Python 2.3 bool object."""
+               def __new__(cls, value):
+                       return int.__new__(cls, not not value)
+               def __repr__(self):
+                       if self:
+                               return "True"
+                       else:
+                               return "False"
+       True = bool(1)
+       False = bool(0)
+else:
+       import __builtin__
+       True = __builtin__.True
+       False = __builtin__.False
+       bool = __builtin__.bool
+
+
+if __name__ == "__main__":
+       from StringIO import StringIO
+       import time
+       if len(sys.argv) == 1:
+               pl = Plist(
+                               Foo="Doodah",
+                               aList=["A", "B", 12, 32.1, [1, 2, 3]],
+                               aFloat = 0.1,
+                               anInt = 728,
+                               aDict=Dict(
+                                       aString="<hello & hi there!>",
+                                       SomeUnicodeValue=u'M\xe4ssig, Ma\xdf',
+                                       someTrueValue=True,
+                                       someFalseValue=False,
+                               ),
+                               someData = Data("hello there!"),
+                               someMoreData = Data("hello there! " * 10),
+                               aDate = Date(time.mktime(time.gmtime())),
+               )
+       elif len(sys.argv) == 2:
+               pl = Plist.fromFile(sys.argv[1])
+       else:
+               print "Too many arguments: at most 1 plist file can be given."
+               sys.exit(1)
+
+       # unicode keys are possible, but a little awkward to use:
+       pl[u'\xc5benraa'] = "That was a unicode key."
+       f = StringIO()
+       pl.write(f)
+       xml = f.getvalue()
+       print xml
+       f.seek(0)
+       pl2 = Plist.fromFile(f)
+       assert pl == pl2
+       f = StringIO()
+       pl2.write(f)
+       assert xml == f.getvalue()
+       #print repr(pl2)