1
# -*- test-case-name: twisted.conch.test.test_manhole -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
5
"""Line-input oriented interactive interpreter loop.
7
Provides classes for handling Python source input and arbitrary output
8
interactively from a Twisted application. Also included is syntax coloring
9
code with support for VT102 terminals, control code handling (^C, ^D, ^Q),
10
and reasonable handling of Deferreds.
12
API Stability: Unstable
14
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
17
import code, sys, StringIO, tokenize
19
from twisted.conch import recvline
21
from twisted.internet import defer
22
from twisted.python.htmlizer import TokenPrinter
25
"""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.
34
def __init__(self, o):
40
def write(self, data):
41
self.o.addOutput(data.replace('\r\n', '\n'))
43
def writelines(self, lines):
44
self.write(''.join(lines))
46
class ManholeInterpreter(code.InteractiveInterpreter):
47
"""Interactive Interpreter with special output and Deferred support.
49
Aside from the features provided by L{code.InteractiveInterpreter}, this
50
class captures sys.stdout output and redirects it to the appropriate
51
location (the Manhole protocol instance). It also treats Deferreds
52
which reach the top-level specially: each is formatted to the user with
53
a unique identifier and a new callback and errback added to it, each of
54
which will format the unique identifier and the result with which the
55
Deferred fires and then pass it on to the next participant in the
60
def __init__(self, handler, locals=None, filename="<console>"):
61
code.InteractiveInterpreter.__init__(self, locals)
62
self._pendingDeferreds = {}
63
self.handler = handler
64
self.filename = filename
67
def resetBuffer(self):
68
"""Reset the input buffer."""
72
"""Push a line to the interpreter.
74
The line should not have a trailing newline; it may have
75
internal newlines. The line is appended to a buffer and the
76
interpreter's runsource() method is called with the
77
concatenated contents of the buffer as source. If this
78
indicates that the command was executed or invalid, the buffer
79
is reset; otherwise, the command is incomplete, and the buffer
80
is left as it was after the line was appended. The return
81
value is 1 if more input is required, 0 if the line was dealt
82
with in some way (this is the same as runsource()).
85
self.buffer.append(line)
86
source = "\n".join(self.buffer)
87
more = self.runsource(source, self.filename)
92
def runcode(self, *a, **kw):
93
orighook, sys.displayhook = sys.displayhook, self.displayhook
95
origout, sys.stdout = sys.stdout, FileWrapper(self.handler)
97
code.InteractiveInterpreter.runcode(self, *a, **kw)
101
sys.displayhook = orighook
103
def displayhook(self, obj):
104
self.locals['_'] = obj
105
if isinstance(obj, defer.Deferred):
106
# XXX Ick, where is my "hasFired()" interface?
107
if hasattr(obj, "result"):
108
self.write(repr(obj))
109
elif id(obj) in self._pendingDeferreds:
110
self.write("<Deferred #%d>" % (self._pendingDeferreds[id(obj)][0],))
112
d = self._pendingDeferreds
113
k = self.numDeferreds
114
d[id(obj)] = (k, obj)
115
self.numDeferreds += 1
116
obj.addCallbacks(self._cbDisplayDeferred, self._ebDisplayDeferred,
117
callbackArgs=(k, obj), errbackArgs=(k, obj))
118
self.write("<Deferred #%d>" % (k,))
119
elif obj is not None:
120
self.write(repr(obj))
122
def _cbDisplayDeferred(self, result, k, obj):
123
self.write("Deferred #%d called back: %r" % (k, result), True)
124
del self._pendingDeferreds[id(obj)]
127
def _ebDisplayDeferred(self, failure, k, obj):
128
self.write("Deferred #%d failed: %r" % (k, failure.getErrorMessage()), True)
129
del self._pendingDeferreds[id(obj)]
132
def write(self, data, async=False):
133
self.handler.addOutput(data, async)
137
CTRL_BACKSLASH = '\x1c'
140
class Manhole(recvline.HistoricRecvLine):
141
"""Mediator between a fancy line source and an interactive interpreter.
143
This accepts lines from its transport and passes them on to a
144
L{ManholeInterpreter}. Control commands (^C, ^D, ^\) are also handled
145
with something approximating their normal terminal-mode behavior. It
146
can optionally be constructed with a dict which will be used as the
147
local namespace for any code executed.
152
def __init__(self, namespace=None):
153
recvline.HistoricRecvLine.__init__(self, namespace)
154
if namespace is not None:
155
self.namespace = namespace.copy()
157
def connectionMade(self):
158
recvline.HistoricRecvLine.connectionMade(self)
159
self.interpreter = ManholeInterpreter(self, self.namespace)
160
self.keyHandlers[CTRL_C] = self.handle_INT
161
self.keyHandlers[CTRL_D] = self.handle_EOF
162
self.keyHandlers[CTRL_L] = self.handle_FF
163
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
166
def handle_INT(self):
167
self.terminal.nextLine()
168
self.terminal.write("KeyboardInterrupt")
169
self.terminal.nextLine()
170
self.terminal.write(self.ps[self.pn])
172
self.lineBufferIndex = 0
175
def handle_EOF(self):
177
self.terminal.write('\a')
184
Handle a 'form feed' byte - generally used to request a screen
187
self.terminal.eraseDisplay()
188
self.terminal.cursorHome()
192
def handle_QUIT(self):
193
self.terminal.loseConnection()
196
def _needsNewline(self):
197
w = self.terminal.lastWrite
198
return not w.endswith('\n') and not w.endswith('\x1bE')
200
def addOutput(self, bytes, async=False):
202
self.terminal.eraseLine()
203
self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
205
self.terminal.write(bytes)
208
if self._needsNewline():
209
self.terminal.nextLine()
211
self.terminal.write(self.ps[self.pn])
214
oldBuffer = self.lineBuffer
216
self.lineBufferIndex = 0
218
self._deliverBuffer(oldBuffer)
220
def lineReceived(self, line):
221
more = self.interpreter.push(line)
223
if self._needsNewline():
224
self.terminal.nextLine()
225
self.terminal.write(self.ps[self.pn])
228
"""Colorizer for Python tokens.
230
A series of tokens are written to instances of this object. Each is
231
colored in a particular way. The final line of the result of this is
232
generally added to the output.
236
'identifier': '\x1b[31m',
237
'keyword': '\x1b[32m',
238
'parameter': '\x1b[33m',
239
'variable': '\x1b[1;33m',
240
'string': '\x1b[35m',
241
'number': '\x1b[36m',
244
normalColor = '\x1b[0m'
249
def color(self, type):
250
r = self.typeToColor.get(type, '')
253
def write(self, token, type=None):
254
if token and token != '\r':
257
self.written.append(c)
258
self.written.append(token)
260
self.written.append(self.normalColor)
263
s = ''.join(self.written)
264
return s.strip('\n').splitlines()[-1]
266
def lastColorizedLine(source):
267
"""Tokenize and colorize the given Python source.
269
Returns a VT102-format colorized version of the last line of C{source}.
272
p = TokenPrinter(w.write).printtoken
273
s = StringIO.StringIO(source)
275
tokenize.tokenize(s.readline, p)
279
class ColoredManhole(Manhole):
280
"""A REPL which syntax colors input as users type it.
284
"""Return a string containing the currently entered source.
286
This is only the code which will be considered for execution
289
return ('\n'.join(self.interpreter.buffer) +
291
''.join(self.lineBuffer))
294
def characterReceived(self, ch, moreCharactersComing):
295
if self.mode == 'insert':
296
self.lineBuffer.insert(self.lineBufferIndex, ch)
298
self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
299
self.lineBufferIndex += 1
301
if moreCharactersComing:
302
# Skip it all, we'll get called with another character in
303
# like 2 femtoseconds.
307
# Don't bother to try to color whitespace
308
self.terminal.write(ch)
311
source = self.getSource()
313
# Try to write some junk
315
coloredLine = lastColorizedLine(source)
316
except tokenize.TokenError:
317
# We couldn't do it. Strange. Oh well, just add the character.
318
self.terminal.write(ch)
320
# Success! Clear the source on this line.
321
self.terminal.eraseLine()
322
self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]) - 1)
324
# And write a new, colorized one.
325
self.terminal.write(self.ps[self.pn] + coloredLine)
327
# And move the cursor to where it belongs
328
n = len(self.lineBuffer) - self.lineBufferIndex
330
self.terminal.cursorBackward(n)