7
|
1 """PListReader
|
|
2
|
|
3 Reads Apple's plist XML data structure serialization format and returns a
|
|
4 native Python data structure. In conjunction with XMLFilter, it should
|
|
5 be compatible with all versions of Python from 1.5.2 up, on all platforms.
|
|
6
|
|
7 Copyright Andrew Shearer 2003-2004
|
|
8 <mailto:awshearer@shearersoftware.com>
|
|
9 For the current version, see:
|
|
10 http://www.shearersoftware.com/software/developers/plist/
|
|
11
|
|
12 Revision History:
|
|
13 2003-08-16 ashearer Initial version
|
|
14 2003-09-04 ashearer Support getRecommendedFeatures() & the XMLFilter features
|
|
15 API, and use it to disable feature_external_ges, allowing
|
|
16 parsing to work even while disconnected from the Internet
|
|
17 2004-02-14 ashearer Use native booleans if available; minor changes to
|
|
18 documentation
|
|
19
|
|
20 Dependencies:
|
|
21 XMLFilter module (optional). Provides compatibility for versions
|
|
22 of Python without xml.sax support, such as the version of Python
|
|
23 2.2 that shipped with Jaguar (Mac OS X 10.2). If XMLFilter isn't
|
|
24 found, xml.sax.handler will be tried instead. See:
|
|
25 http://www.shearersoftware.com/software/developers/xmlfilter/
|
|
26
|
|
27 W3CDate module. Parses the W3C Date format (a subset of ISO 8601), which
|
|
28 is the standard datetime format for plists. See:
|
|
29 http://www.shearersoftware.com/software/developers/w3cdate/
|
|
30
|
|
31
|
|
32 Usage:
|
|
33 import XMLFilter
|
|
34 reader = PListReader()
|
|
35 XMLFilter.parseFilePath(filePath, reader,
|
|
36 features = reader.getRecommendedFeatures())
|
|
37 result = reader.getResult()
|
|
38 # result could be a Python dict, list, string, number, etc.
|
|
39
|
|
40 Notes: This class assumes the target file is a valid plist file. In
|
|
41 particular, the root element isn't checked to make sure that it's
|
|
42 "plist", and unknown elements are ignored. So it's more lax than a
|
|
43 validating parser. If nothing in the document was recognized, the result
|
|
44 will be Python's None value. If the known elements are corrupt, the
|
|
45 error messages may not be that helpful. Exceptions will generally be
|
|
46 thrown at the right times, but their contents could be improved.
|
|
47
|
|
48 License: dual-licensed under the Python License and MPSL 1.1 (Mozilla License).
|
|
49
|
|
50 """
|
|
51
|
|
52 try:
|
|
53 from XMLFilter import XMLSAXHandler, \
|
|
54 feature_namespaces, feature_external_ges, feature_external_pes
|
|
55 except ImportError:
|
|
56 try:
|
|
57 from xml.sax.handler import ContentHandler as XMLSAXHandler, \
|
|
58 feature_namespaces, feature_external_ges, feature_external_pes
|
|
59
|
|
60 except ImportError:
|
|
61 raise ImportError, "PListReader requires either the XMLFilter module (available separately from www.shearersoftware.com) or a version of Python with xml.sax support."
|
|
62
|
|
63 import W3CDate
|
|
64 import base64
|
|
65
|
|
66 class PListReader(XMLSAXHandler):
|
|
67 def __init__(self):
|
|
68 XMLSAXHandler.__init__(self)
|
|
69 self._currentKey = None # pending key; can tell us we're in a dict
|
|
70 self._text = None # accumulated text
|
|
71 self._result = None # parsed Python data structure
|
|
72 self._stack = [] # stack for nested arrays/dicts
|
|
73
|
|
74 def getRecommendedFeatures(self):
|
|
75 """The recommended features (which clients should pass along to the
|
|
76 SAX parser) disable namespace parsing and external entities.
|
|
77
|
|
78 If external_ges aren't disabled, xml.sax.handler may try to load the DTD
|
|
79 from Apple's web server, which prevents parsing when there's no Internet
|
|
80 connection."""
|
|
81 return {feature_namespaces: 0, feature_external_ges: 0, feature_external_pes: 0}
|
|
82
|
|
83 # map plist element names to functions that make an empty Python object
|
|
84 complexTypeConstructors = {'dict': dict, 'array': list}
|
|
85
|
|
86 def startElement(self, name, attrs):
|
|
87 constructor = self.complexTypeConstructors.get(name)
|
|
88 if constructor is not None:
|
|
89 # call constructor func, making an empty Python dict or list
|
|
90 newItem = constructor()
|
|
91 # add it to the current container; it will be populated in place
|
|
92 self._handleValue(newItem)
|
|
93 # push onto stack, making it the new "current container"
|
|
94 self._stack.append(newItem)
|
|
95 else: # not a complex type?
|
|
96 self._text = '' # turn on text accumulation
|
|
97
|
|
98 # map plist element names to functions that convert text into a Python object
|
|
99 simpleTypeConverterFunctions = {
|
|
100 'data': base64.decodestring, 'date': W3CDate.W3CDate,
|
|
101 'integer': int, 'real': float, 'string': lambda x: x,
|
|
102 'true': lambda x: 1==1, 'false': lambda x: 0==1}
|
|
103
|
|
104 def endElement(self, name):
|
|
105 converter = self.simpleTypeConverterFunctions.get(name)
|
|
106 if converter is not None:
|
|
107 self._handleValue(converter(self._text)) # call converter func on text
|
|
108 self._currentKey = None
|
|
109 elif name == 'key':
|
|
110 self._currentKey = self._text # stash dict key for next _endValue
|
|
111 elif name in ('array', 'dict'):
|
|
112 self._stack.pop() # array or dict was already updated in place
|
|
113 self._currentKey = None
|
|
114 self._text = None # turn off text accumulation
|
|
115
|
|
116 def _handleValue(self, value):
|
|
117 """
|
|
118 Add a parsed value to the current container.
|
|
119 The container could be a dict, array, or even the root.
|
|
120 """
|
|
121 if self._result is None: # first item in file (the result's root)
|
|
122 self._result = value
|
|
123 self._stack.append(value)
|
|
124 elif self._currentKey is not None: # put value into dict; there was a key
|
|
125 self._stack[-1][self._currentKey] = value
|
|
126 self._currentKey = None
|
|
127 else: # append value to array
|
|
128 self._stack[-1].append(value) # (must be array; there was no <key>)
|
|
129
|
|
130 def characters(self, content):
|
|
131 if self._text is not None: # accumulate text
|
|
132 self._text = self._text + content
|
|
133
|
|
134 def getResult(self):
|
|
135 return self._result
|
|
136
|