root/trunk/twisted/conch/manhole.py

Revision 32557, 10.7 KB (checked in by exarkun, 8 months ago)

Merge manhole-home-end-5252

Author: djfroofy
Reviewer: exarkun
Fixes: #5252

Add C-a and C-e support for home and end functions to twisted.conch.manhole.

Line 
1# -*- test-case-name: twisted.conch.test.test_manhole -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Line-input oriented interactive interpreter loop.
7
8Provides classes for handling Python source input and arbitrary output
9interactively from a Twisted application.  Also included is syntax coloring
10code with support for VT102 terminals, control code handling (^C, ^D, ^Q),
11and reasonable handling of Deferreds.
12
13@author: Jp Calderone
14"""
15
16import code, sys, StringIO, tokenize
17
18from twisted.conch import recvline
19
20from twisted.internet import defer
21from twisted.python.htmlizer import TokenPrinter
22
23class 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
45class 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._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))
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()), True)
128        del self._pendingDeferreds[id(obj)]
129        return failure
130
131    def write(self, data, async=False):
132        self.handler.addOutput(data, async)
133
134CTRL_C = '\x03'
135CTRL_D = '\x04'
136CTRL_BACKSLASH = '\x1c'
137CTRL_L = '\x0c'
138CTRL_A = '\x01'
139CTRL_E = '\x05'
140
141class Manhole(recvline.HistoricRecvLine):
142    """Mediator between a fancy line source and an interactive interpreter.
143
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.
149    """
150
151    namespace = None
152
153    def __init__(self, namespace=None):
154        recvline.HistoricRecvLine.__init__(self)
155        if namespace is not None:
156            self.namespace = namespace.copy()
157
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
167
168
169    def handle_INT(self):
170        """
171        Handle ^C as an interrupt keystroke by resetting the current input
172        variables to their initial state.
173        """
174        self.pn = 0
175        self.lineBuffer = []
176        self.lineBufferIndex = 0
177        self.interpreter.resetBuffer()
178
179        self.terminal.nextLine()
180        self.terminal.write("KeyboardInterrupt")
181        self.terminal.nextLine()
182        self.terminal.write(self.ps[self.pn])
183
184
185    def handle_EOF(self):
186        if self.lineBuffer:
187            self.terminal.write('\a')
188        else:
189            self.handle_QUIT()
190
191
192    def handle_FF(self):
193        """
194        Handle a 'form feed' byte - generally used to request a screen
195        refresh/redraw.
196        """
197        self.terminal.eraseDisplay()
198        self.terminal.cursorHome()
199        self.drawInputLine()
200
201
202    def handle_QUIT(self):
203        self.terminal.loseConnection()
204
205
206    def _needsNewline(self):
207        w = self.terminal.lastWrite
208        return not w.endswith('\n') and not w.endswith('\x1bE')
209
210    def addOutput(self, bytes, async=False):
211        if async:
212            self.terminal.eraseLine()
213            self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
214
215        self.terminal.write(bytes)
216
217        if async:
218            if self._needsNewline():
219                self.terminal.nextLine()
220
221            self.terminal.write(self.ps[self.pn])
222
223            if self.lineBuffer:
224                oldBuffer = self.lineBuffer
225                self.lineBuffer = []
226                self.lineBufferIndex = 0
227
228                self._deliverBuffer(oldBuffer)
229
230    def lineReceived(self, line):
231        more = self.interpreter.push(line)
232        self.pn = bool(more)
233        if self._needsNewline():
234            self.terminal.nextLine()
235        self.terminal.write(self.ps[self.pn])
236
237class VT102Writer:
238    """Colorizer for Python tokens.
239
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.
243    """
244
245    typeToColor = {
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',
252        'op': '\x1b[37m'}
253
254    normalColor = '\x1b[0m'
255
256    def __init__(self):
257        self.written = []
258
259    def color(self, type):
260        r = self.typeToColor.get(type, '')
261        return r
262
263    def write(self, token, type=None):
264        if token and token != '\r':
265            c = self.color(type)
266            if c:
267                self.written.append(c)
268            self.written.append(token)
269            if c:
270                self.written.append(self.normalColor)
271
272    def __str__(self):
273        s = ''.join(self.written)
274        return s.strip('\n').splitlines()[-1]
275
276def lastColorizedLine(source):
277    """Tokenize and colorize the given Python source.
278
279    Returns a VT102-format colorized version of the last line of C{source}.
280    """
281    w = VT102Writer()
282    p = TokenPrinter(w.write).printtoken
283    s = StringIO.StringIO(source)
284
285    tokenize.tokenize(s.readline, p)
286
287    return str(w)
288
289class ColoredManhole(Manhole):
290    """A REPL which syntax colors input as users type it.
291    """
292
293    def getSource(self):
294        """Return a string containing the currently entered source.
295
296        This is only the code which will be considered for execution
297        next.
298        """
299        return ('\n'.join(self.interpreter.buffer) +
300                '\n' +
301                ''.join(self.lineBuffer))
302
303
304    def characterReceived(self, ch, moreCharactersComing):
305        if self.mode == 'insert':
306            self.lineBuffer.insert(self.lineBufferIndex, ch)
307        else:
308            self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
309        self.lineBufferIndex += 1
310
311        if moreCharactersComing:
312            # Skip it all, we'll get called with another character in
313            # like 2 femtoseconds.
314            return
315
316        if ch == ' ':
317            # Don't bother to try to color whitespace
318            self.terminal.write(ch)
319            return
320
321        source = self.getSource()
322
323        # Try to write some junk
324        try:
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)
329        else:
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)
333
334            # And write a new, colorized one.
335            self.terminal.write(self.ps[self.pn] + coloredLine)
336
337            # And move the cursor to where it belongs
338            n = len(self.lineBuffer) - self.lineBufferIndex
339            if n:
340                self.terminal.cursorBackward(n)
Note: See TracBrowser for help on using the browser.