manhole.py 12 KB

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