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.

manhole.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. # -*- test-case-name: twisted.conch.test.test_manhole -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Line-input oriented interactive interpreter loop.
  6. Provides classes for handling Python source input and arbitrary output
  7. interactively from a Twisted application. Also included is syntax coloring
  8. code with support for VT102 terminals, control code handling (^C, ^D, ^Q),
  9. and reasonable handling of Deferreds.
  10. @author: Jp Calderone
  11. """
  12. import code
  13. import sys
  14. import tokenize
  15. from io import BytesIO
  16. from traceback import format_exception
  17. from types import TracebackType
  18. from typing import Type
  19. from twisted.conch import recvline
  20. from twisted.internet import defer
  21. from twisted.python.compat import _get_async_param
  22. from twisted.python.htmlizer import TokenPrinter
  23. from twisted.python.monkey import MonkeyPatcher
  24. class FileWrapper:
  25. """
  26. Minimal write-file-like object.
  27. Writes are translated into addOutput calls on an object passed to
  28. __init__. Newlines are also converted from network to local style.
  29. """
  30. softspace = 0
  31. state = "normal"
  32. def __init__(self, o):
  33. self.o = o
  34. def flush(self):
  35. pass
  36. def write(self, data):
  37. self.o.addOutput(data.replace("\r\n", "\n"))
  38. def writelines(self, lines):
  39. self.write("".join(lines))
  40. class ManholeInterpreter(code.InteractiveInterpreter):
  41. """
  42. Interactive Interpreter with special output and Deferred support.
  43. Aside from the features provided by L{code.InteractiveInterpreter}, this
  44. class captures sys.stdout output and redirects it to the appropriate
  45. location (the Manhole protocol instance). It also treats Deferreds
  46. which reach the top-level specially: each is formatted to the user with
  47. a unique identifier and a new callback and errback added to it, each of
  48. which will format the unique identifier and the result with which the
  49. Deferred fires and then pass it on to the next participant in the
  50. callback chain.
  51. """
  52. numDeferreds = 0
  53. def __init__(self, handler, locals=None, filename="<console>"):
  54. code.InteractiveInterpreter.__init__(self, locals)
  55. self._pendingDeferreds = {}
  56. self.handler = handler
  57. self.filename = filename
  58. self.resetBuffer()
  59. self.monkeyPatcher = MonkeyPatcher()
  60. self.monkeyPatcher.addPatch(sys, "displayhook", self.displayhook)
  61. self.monkeyPatcher.addPatch(sys, "excepthook", self.excepthook)
  62. self.monkeyPatcher.addPatch(sys, "stdout", FileWrapper(self.handler))
  63. def resetBuffer(self):
  64. """
  65. Reset the input buffer.
  66. """
  67. self.buffer = []
  68. def push(self, line):
  69. """
  70. Push a line to the interpreter.
  71. The line should not have a trailing newline; it may have
  72. internal newlines. The line is appended to a buffer and the
  73. interpreter's runsource() method is called with the
  74. concatenated contents of the buffer as source. If this
  75. indicates that the command was executed or invalid, the buffer
  76. is reset; otherwise, the command is incomplete, and the buffer
  77. is left as it was after the line was appended. The return
  78. value is 1 if more input is required, 0 if the line was dealt
  79. with in some way (this is the same as runsource()).
  80. @param line: line of text
  81. @type line: L{bytes}
  82. @return: L{bool} from L{code.InteractiveInterpreter.runsource}
  83. """
  84. self.buffer.append(line)
  85. source = b"\n".join(self.buffer)
  86. source = source.decode("utf-8")
  87. more = self.runsource(source, self.filename)
  88. if not more:
  89. self.resetBuffer()
  90. return more
  91. def runcode(self, *a, **kw):
  92. with self.monkeyPatcher:
  93. code.InteractiveInterpreter.runcode(self, *a, **kw)
  94. def excepthook(
  95. self,
  96. excType: Type[BaseException],
  97. excValue: BaseException,
  98. excTraceback: TracebackType,
  99. ) -> None:
  100. """
  101. Format exception tracebacks and write them to the output handler.
  102. """
  103. lines = format_exception(excType, excValue, excTraceback.tb_next)
  104. self.write("".join(lines))
  105. def displayhook(self, obj):
  106. self.locals["_"] = obj
  107. if isinstance(obj, defer.Deferred):
  108. # XXX Ick, where is my "hasFired()" interface?
  109. if hasattr(obj, "result"):
  110. self.write(repr(obj))
  111. elif id(obj) in self._pendingDeferreds:
  112. self.write("<Deferred #%d>" % (self._pendingDeferreds[id(obj)][0],))
  113. else:
  114. d = self._pendingDeferreds
  115. k = self.numDeferreds
  116. d[id(obj)] = (k, obj)
  117. self.numDeferreds += 1
  118. obj.addCallbacks(
  119. self._cbDisplayDeferred,
  120. self._ebDisplayDeferred,
  121. callbackArgs=(k, obj),
  122. errbackArgs=(k, obj),
  123. )
  124. self.write("<Deferred #%d>" % (k,))
  125. elif obj is not None:
  126. self.write(repr(obj))
  127. def _cbDisplayDeferred(self, result, k, obj):
  128. self.write("Deferred #%d called back: %r" % (k, result), True)
  129. del self._pendingDeferreds[id(obj)]
  130. return result
  131. def _ebDisplayDeferred(self, failure, k, obj):
  132. self.write("Deferred #%d failed: %r" % (k, failure.getErrorMessage()), True)
  133. del self._pendingDeferreds[id(obj)]
  134. return failure
  135. def write(self, data, isAsync=None, **kwargs):
  136. isAsync = _get_async_param(isAsync, **kwargs)
  137. self.handler.addOutput(data, isAsync)
  138. CTRL_C = b"\x03"
  139. CTRL_D = b"\x04"
  140. CTRL_BACKSLASH = b"\x1c"
  141. CTRL_L = b"\x0c"
  142. CTRL_A = b"\x01"
  143. CTRL_E = b"\x05"
  144. class Manhole(recvline.HistoricRecvLine):
  145. r"""
  146. Mediator between a fancy line source and an interactive interpreter.
  147. This accepts lines from its transport and passes them on to a
  148. L{ManholeInterpreter}. Control commands (^C, ^D, ^\) are also handled
  149. with something approximating their normal terminal-mode behavior. It
  150. can optionally be constructed with a dict which will be used as the
  151. local namespace for any code executed.
  152. """
  153. namespace = None
  154. def __init__(self, namespace=None):
  155. recvline.HistoricRecvLine.__init__(self)
  156. if namespace is not None:
  157. self.namespace = namespace.copy()
  158. def connectionMade(self):
  159. recvline.HistoricRecvLine.connectionMade(self)
  160. self.interpreter = ManholeInterpreter(self, self.namespace)
  161. self.keyHandlers[CTRL_C] = self.handle_INT
  162. self.keyHandlers[CTRL_D] = self.handle_EOF
  163. self.keyHandlers[CTRL_L] = self.handle_FF
  164. self.keyHandlers[CTRL_A] = self.handle_HOME
  165. self.keyHandlers[CTRL_E] = self.handle_END
  166. self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
  167. def handle_INT(self):
  168. """
  169. Handle ^C as an interrupt keystroke by resetting the current input
  170. variables to their initial state.
  171. """
  172. self.pn = 0
  173. self.lineBuffer = []
  174. self.lineBufferIndex = 0
  175. self.interpreter.resetBuffer()
  176. self.terminal.nextLine()
  177. self.terminal.write(b"KeyboardInterrupt")
  178. self.terminal.nextLine()
  179. self.terminal.write(self.ps[self.pn])
  180. def handle_EOF(self):
  181. if self.lineBuffer:
  182. self.terminal.write(b"\a")
  183. else:
  184. self.handle_QUIT()
  185. def handle_FF(self):
  186. """
  187. Handle a 'form feed' byte - generally used to request a screen
  188. refresh/redraw.
  189. """
  190. self.terminal.eraseDisplay()
  191. self.terminal.cursorHome()
  192. self.drawInputLine()
  193. def handle_QUIT(self):
  194. self.terminal.loseConnection()
  195. def _needsNewline(self):
  196. w = self.terminal.lastWrite
  197. return not w.endswith(b"\n") and not w.endswith(b"\x1bE")
  198. def addOutput(self, data, isAsync=None, **kwargs):
  199. isAsync = _get_async_param(isAsync, **kwargs)
  200. if isAsync:
  201. self.terminal.eraseLine()
  202. self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
  203. self.terminal.write(data)
  204. if isAsync:
  205. if self._needsNewline():
  206. self.terminal.nextLine()
  207. self.terminal.write(self.ps[self.pn])
  208. if self.lineBuffer:
  209. oldBuffer = self.lineBuffer
  210. self.lineBuffer = []
  211. self.lineBufferIndex = 0
  212. self._deliverBuffer(oldBuffer)
  213. def lineReceived(self, line):
  214. more = self.interpreter.push(line)
  215. self.pn = bool(more)
  216. if self._needsNewline():
  217. self.terminal.nextLine()
  218. self.terminal.write(self.ps[self.pn])
  219. class VT102Writer:
  220. """
  221. Colorizer for Python tokens.
  222. A series of tokens are written to instances of this object. Each is
  223. colored in a particular way. The final line of the result of this is
  224. generally added to the output.
  225. """
  226. typeToColor = {
  227. "identifier": b"\x1b[31m",
  228. "keyword": b"\x1b[32m",
  229. "parameter": b"\x1b[33m",
  230. "variable": b"\x1b[1;33m",
  231. "string": b"\x1b[35m",
  232. "number": b"\x1b[36m",
  233. "op": b"\x1b[37m",
  234. }
  235. normalColor = b"\x1b[0m"
  236. def __init__(self):
  237. self.written = []
  238. def color(self, type):
  239. r = self.typeToColor.get(type, b"")
  240. return r
  241. def write(self, token, type=None):
  242. if token and token != b"\r":
  243. c = self.color(type)
  244. if c:
  245. self.written.append(c)
  246. self.written.append(token)
  247. if c:
  248. self.written.append(self.normalColor)
  249. def __bytes__(self):
  250. s = b"".join(self.written)
  251. return s.strip(b"\n").splitlines()[-1]
  252. if bytes == str:
  253. # Compat with Python 2.7
  254. __str__ = __bytes__
  255. def lastColorizedLine(source):
  256. """
  257. Tokenize and colorize the given Python source.
  258. Returns a VT102-format colorized version of the last line of C{source}.
  259. @param source: Python source code
  260. @type source: L{str} or L{bytes}
  261. @return: L{bytes} of colorized source
  262. """
  263. if not isinstance(source, bytes):
  264. source = source.encode("utf-8")
  265. w = VT102Writer()
  266. p = TokenPrinter(w.write).printtoken
  267. s = BytesIO(source)
  268. for token in tokenize.tokenize(s.readline):
  269. (tokenType, string, start, end, line) = token
  270. p(tokenType, string, start, end, line)
  271. return bytes(w)
  272. class ColoredManhole(Manhole):
  273. """
  274. A REPL which syntax colors input as users type it.
  275. """
  276. def getSource(self):
  277. """
  278. Return a string containing the currently entered source.
  279. This is only the code which will be considered for execution
  280. next.
  281. """
  282. return b"\n".join(self.interpreter.buffer) + b"\n" + b"".join(self.lineBuffer)
  283. def characterReceived(self, ch, moreCharactersComing):
  284. if self.mode == "insert":
  285. self.lineBuffer.insert(self.lineBufferIndex, ch)
  286. else:
  287. self.lineBuffer[self.lineBufferIndex : self.lineBufferIndex + 1] = [ch]
  288. self.lineBufferIndex += 1
  289. if moreCharactersComing:
  290. # Skip it all, we'll get called with another character in
  291. # like 2 femtoseconds.
  292. return
  293. if ch == b" ":
  294. # Don't bother to try to color whitespace
  295. self.terminal.write(ch)
  296. return
  297. source = self.getSource()
  298. # Try to write some junk
  299. try:
  300. coloredLine = lastColorizedLine(source)
  301. except tokenize.TokenError:
  302. # We couldn't do it. Strange. Oh well, just add the character.
  303. self.terminal.write(ch)
  304. else:
  305. # Success! Clear the source on this line.
  306. self.terminal.eraseLine()
  307. self.terminal.cursorBackward(
  308. len(self.lineBuffer) + len(self.ps[self.pn]) - 1
  309. )
  310. # And write a new, colorized one.
  311. self.terminal.write(self.ps[self.pn] + coloredLine)
  312. # And move the cursor to where it belongs
  313. n = len(self.lineBuffer) - self.lineBufferIndex
  314. if n:
  315. self.terminal.cursorBackward(n)