Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / web / _flatten.py
1 # -*- test-case-name: twisted.web.test.test_flatten -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Context-free flattener/serializer for rendering Python objects, possibly
7 complex or arbitrarily nested, as strings.
8
9 """
10
11 from cStringIO import StringIO
12 from sys import exc_info
13 from types import GeneratorType
14 from traceback import extract_tb
15 from twisted.internet.defer import Deferred
16 from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
17
18 from twisted.web.iweb import IRenderable
19 from twisted.web._stan import (
20     Tag, slot, voidElements, Comment, CDATA, CharRef)
21
22
23
24 def escapedData(data, inAttribute):
25     """
26     Escape a string for inclusion in a document.
27
28     @type data: C{str} or C{unicode}
29     @param data: The string to escape.
30
31     @type inAttribute: C{bool}
32     @param inAttribute: A flag which, if set, indicates that the string should
33         be quoted for use as the value of an XML tag value.
34
35     @rtype: C{str}
36     @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
37         encoded string.
38     """
39     if isinstance(data, unicode):
40         data = data.encode('utf-8')
41     data = data.replace('&', '&'
42         ).replace('<', '&lt;'
43         ).replace('>', '&gt;')
44     if inAttribute:
45         data = data.replace('"', '&quot;')
46     return data
47
48
49 def escapedCDATA(data):
50     """
51     Escape CDATA for inclusion in a document.
52
53     @type data: C{str} or C{unicode}
54     @param data: The string to escape.
55
56     @rtype: C{str}
57     @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
58         encoded string.
59     """
60     if isinstance(data, unicode):
61         data = data.encode('utf-8')
62     return data.replace(']]>', ']]]]><![CDATA[>')
63
64
65 def escapedComment(data):
66     """
67     Escape a comment for inclusion in a document.
68
69     @type data: C{str} or C{unicode}
70     @param data: The string to escape.
71
72     @rtype: C{str}
73     @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
74         encoded string.
75     """
76     if isinstance(data, unicode):
77         data = data.encode('utf-8')
78     data = data.replace('--', '- - ').replace('>', '&gt;')
79     if data and data[-1] == '-':
80         data += ' '
81     return data
82
83
84 def _getSlotValue(name, slotData, default=None):
85     """
86     Find the value of the named slot in the given stack of slot data.
87     """
88     for slotFrame in slotData[::-1]:
89         if slotFrame is not None and name in slotFrame:
90             return slotFrame[name]
91     else:
92         if default is not None:
93             return default
94         raise UnfilledSlot(name)
95
96
97 def _flattenElement(request, root, slotData, renderFactory, inAttribute):
98     """
99     Make C{root} slightly more flat by yielding all its immediate contents 
100     as strings, deferreds or generators that are recursive calls to itself.
101
102     @param request: A request object which will be passed to
103         L{IRenderable.render}.
104
105     @param root: An object to be made flatter.  This may be of type C{unicode},
106         C{str}, L{slot}, L{Tag}, L{URL}, L{tuple}, L{list}, L{GeneratorType},
107         L{Deferred}, or an object that implements L{IRenderable}.
108
109     @param slotData: A C{list} of C{dict} mapping C{str} slot names to data
110         with which those slots will be replaced.
111
112     @param renderFactory: If not C{None}, An object that provides L{IRenderable}.
113
114     @param inAttribute: A flag which, if set, indicates that C{str} and
115         C{unicode} instances encountered must be quoted as for XML tag
116         attribute values.
117
118     @return: An iterator which yields C{str}, L{Deferred}, and more iterators
119         of the same type.
120     """
121
122     if isinstance(root, (str, unicode)):
123         yield escapedData(root, inAttribute)
124     elif isinstance(root, slot):
125         slotValue = _getSlotValue(root.name, slotData, root.default)
126         yield _flattenElement(request, slotValue, slotData, renderFactory,
127                 inAttribute)
128     elif isinstance(root, CDATA):
129         yield '<![CDATA['
130         yield escapedCDATA(root.data)
131         yield ']]>'
132     elif isinstance(root, Comment):
133         yield '<!--'
134         yield escapedComment(root.data)
135         yield '-->'
136     elif isinstance(root, Tag):
137         slotData.append(root.slotData)
138         if root.render is not None:
139             rendererName = root.render
140             rootClone = root.clone(False)
141             rootClone.render = None
142             renderMethod = renderFactory.lookupRenderMethod(rendererName)
143             result = renderMethod(request, rootClone)
144             yield _flattenElement(request, result, slotData, renderFactory,
145                     False)
146             slotData.pop()
147             return
148
149         if not root.tagName:
150             yield _flattenElement(request, root.children, slotData, renderFactory, False)
151             return
152
153         yield '<'
154         if isinstance(root.tagName, unicode):
155             tagName = root.tagName.encode('ascii')
156         else:
157             tagName = str(root.tagName)
158         yield tagName
159         for k, v in root.attributes.iteritems():
160             if isinstance(k, unicode):
161                 k = k.encode('ascii')
162             yield ' ' + k + '="'
163             yield _flattenElement(request, v, slotData, renderFactory, True)
164             yield '"'
165         if root.children or tagName not in voidElements:
166             yield '>'
167             yield _flattenElement(request, root.children, slotData, renderFactory, False)
168             yield '</' + tagName + '>'
169         else:
170             yield ' />'
171
172     elif isinstance(root, (tuple, list, GeneratorType)):
173         for element in root:
174             yield _flattenElement(request, element, slotData, renderFactory,
175                     inAttribute)
176     elif isinstance(root, CharRef):
177         yield '&#%d;' % (root.ordinal,)
178     elif isinstance(root, Deferred):
179         yield root.addCallback(
180             lambda result: (result, _flattenElement(request, result, slotData,
181                                              renderFactory, inAttribute)))
182     elif IRenderable.providedBy(root):
183         result = root.render(request)
184         yield _flattenElement(request, result, slotData, root, inAttribute)
185     else:
186         raise UnsupportedType(root)
187
188
189 def _flattenTree(request, root):
190     """
191     Make C{root} into an iterable of C{str} and L{Deferred} by doing a
192     depth first traversal of the tree.
193
194     @param request: A request object which will be passed to
195         L{IRenderable.render}.
196
197     @param root: An object to be made flatter.  This may be of type C{unicode},
198         C{str}, L{slot}, L{Tag}, L{tuple}, L{list}, L{GeneratorType},
199         L{Deferred}, or something providing L{IRenderable}.
200
201     @return: An iterator which yields objects of type C{str} and L{Deferred}.
202         A L{Deferred} is only yielded when one is encountered in the process of
203         flattening C{root}.  The returned iterator must not be iterated again
204         until the L{Deferred} is called back.
205     """
206     stack = [_flattenElement(request, root, [], None, False)]
207     while stack:
208         try:
209             # In Python 2.5, after an exception, a generator's gi_frame is
210             # None.
211             frame = stack[-1].gi_frame
212             element = stack[-1].next()
213         except StopIteration:
214             stack.pop()
215         except Exception, e:
216             stack.pop()
217             roots = []
218             for generator in stack:
219                 roots.append(generator.gi_frame.f_locals['root'])
220             roots.append(frame.f_locals['root'])
221             raise FlattenerError(e, roots, extract_tb(exc_info()[2]))
222         else:
223             if type(element) is str:
224                 yield element
225             elif isinstance(element, Deferred):
226                 def cbx((original, toFlatten)):
227                     stack.append(toFlatten)
228                     return original
229                 yield element.addCallback(cbx)
230             else:
231                 stack.append(element)
232
233
234 def _writeFlattenedData(state, write, result):
235     """
236     Take strings from an iterator and pass them to a writer function.
237
238     @param state: An iterator of C{str} and L{Deferred}.  C{str} instances will
239         be passed to C{write}.  L{Deferred} instances will be waited on before
240         resuming iteration of C{state}.
241
242     @param write: A callable which will be invoked with each C{str}
243         produced by iterating C{state}.
244
245     @param result: A L{Deferred} which will be called back when C{state} has
246         been completely flattened into C{write} or which will be errbacked if
247         an exception in a generator passed to C{state} or an errback from a
248         L{Deferred} from state occurs.
249
250     @return: C{None}
251     """
252     while True:
253         try:
254             element = state.next()
255         except StopIteration:
256             result.callback(None)
257         except:
258             result.errback()
259         else:
260             if type(element) is str:
261                 write(element)
262                 continue
263             else:
264                 def cby(original):
265                     _writeFlattenedData(state, write, result)
266                     return original
267                 element.addCallbacks(cby, result.errback)
268         break
269
270
271 def flatten(request, root, write):
272     """
273     Incrementally write out a string representation of C{root} using C{write}.
274
275     In order to create a string representation, C{root} will be decomposed into
276     simpler objects which will themselves be decomposed and so on until strings
277     or objects which can easily be converted to strings are encountered.
278
279     @param request: A request object which will be passed to the C{render}
280         method of any L{IRenderable} provider which is encountered.
281
282     @param root: An object to be made flatter.  This may be of type C{unicode},
283         C{str}, L{slot}, L{Tag}, L{tuple}, L{list}, L{GeneratorType},
284         L{Deferred}, or something that provides L{IRenderable}.
285
286     @param write: A callable which will be invoked with each C{str}
287         produced by flattening C{root}.
288
289     @return: A L{Deferred} which will be called back when C{root} has
290         been completely flattened into C{write} or which will be errbacked if
291         an unexpected exception occurs.
292     """
293     result = Deferred()
294     state = _flattenTree(request, root)
295     _writeFlattenedData(state, write, result)
296     return result
297
298
299 def flattenString(request, root):
300     """
301     Collate a string representation of C{root} into a single string.
302
303     This is basically gluing L{flatten} to a C{StringIO} and returning the
304     results. See L{flatten} for the exact meanings of C{request} and
305     C{root}.
306
307     @return: A L{Deferred} which will be called back with a single string as
308         its result when C{root} has been completely flattened into C{write} or
309         which will be errbacked if an unexpected exception occurs.
310     """
311     io = StringIO()
312     d = flatten(request, root, io.write)
313     d.addCallback(lambda _: io.getvalue())
314     return d