#!/usr/bin/env python
"""
A `Property Lists`_ is a data representation used in Apple's Mac OS X as
a convenient way to store standard object types, such as string, number,
boolean, and container object.

This file contains a class ``XmlPropertyListParser`` for parse
a property list file and get back a python native data structure.

    :copyright: 2008 by Takanori Ishikawa <takanori.ishikawa@gmail.com>
    :license: MIT (See LICENSE file for more details)

.. _Property Lists: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/
"""
from xml.sax import make_parser, handler, xmlreader, \
                    SAXParseException


class PropertyListParseError(Exception):
    """Raised when parsing a property list is failed."""
    pass


class XmlPropertyListParser(handler.ContentHandler):
    """
    The ``XmlPropertyListParser`` class provides methods that
    convert `Property Lists`_ objects from xml format.
    Property list objects include ``string``, ``unicode``,
    ``list``, ``dict``, ``datetime``, and ``int`` or ``float``.

        :copyright: 2008 by Takanori Ishikawa <takanori.ishikawa@gmail.com>
        :license: MIT License

    .. _Property List: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/
    """

    def _assert(self, test, message):
        if not test:
            raise PropertyListParseError(message)

    # ------------------------------------------------
    # SAX2: ContentHandler
    # ------------------------------------------------
    def startDocument(self):
        self.__stack = []
        self.__plist = None
        self.__key = None
        self.__characters = None

    def endDocument(self):
        self._assert(self.__plist is not None, "A top level element must be <plist>.")        

    def startElement(self, name, attributes):
        if name in XmlPropertyListParser.START_CALLBACKS:
            XmlPropertyListParser.START_CALLBACKS[name](self, name, attributes)
        if name in XmlPropertyListParser.PARSE_CALLBACKS:
            self.__characters = []

    def endElement(self, name):
        if name in XmlPropertyListParser.END_CALLBACKS:
            XmlPropertyListParser.END_CALLBACKS[name](self, name)
        if name in XmlPropertyListParser.PARSE_CALLBACKS:
            # Creates character string from buffered characters.
            content = ''.join(self.__characters)
            XmlPropertyListParser.PARSE_CALLBACKS[name](self, name, content)
            self.__characters = None

    def characters(self, content):
        if self.__characters is not None:
            self.__characters.append(content)

    # ------------------------------------------------
    # XmlPropertyListParser private
    # ------------------------------------------------
    def _push_value(self, value):
        if self.__plist is None:
            self.__plist = value
        else:
            top = self.__stack[-1]
            if isinstance(top, dict):
                self._assert(self.__key is not None, "Missing key for dictionary.")
                top[self.__key] = value
                self.__key = None
            elif isinstance(top, list):
                top.append(value)
            else:
                raise PropertyListParseError(
                    "multiple objects at top level")

    def _pop_value(self):
        self.__stack.pop()

    def _start_plist(self, name, attrs):
        self._assert(not self.__stack and self.__plist is None, "<plist> more than once.")
        self._assert(attrs.get('version', '1.0') == '1.0',
            "version 1.0 is only supported, but was '%s'." % attrs.get('version'))

    def _start_array(self, name, attrs):
        v = list()
        self._push_value(v)
        self.__stack.append(v)

    def _start_dict(self, name, attrs):
        v = dict()
        self._push_value(v)
        self.__stack.append(v)

    def _end_array(self, name):
        self._pop_value()

    def _end_dict(self, name):
        self._pop_value()

    def _start_true(self, name, attrs):
        self._push_value(True)

    def _start_false(self, name, attrs):
        self._push_value(False)

    def _parse_key(self, name, content):
        self.__key = content

    def _parse_string(self, name, content):
        self._push_value(content)

    def _parse_data(self, name, content):
        import base64
        self._push_value(base64.b64decode(content))

    def _parse_date(self, name, content):
        # http://www.apple.com/DTDs/PropertyList-1.0.dtd says:
        #
        # 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)
        import re, datetime
        
        units = ('year', 'month', 'day', 'hour', 'minute', 'second')
        pattern = 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$")
        
        match = pattern.match(content)
        if not match:
            raise PropertyListParseError("Failed to parse datetime '%s'" % content)

        groups, components = match.groupdict(), []
        for key in units:
            value = groups[key]
            if value is None:
                break
            components.append(int(value))
        
        while len(components) < 3:
            components.append(1)
        d = datetime.datetime(*components)
        self._push_value(d)

    def _parse_real(self, name, content):
        self._push_value(float(content))

    def _parse_integer(self, name, content):
        self._push_value(int(content))

    START_CALLBACKS = {
        'plist': _start_plist,
        'array': _start_array,
        'dict': _start_dict,
        'true': _start_true,
        'false': _start_false,
    }

    END_CALLBACKS = {
        'array': _end_array,
        'dict': _end_dict,
    }

    PARSE_CALLBACKS = {
        'key': _parse_key,
        'string': _parse_string,
        'data': _parse_data,
        'date': _parse_date,
        'real': _parse_real,
        'integer': _parse_integer,
    }

    # ------------------------------------------------
    # XmlPropertyListParser
    # ------------------------------------------------
    def parse(self, xml_input):
        """
        Parse the property list (`.plist`, `.xml, for example) ``xml_input``,
        which can be either a string or a file-like object.
        
        >>> parser = XmlPropertyListParser()
        >>> parser.parse(r'<plist version="1.0"><dict><key>Python</key><string>.py</string></dict></plist>')
        {u'Python': u'.py'}
        """

        def make_source(xml_input):
            source = xmlreader.InputSource()
            if isinstance(xml_input, basestring):
                # Creates a string stream for in-memory contents.
                from StringIO import StringIO
                xml_input = StringIO(xml_input)
            source.setByteStream(xml_input)
            return source

        source = make_source(xml_input)
        reader = make_parser()
        reader.setContentHandler(self)
        try:
            reader.parse(source)
        except SAXParseException, e:
            raise PropertyListParseError(e)
        else:
            self._assert(
                len(self.__stack) is 0,
                "multiple objects at top level.")

        return self.__plist


if __name__ == '__main__':
    # doctest, and parse .plist specified by ARGV[1]
    #
    # For example, parsing iTunes Liberary on your mac.
    # % python ./plist_parser.py ~/"Music/iTunes/iTunes Music Library.xml"
    #
    import sys
    import doctest
    doctest.testmod()

    if len(sys.argv) > 1:
        xmlin = open(sys.argv[1])
        try:
            print XmlPropertyListParser().parse(xmlin),
        finally:
            xmlin.close()

@@ -6878,16 +6878,17 @@
    from 
+c
 StringIO

Use cStringIO module