1 # -*- test-case-name: twisted.conch.test.test_manhole -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 Line-input oriented interactive interpreter loop.
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.
16 import code, sys, StringIO, tokenize
18 from twisted.conch import recvline
20 from twisted.internet import defer
21 from twisted.python.htmlizer import TokenPrinter
24 """Minimal write-file-like object.
26 Writes are translated into addOutput calls on an object passed to
27 __init__. Newlines are also converted from network to local style.
33 def __init__(self, o):
39 def write(self, data):
40 self.o.addOutput(data.replace('\r\n', '\n'))
42 def writelines(self, lines):
43 self.write(''.join(lines))
45 class ManholeInterpreter(code.InteractiveInterpreter):
46 """Interactive Interpreter with special output and Deferred support.
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
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
66 def resetBuffer(self):
67 """Reset the input buffer."""
71 """Push a line to the interpreter.
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()).
84 self.buffer.append(line)
85 source = "\n".join(self.buffer)
86 more = self.runsource(source, self.filename)
91 def runcode(self, *a, **kw):
92 orighook, sys.displayhook = sys.displayhook, self.displayhook
94 origout, sys.stdout = sys.stdout, FileWrapper(self.handler)
96 code.InteractiveInterpreter.runcode(self, *a, **kw)
100 sys.displayhook = orighook
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],))
111 d = self._pendingDeferreds
112 k = self.numDeferreds
113 d[id(obj)] = (k, obj)
114 self.numDeferreds += 1
115 obj.addCallbacks(self._cbDisplayDeferred, self._ebDisplayDeferred,
116 callbackArgs=(k, obj), errbackArgs=(k, obj))
117 self.write("<Deferred #%d>" % (k,))
118 elif obj is not None:
119 self.write(repr(obj))
121 def _cbDisplayDeferred(self, result, k, obj):
122 self.write("Deferred #%d called back: %r" % (k, result), True)
123 del self._pendingDeferreds[id(obj)]
126 def _ebDisplayDeferred(self, failure, k, obj):
127 self.write("Deferred #%d failed: %r" % (k, failure.getErrorMessage()), True)
128 del self._pendingDeferreds[id(obj)]
131 def write(self, data, async=False):
132 self.handler.addOutput(data, async)
136 CTRL_BACKSLASH = '\x1c'
141 class Manhole(recvline.HistoricRecvLine):
142 """Mediator between a fancy line source and an interactive interpreter.
144 This accepts lines from its transport and passes them on to a
145 L{ManholeInterpreter}. Control commands (^C, ^D, ^\) are also handled
146 with something approximating their normal terminal-mode behavior. It
147 can optionally be constructed with a dict which will be used as the
148 local namespace for any code executed.
153 def __init__(self, namespace=None):
154 recvline.HistoricRecvLine.__init__(self)
155 if namespace is not None:
156 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
169 def handle_INT(self):
171 Handle ^C as an interrupt keystroke by resetting the current input
172 variables to their initial state.
176 self.lineBufferIndex = 0
177 self.interpreter.resetBuffer()
179 self.terminal.nextLine()
180 self.terminal.write("KeyboardInterrupt")
181 self.terminal.nextLine()
182 self.terminal.write(self.ps[self.pn])
185 def handle_EOF(self):
187 self.terminal.write('\a')
194 Handle a 'form feed' byte - generally used to request a screen
197 self.terminal.eraseDisplay()
198 self.terminal.cursorHome()
202 def handle_QUIT(self):
203 self.terminal.loseConnection()
206 def _needsNewline(self):
207 w = self.terminal.lastWrite
208 return not w.endswith('\n') and not w.endswith('\x1bE')
210 def addOutput(self, bytes, async=False):
212 self.terminal.eraseLine()
213 self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
215 self.terminal.write(bytes)
218 if self._needsNewline():
219 self.terminal.nextLine()
221 self.terminal.write(self.ps[self.pn])
224 oldBuffer = self.lineBuffer
226 self.lineBufferIndex = 0
228 self._deliverBuffer(oldBuffer)
230 def lineReceived(self, line):
231 more = self.interpreter.push(line)
233 if self._needsNewline():
234 self.terminal.nextLine()
235 self.terminal.write(self.ps[self.pn])
238 """Colorizer for Python tokens.
240 A series of tokens are written to instances of this object. Each is
241 colored in a particular way. The final line of the result of this is
242 generally added to the output.
246 'identifier': '\x1b[31m',
247 'keyword': '\x1b[32m',
248 'parameter': '\x1b[33m',
249 'variable': '\x1b[1;33m',
250 'string': '\x1b[35m',
251 'number': '\x1b[36m',
254 normalColor = '\x1b[0m'
259 def color(self, type):
260 r = self.typeToColor.get(type, '')
263 def write(self, token, type=None):
264 if token and token != '\r':
267 self.written.append(c)
268 self.written.append(token)
270 self.written.append(self.normalColor)
273 s = ''.join(self.written)
274 return s.strip('\n').splitlines()[-1]
276 def lastColorizedLine(source):
277 """Tokenize and colorize the given Python source.
279 Returns a VT102-format colorized version of the last line of C{source}.
282 p = TokenPrinter(w.write).printtoken
283 s = StringIO.StringIO(source)
285 tokenize.tokenize(s.readline, p)
289 class ColoredManhole(Manhole):
290 """A REPL which syntax colors input as users type it.
294 """Return a string containing the currently entered source.
296 This is only the code which will be considered for execution
299 return ('\n'.join(self.interpreter.buffer) +
301 ''.join(self.lineBuffer))
304 def characterReceived(self, ch, moreCharactersComing):
305 if self.mode == 'insert':
306 self.lineBuffer.insert(self.lineBufferIndex, ch)
308 self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
309 self.lineBufferIndex += 1
311 if moreCharactersComing:
312 # Skip it all, we'll get called with another character in
313 # like 2 femtoseconds.
317 # Don't bother to try to color whitespace
318 self.terminal.write(ch)
321 source = self.getSource()
323 # Try to write some junk
325 coloredLine = lastColorizedLine(source)
326 except tokenize.TokenError:
327 # We couldn't do it. Strange. Oh well, just add the character.
328 self.terminal.write(ch)
330 # Success! Clear the source on this line.
331 self.terminal.eraseLine()
332 self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]) - 1)
334 # And write a new, colorized one.
335 self.terminal.write(self.ps[self.pn] + coloredLine)
337 # And move the cursor to where it belongs
338 n = len(self.lineBuffer) - self.lineBufferIndex
340 self.terminal.cursorBackward(n)