root/trunk/twisted/web/_flatten.py

Revision 33495, 10.7 KB (checked in by exarkun, 4 months ago)

Merge Twisted dont-unroll-5460

Author: exarkun
Reviewer: MostAwesomeDude
Fixes: #5460

Slightly simplify the implementation of serializing tags and such to HTML
by removing several occurrences of manual loop unrolling. These will be
handled by the serialization trampoline automatically.

Line 
1# -*- test-case-name: twisted.web.test.test_flatten -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Context-free flattener/serializer for rendering Python objects, possibly
7complex or arbitrarily nested, as strings.
8
9"""
10
11from cStringIO import StringIO
12from sys import exc_info
13from types import GeneratorType
14from traceback import extract_tb
15from twisted.internet.defer import Deferred
16from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
17
18from twisted.web.iweb import IRenderable
19from twisted.web._stan import (
20    Tag, slot, voidElements, Comment, CDATA, CharRef)
21
22
23
24def 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
49def 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
65def 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
84def _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
97def _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
189def _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
234def _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
271def 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
299def 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
Note: See TracBrowser for help on using the browser.