1
# -*- test-case-name: twisted.conch.test.test_manhole -*-
2
# Copyright (c) 2001-2007 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'
139
class Manhole(recvline.HistoricRecvLine):
140
"""Mediator between a fancy line source and an interactive interpreter.
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.
151
def __init__(self, namespace=None):
152
recvline.HistoricRecvLine.__init__(self)
153
if namespace is not None:
154
self.namespace = namespace.copy()
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
165
def handle_INT(self):
167
Handle ^C as an interrupt keystroke by resetting the current input
168
variables to their initial state.
172
self.lineBufferIndex = 0
173
self.interpreter.resetBuffer()
175
self.terminal.nextLine()
176
self.terminal.write("KeyboardInterrupt")
177
self.terminal.nextLine()
178
self.terminal.write(self.ps[self.pn])
181
def handle_EOF(self):
183
self.terminal.write('\a')
190
Handle a 'form feed' byte - generally used to request a screen
193
self.terminal.eraseDisplay()
194
self.terminal.cursorHome()
198
def handle_QUIT(self):
199
self.terminal.loseConnection()
202
def _needsNewline(self):
203
w = self.terminal.lastWrite
204
return not w.endswith('\n') and not w.endswith('\x1bE')
206
def addOutput(self, bytes, async=False):
208
self.terminal.eraseLine()
209
self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
211
self.terminal.write(bytes)
214
if self._needsNewline():
215
self.terminal.nextLine()
217
self.terminal.write(self.ps[self.pn])
220
oldBuffer = self.lineBuffer
222
self.lineBufferIndex = 0
224
self._deliverBuffer(oldBuffer)
226
def lineReceived(self, line):
227
more = self.interpreter.push(line)
229
if self._needsNewline():
230
self.terminal.nextLine()
231
self.terminal.write(self.ps[self.pn])
234
"""Colorizer for Python tokens.
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.
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',
250
normalColor = '\x1b[0m'
255
def color(self, type):
256
r = self.typeToColor.get(type, '')
259
def write(self, token, type=None):
260
if token and token != '\r':
263
self.written.append(c)
264
self.written.append(token)
266
self.written.append(self.normalColor)
269
s = ''.join(self.written)
270
return s.strip('\n').splitlines()[-1]
272
def lastColorizedLine(source):
273
"""Tokenize and colorize the given Python source.
275
Returns a VT102-format colorized version of the last line of C{source}.
278
p = TokenPrinter(w.write).printtoken
279
s = StringIO.StringIO(source)
281
tokenize.tokenize(s.readline, p)
285
class ColoredManhole(Manhole):
286
"""A REPL which syntax colors input as users type it.
290
"""Return a string containing the currently entered source.
292
This is only the code which will be considered for execution
295
return ('\n'.join(self.interpreter.buffer) +
297
''.join(self.lineBuffer))
300
def characterReceived(self, ch, moreCharactersComing):
301
if self.mode == 'insert':
302
self.lineBuffer.insert(self.lineBufferIndex, ch)
304
self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
305
self.lineBufferIndex += 1
307
if moreCharactersComing:
308
# Skip it all, we'll get called with another character in
309
# like 2 femtoseconds.
313
# Don't bother to try to color whitespace
314
self.terminal.write(ch)
317
source = self.getSource()
319
# Try to write some junk
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)
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)
330
# And write a new, colorized one.
331
self.terminal.write(self.ps[self.pn] + coloredLine)
333
# And move the cursor to where it belongs
334
n = len(self.lineBuffer) - self.lineBufferIndex
336
self.terminal.cursorBackward(n)