Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

_flatten.py 17KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. # -*- test-case-name: twisted.web.test.test_flatten,twisted.web.test.test_template -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Context-free flattener/serializer for rendering Python objects, possibly
  6. complex or arbitrarily nested, as strings.
  7. """
  8. from inspect import iscoroutine
  9. from io import BytesIO
  10. from sys import exc_info
  11. from traceback import extract_tb
  12. from types import GeneratorType
  13. from typing import (
  14. Any,
  15. Callable,
  16. Coroutine,
  17. Generator,
  18. List,
  19. Mapping,
  20. Optional,
  21. Sequence,
  22. Tuple,
  23. TypeVar,
  24. Union,
  25. cast,
  26. )
  27. from twisted.internet.defer import Deferred, ensureDeferred
  28. from twisted.python.compat import nativeString
  29. from twisted.python.failure import Failure
  30. from twisted.web._stan import CDATA, CharRef, Comment, Tag, slot, voidElements
  31. from twisted.web.error import FlattenerError, UnfilledSlot, UnsupportedType
  32. from twisted.web.iweb import IRenderable, IRequest
  33. T = TypeVar("T")
  34. FlattenableRecursive = Any
  35. """
  36. For documentation purposes, read C{FlattenableRecursive} as L{Flattenable}.
  37. However, since mypy doesn't support recursive type definitions (yet?),
  38. we'll put Any in the actual definition.
  39. """
  40. Flattenable = Union[
  41. bytes,
  42. str,
  43. slot,
  44. CDATA,
  45. Comment,
  46. Tag,
  47. Tuple[FlattenableRecursive, ...],
  48. List[FlattenableRecursive],
  49. Generator[FlattenableRecursive, None, None],
  50. CharRef,
  51. Deferred[FlattenableRecursive],
  52. Coroutine[Deferred[FlattenableRecursive], object, FlattenableRecursive],
  53. IRenderable,
  54. ]
  55. """
  56. Type alias containing all types that can be flattened by L{flatten()}.
  57. """
  58. # The maximum number of bytes to synchronously accumulate in the flattener
  59. # buffer before delivering them onwards.
  60. BUFFER_SIZE = 2 ** 16
  61. def escapeForContent(data: Union[bytes, str]) -> bytes:
  62. """
  63. Escape some character or UTF-8 byte data for inclusion in an HTML or XML
  64. document, by replacing metacharacters (C{&<>}) with their entity
  65. equivalents (C{&amp;&lt;&gt;}).
  66. This is used as an input to L{_flattenElement}'s C{dataEscaper} parameter.
  67. @param data: The string to escape.
  68. @return: The quoted form of C{data}. If C{data} is L{str}, return a utf-8
  69. encoded string.
  70. """
  71. if isinstance(data, str):
  72. data = data.encode("utf-8")
  73. data = data.replace(b"&", b"&amp;").replace(b"<", b"&lt;").replace(b">", b"&gt;")
  74. return data
  75. def attributeEscapingDoneOutside(data: Union[bytes, str]) -> bytes:
  76. """
  77. Escape some character or UTF-8 byte data for inclusion in the top level of
  78. an attribute. L{attributeEscapingDoneOutside} actually passes the data
  79. through unchanged, because L{writeWithAttributeEscaping} handles the
  80. quoting of the text within attributes outside the generator returned by
  81. L{_flattenElement}; this is used as the C{dataEscaper} argument to that
  82. L{_flattenElement} call so that that generator does not redundantly escape
  83. its text output.
  84. @param data: The string to escape.
  85. @return: The string, unchanged, except for encoding.
  86. """
  87. if isinstance(data, str):
  88. return data.encode("utf-8")
  89. return data
  90. def writeWithAttributeEscaping(
  91. write: Callable[[bytes], object]
  92. ) -> Callable[[bytes], None]:
  93. """
  94. Decorate a C{write} callable so that all output written is properly quoted
  95. for inclusion within an XML attribute value.
  96. If a L{Tag <twisted.web.template.Tag>} C{x} is flattened within the context
  97. of the contents of another L{Tag <twisted.web.template.Tag>} C{y}, the
  98. metacharacters (C{<>&"}) delimiting C{x} should be passed through
  99. unchanged, but the textual content of C{x} should still be quoted, as
  100. usual. For example: C{<y><x>&amp;</x></y>}. That is the default behavior
  101. of L{_flattenElement} when L{escapeForContent} is passed as the
  102. C{dataEscaper}.
  103. However, when a L{Tag <twisted.web.template.Tag>} C{x} is flattened within
  104. the context of an I{attribute} of another L{Tag <twisted.web.template.Tag>}
  105. C{y}, then the metacharacters delimiting C{x} should be quoted so that it
  106. can be parsed from the attribute's value. In the DOM itself, this is not a
  107. valid thing to do, but given that renderers and slots may be freely moved
  108. around in a L{twisted.web.template} template, it is a condition which may
  109. arise in a document and must be handled in a way which produces valid
  110. output. So, for example, you should be able to get C{<y attr="&lt;x /&gt;"
  111. />}. This should also be true for other XML/HTML meta-constructs such as
  112. comments and CDATA, so if you were to serialize a L{comment
  113. <twisted.web.template.Comment>} in an attribute you should get C{<y
  114. attr="&lt;-- comment --&gt;" />}. Therefore in order to capture these
  115. meta-characters, flattening is done with C{write} callable that is wrapped
  116. with L{writeWithAttributeEscaping}.
  117. The final case, and hopefully the much more common one as compared to
  118. serializing L{Tag <twisted.web.template.Tag>} and arbitrary L{IRenderable}
  119. objects within an attribute, is to serialize a simple string, and those
  120. should be passed through for L{writeWithAttributeEscaping} to quote
  121. without applying a second, redundant level of quoting.
  122. @param write: A callable which will be invoked with the escaped L{bytes}.
  123. @return: A callable that writes data with escaping.
  124. """
  125. def _write(data: bytes) -> None:
  126. write(escapeForContent(data).replace(b'"', b"&quot;"))
  127. return _write
  128. def escapedCDATA(data: Union[bytes, str]) -> bytes:
  129. """
  130. Escape CDATA for inclusion in a document.
  131. @param data: The string to escape.
  132. @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
  133. encoded string.
  134. """
  135. if isinstance(data, str):
  136. data = data.encode("utf-8")
  137. return data.replace(b"]]>", b"]]]]><![CDATA[>")
  138. def escapedComment(data: Union[bytes, str]) -> bytes:
  139. """
  140. Escape a comment for inclusion in a document.
  141. @param data: The string to escape.
  142. @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
  143. encoded string.
  144. """
  145. if isinstance(data, str):
  146. data = data.encode("utf-8")
  147. data = data.replace(b"--", b"- - ").replace(b">", b"&gt;")
  148. if data and data[-1:] == b"-":
  149. data += b" "
  150. return data
  151. def _getSlotValue(
  152. name: str,
  153. slotData: Sequence[Optional[Mapping[str, Flattenable]]],
  154. default: Optional[Flattenable] = None,
  155. ) -> Flattenable:
  156. """
  157. Find the value of the named slot in the given stack of slot data.
  158. """
  159. for slotFrame in slotData[::-1]:
  160. if slotFrame is not None and name in slotFrame:
  161. return slotFrame[name]
  162. else:
  163. if default is not None:
  164. return default
  165. raise UnfilledSlot(name)
  166. def _fork(d: Deferred[T]) -> Deferred[T]:
  167. """
  168. Create a new L{Deferred} based on C{d} that will fire and fail with C{d}'s
  169. result or error, but will not modify C{d}'s callback type.
  170. """
  171. d2: Deferred[T] = Deferred(lambda _: d.cancel())
  172. def callback(result: T) -> T:
  173. d2.callback(result)
  174. return result
  175. def errback(failure: Failure) -> Failure:
  176. d2.errback(failure)
  177. return failure
  178. d.addCallbacks(callback, errback)
  179. return d2
  180. def _flattenElement(
  181. request: Optional[IRequest],
  182. root: Flattenable,
  183. write: Callable[[bytes], object],
  184. slotData: List[Optional[Mapping[str, Flattenable]]],
  185. renderFactory: Optional[IRenderable],
  186. dataEscaper: Callable[[Union[bytes, str]], bytes],
  187. # This is annotated as Generator[T, None, None] instead of Iterator[T]
  188. # because mypy does not consider an Iterator to be an instance of
  189. # GeneratorType.
  190. ) -> Generator[Union[Generator, Deferred[Flattenable]], None, None]:
  191. """
  192. Make C{root} slightly more flat by yielding all its immediate contents as
  193. strings, deferreds or generators that are recursive calls to itself.
  194. @param request: A request object which will be passed to
  195. L{IRenderable.render}.
  196. @param root: An object to be made flatter. This may be of type C{unicode},
  197. L{str}, L{slot}, L{Tag <twisted.web.template.Tag>}, L{tuple}, L{list},
  198. L{types.GeneratorType}, L{Deferred}, or an object that implements
  199. L{IRenderable}.
  200. @param write: A callable which will be invoked with each L{bytes} produced
  201. by flattening C{root}.
  202. @param slotData: A L{list} of L{dict} mapping L{str} slot names to data
  203. with which those slots will be replaced.
  204. @param renderFactory: If not L{None}, an object that provides
  205. L{IRenderable}.
  206. @param dataEscaper: A 1-argument callable which takes L{bytes} or
  207. L{unicode} and returns L{bytes}, quoted as appropriate for the
  208. rendering context. This is really only one of two values:
  209. L{attributeEscapingDoneOutside} or L{escapeForContent}, depending on
  210. whether the rendering context is within an attribute or not. See the
  211. explanation in L{writeWithAttributeEscaping}.
  212. @return: An iterator that eventually writes L{bytes} to C{write}.
  213. It can yield other iterators or L{Deferred}s; if it yields another
  214. iterator, the caller will iterate it; if it yields a L{Deferred},
  215. the result of that L{Deferred} will be another generator, in which
  216. case it is iterated. See L{_flattenTree} for the trampoline that
  217. consumes said values.
  218. """
  219. def keepGoing(
  220. newRoot: Flattenable,
  221. dataEscaper: Callable[[Union[bytes, str]], bytes] = dataEscaper,
  222. renderFactory: Optional[IRenderable] = renderFactory,
  223. write: Callable[[bytes], object] = write,
  224. ) -> Generator[Union[Generator, Deferred[Generator]], None, None]:
  225. return _flattenElement(
  226. request, newRoot, write, slotData, renderFactory, dataEscaper
  227. )
  228. def keepGoingAsync(result: Deferred[Flattenable]) -> Deferred[Flattenable]:
  229. return result.addCallback(keepGoing)
  230. if isinstance(root, (bytes, str)):
  231. write(dataEscaper(root))
  232. elif isinstance(root, slot):
  233. slotValue = _getSlotValue(root.name, slotData, root.default)
  234. yield keepGoing(slotValue)
  235. elif isinstance(root, CDATA):
  236. write(b"<![CDATA[")
  237. write(escapedCDATA(root.data))
  238. write(b"]]>")
  239. elif isinstance(root, Comment):
  240. write(b"<!--")
  241. write(escapedComment(root.data))
  242. write(b"-->")
  243. elif isinstance(root, Tag):
  244. slotData.append(root.slotData)
  245. rendererName = root.render
  246. if rendererName is not None:
  247. if renderFactory is None:
  248. raise ValueError(
  249. f'Tag wants to be rendered by method "{rendererName}" '
  250. f"but is not contained in any IRenderable"
  251. )
  252. rootClone = root.clone(False)
  253. rootClone.render = None
  254. renderMethod = renderFactory.lookupRenderMethod(rendererName)
  255. result = renderMethod(request, rootClone)
  256. yield keepGoing(result)
  257. slotData.pop()
  258. return
  259. if not root.tagName:
  260. yield keepGoing(root.children)
  261. return
  262. write(b"<")
  263. if isinstance(root.tagName, str):
  264. tagName = root.tagName.encode("ascii")
  265. else:
  266. tagName = root.tagName
  267. write(tagName)
  268. for k, v in root.attributes.items():
  269. if isinstance(k, str):
  270. k = k.encode("ascii")
  271. write(b" " + k + b'="')
  272. # Serialize the contents of the attribute, wrapping the results of
  273. # that serialization so that _everything_ is quoted.
  274. yield keepGoing(
  275. v, attributeEscapingDoneOutside, write=writeWithAttributeEscaping(write)
  276. )
  277. write(b'"')
  278. if root.children or nativeString(tagName) not in voidElements:
  279. write(b">")
  280. # Regardless of whether we're in an attribute or not, switch back
  281. # to the escapeForContent dataEscaper. The contents of a tag must
  282. # be quoted no matter what; in the top-level document, just so
  283. # they're valid, and if they're within an attribute, they have to
  284. # be quoted so that after applying the *un*-quoting required to re-
  285. # parse the tag within the attribute, all the quoting is still
  286. # correct.
  287. yield keepGoing(root.children, escapeForContent)
  288. write(b"</" + tagName + b">")
  289. else:
  290. write(b" />")
  291. elif isinstance(root, (tuple, list, GeneratorType)):
  292. for element in root:
  293. yield keepGoing(element)
  294. elif isinstance(root, CharRef):
  295. escaped = "&#%d;" % (root.ordinal,)
  296. write(escaped.encode("ascii"))
  297. elif isinstance(root, Deferred):
  298. yield keepGoingAsync(_fork(root))
  299. elif iscoroutine(root):
  300. yield keepGoingAsync(
  301. Deferred.fromCoroutine(
  302. cast(Coroutine[Deferred[Flattenable], object, Flattenable], root)
  303. )
  304. )
  305. elif IRenderable.providedBy(root):
  306. result = root.render(request)
  307. yield keepGoing(result, renderFactory=root)
  308. else:
  309. raise UnsupportedType(root)
  310. async def _flattenTree(
  311. request: Optional[IRequest], root: Flattenable, write: Callable[[bytes], object]
  312. ) -> None:
  313. """
  314. Make C{root} into an iterable of L{bytes} and L{Deferred} by doing a depth
  315. first traversal of the tree.
  316. @param request: A request object which will be passed to
  317. L{IRenderable.render}.
  318. @param root: An object to be made flatter. This may be of type C{unicode},
  319. L{bytes}, L{slot}, L{Tag <twisted.web.template.Tag>}, L{tuple},
  320. L{list}, L{types.GeneratorType}, L{Deferred}, or something providing
  321. L{IRenderable}.
  322. @param write: A callable which will be invoked with each L{bytes} produced
  323. by flattening C{root}.
  324. @return: A C{Deferred}-returning coroutine that resolves to C{None}.
  325. """
  326. buf = []
  327. bufSize = 0
  328. # Accumulate some bytes up to the buffer size so that we don't annoy the
  329. # upstream writer with a million tiny string.
  330. def bufferedWrite(bs: bytes) -> None:
  331. nonlocal bufSize
  332. buf.append(bs)
  333. bufSize += len(bs)
  334. if bufSize >= BUFFER_SIZE:
  335. flushBuffer()
  336. # Deliver the buffered content to the upstream writer as a single string.
  337. # This is how a "big enough" buffer gets delivered, how a buffer of any
  338. # size is delivered before execution is suspended to wait for an
  339. # asynchronous value, and how anything left in the buffer when we're
  340. # finished is delivered.
  341. def flushBuffer() -> None:
  342. nonlocal bufSize
  343. if bufSize > 0:
  344. write(b"".join(buf))
  345. del buf[:]
  346. bufSize = 0
  347. stack: List[Generator] = [
  348. _flattenElement(request, root, bufferedWrite, [], None, escapeForContent)
  349. ]
  350. while stack:
  351. try:
  352. frame = stack[-1].gi_frame
  353. element = next(stack[-1])
  354. if isinstance(element, Deferred):
  355. # Before suspending flattening for an unknown amount of time,
  356. # flush whatever data we have collected so far.
  357. flushBuffer()
  358. element = await element
  359. except StopIteration:
  360. stack.pop()
  361. except Exception as e:
  362. stack.pop()
  363. roots = []
  364. for generator in stack:
  365. roots.append(generator.gi_frame.f_locals["root"])
  366. roots.append(frame.f_locals["root"])
  367. raise FlattenerError(e, roots, extract_tb(exc_info()[2]))
  368. else:
  369. stack.append(element)
  370. # Flush any data that remains in the buffer before finishing.
  371. flushBuffer()
  372. def flatten(
  373. request: Optional[IRequest], root: Flattenable, write: Callable[[bytes], object]
  374. ) -> Deferred[None]:
  375. """
  376. Incrementally write out a string representation of C{root} using C{write}.
  377. In order to create a string representation, C{root} will be decomposed into
  378. simpler objects which will themselves be decomposed and so on until strings
  379. or objects which can easily be converted to strings are encountered.
  380. @param request: A request object which will be passed to the C{render}
  381. method of any L{IRenderable} provider which is encountered.
  382. @param root: An object to be made flatter. This may be of type L{str},
  383. L{bytes}, L{slot}, L{Tag <twisted.web.template.Tag>}, L{tuple},
  384. L{list}, L{types.GeneratorType}, L{Deferred}, or something that
  385. provides L{IRenderable}.
  386. @param write: A callable which will be invoked with each L{bytes} produced
  387. by flattening C{root}.
  388. @return: A L{Deferred} which will be called back with C{None} when C{root}
  389. has been completely flattened into C{write} or which will be errbacked
  390. if an unexpected exception occurs.
  391. """
  392. return ensureDeferred(_flattenTree(request, root, write))
  393. def flattenString(request: Optional[IRequest], root: Flattenable) -> Deferred[bytes]:
  394. """
  395. Collate a string representation of C{root} into a single string.
  396. This is basically gluing L{flatten} to an L{io.BytesIO} and returning
  397. the results. See L{flatten} for the exact meanings of C{request} and
  398. C{root}.
  399. @return: A L{Deferred} which will be called back with a single UTF-8 encoded
  400. string as its result when C{root} has been completely flattened or which
  401. will be errbacked if an unexpected exception occurs.
  402. """
  403. io = BytesIO()
  404. d = flatten(request, root, io.write)
  405. d.addCallback(lambda _: io.getvalue())
  406. return cast(Deferred[bytes], d)