| 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('<', '<' |
|---|
| 43 | ).replace('>', '>') |
|---|
| 44 | if inAttribute: |
|---|
| 45 | data = data.replace('"', '"') |
|---|
| 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('>', '>') |
|---|
| 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 |
|---|