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