1 # -*- test-case-name: twisted.web.test.test_flatten -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 Context-free flattener/serializer for rendering Python objects, possibly
7 complex or arbitrarily nested, as strings.
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
18 from twisted.web.iweb import IRenderable
19 from twisted.web._stan import (
20 Tag, slot, voidElements, Comment, CDATA, CharRef)
24 def escapedData(data, inAttribute):
26 Escape a string for inclusion in a document.
28 @type data: C{str} or C{unicode}
29 @param data: The string to escape.
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.
36 @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
39 if isinstance(data, unicode):
40 data = data.encode('utf-8')
41 data = data.replace('&', '&'
43 ).replace('>', '>')
45 data = data.replace('"', '"')
49 def escapedCDATA(data):
51 Escape CDATA for inclusion in a document.
53 @type data: C{str} or C{unicode}
54 @param data: The string to escape.
57 @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
60 if isinstance(data, unicode):
61 data = data.encode('utf-8')
62 return data.replace(']]>', ']]]]><![CDATA[>')
65 def escapedComment(data):
67 Escape a comment for inclusion in a document.
69 @type data: C{str} or C{unicode}
70 @param data: The string to escape.
73 @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
76 if isinstance(data, unicode):
77 data = data.encode('utf-8')
78 data = data.replace('--', '- - ').replace('>', '>')
79 if data and data[-1] == '-':
84 def _getSlotValue(name, slotData, default=None):
86 Find the value of the named slot in the given stack of slot data.
88 for slotFrame in slotData[::-1]:
89 if slotFrame is not None and name in slotFrame:
90 return slotFrame[name]
92 if default is not None:
94 raise UnfilledSlot(name)
97 def _flattenElement(request, root, slotData, renderFactory, inAttribute):
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.
102 @param request: A request object which will be passed to
103 L{IRenderable.render}.
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}.
109 @param slotData: A C{list} of C{dict} mapping C{str} slot names to data
110 with which those slots will be replaced.
112 @param renderFactory: If not C{None}, An object that provides L{IRenderable}.
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
118 @return: An iterator which yields C{str}, L{Deferred}, and more iterators
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,
128 elif isinstance(root, CDATA):
130 yield escapedCDATA(root.data)
132 elif isinstance(root, Comment):
134 yield escapedComment(root.data)
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,
150 yield _flattenElement(request, root.children, slotData, renderFactory, False)
154 if isinstance(root.tagName, unicode):
155 tagName = root.tagName.encode('ascii')
157 tagName = str(root.tagName)
159 for k, v in root.attributes.iteritems():
160 if isinstance(k, unicode):
161 k = k.encode('ascii')
163 yield _flattenElement(request, v, slotData, renderFactory, True)
165 if root.children or tagName not in voidElements:
167 yield _flattenElement(request, root.children, slotData, renderFactory, False)
168 yield '</' + tagName + '>'
172 elif isinstance(root, (tuple, list, GeneratorType)):
174 yield _flattenElement(request, element, slotData, renderFactory,
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)
186 raise UnsupportedType(root)
189 def _flattenTree(request, root):
191 Make C{root} into an iterable of C{str} and L{Deferred} by doing a
192 depth first traversal of the tree.
194 @param request: A request object which will be passed to
195 L{IRenderable.render}.
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}.
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.
206 stack = [_flattenElement(request, root, [], None, False)]
209 # In Python 2.5, after an exception, a generator's gi_frame is
211 frame = stack[-1].gi_frame
212 element = stack[-1].next()
213 except StopIteration:
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]))
223 if type(element) is str:
225 elif isinstance(element, Deferred):
226 def cbx((original, toFlatten)):
227 stack.append(toFlatten)
229 yield element.addCallback(cbx)
231 stack.append(element)
234 def _writeFlattenedData(state, write, result):
236 Take strings from an iterator and pass them to a writer function.
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}.
242 @param write: A callable which will be invoked with each C{str}
243 produced by iterating C{state}.
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.
254 element = state.next()
255 except StopIteration:
256 result.callback(None)
260 if type(element) is str:
265 _writeFlattenedData(state, write, result)
267 element.addCallbacks(cby, result.errback)
271 def flatten(request, root, write):
273 Incrementally write out a string representation of C{root} using C{write}.
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.
279 @param request: A request object which will be passed to the C{render}
280 method of any L{IRenderable} provider which is encountered.
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}.
286 @param write: A callable which will be invoked with each C{str}
287 produced by flattening C{root}.
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.
294 state = _flattenTree(request, root)
295 _writeFlattenedData(state, write, result)
299 def flattenString(request, root):
301 Collate a string representation of C{root} into a single string.
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
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.
312 d = flatten(request, root, io.write)
313 d.addCallback(lambda _: io.getvalue())