~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/conch/manhole.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.conch.test.test_manhole -*-
 
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
"""Line-input oriented interactive interpreter loop.
 
6
 
 
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.
 
11
 
 
12
API Stability: Unstable
 
13
 
 
14
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
 
15
"""
 
16
 
 
17
import code, sys, StringIO, tokenize
 
18
 
 
19
from twisted.conch import recvline
 
20
 
 
21
from twisted.internet import defer
 
22
from twisted.python.htmlizer import TokenPrinter
 
23
 
 
24
class FileWrapper:
 
25
    """Minimal write-file-like object.
 
26
 
 
27
    Writes are translated into addOutput calls on an object passed to
 
28
    __init__.  Newlines are also converted from network to local style.
 
29
    """
 
30
 
 
31
    softspace = 0
 
32
    state = 'normal'
 
33
 
 
34
    def __init__(self, o):
 
35
        self.o = o
 
36
 
 
37
    def flush(self):
 
38
        pass
 
39
 
 
40
    def write(self, data):
 
41
        self.o.addOutput(data.replace('\r\n', '\n'))
 
42
 
 
43
    def writelines(self, lines):
 
44
        self.write(''.join(lines))
 
45
 
 
46
class ManholeInterpreter(code.InteractiveInterpreter):
 
47
    """Interactive Interpreter with special output and Deferred support.
 
48
 
 
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
 
56
    callback chain.
 
57
    """
 
58
 
 
59
    numDeferreds = 0
 
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
 
65
        self.resetBuffer()
 
66
 
 
67
    def resetBuffer(self):
 
68
        """Reset the input buffer."""
 
69
        self.buffer = []
 
70
 
 
71
    def push(self, line):
 
72
        """Push a line to the interpreter.
 
73
 
 
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()).
 
83
 
 
84
        """
 
85
        self.buffer.append(line)
 
86
        source = "\n".join(self.buffer)
 
87
        more = self.runsource(source, self.filename)
 
88
        if not more:
 
89
            self.resetBuffer()
 
90
        return more
 
91
 
 
92
    def runcode(self, *a, **kw):
 
93
        orighook, sys.displayhook = sys.displayhook, self.displayhook
 
94
        try:
 
95
            origout, sys.stdout = sys.stdout, FileWrapper(self.handler)
 
96
            try:
 
97
                code.InteractiveInterpreter.runcode(self, *a, **kw)
 
98
            finally:
 
99
                sys.stdout = origout
 
100
        finally:
 
101
            sys.displayhook = orighook
 
102
 
 
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],))
 
111
            else:
 
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))
 
121
 
 
122
    def _cbDisplayDeferred(self, result, k, obj):
 
123
        self.write("Deferred #%d called back: %r" % (k, result), True)
 
124
        del self._pendingDeferreds[id(obj)]
 
125
        return result
 
126
 
 
127
    def _ebDisplayDeferred(self, failure, k, obj):
 
128
        self.write("Deferred #%d failed: %r" % (k, failure.getErrorMessage()), True)
 
129
        del self._pendingDeferreds[id(obj)]
 
130
        return failure
 
131
 
 
132
    def write(self, data, async=False):
 
133
        self.handler.addOutput(data, async)
 
134
 
 
135
CTRL_C = '\x03'
 
136
CTRL_D = '\x04'
 
137
CTRL_BACKSLASH = '\x1c'
 
138
CTRL_L = '\x0c'
 
139
 
 
140
class Manhole(recvline.HistoricRecvLine):
 
141
    """Mediator between a fancy line source and an interactive interpreter.
 
142
 
 
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.
 
148
    """
 
149
 
 
150
    namespace = None
 
151
 
 
152
    def __init__(self, namespace=None):
 
153
        recvline.HistoricRecvLine.__init__(self, namespace)
 
154
        if namespace is not None:
 
155
            self.namespace = namespace.copy()
 
156
 
 
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
 
164
 
 
165
 
 
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])
 
171
        self.lineBuffer = []
 
172
        self.lineBufferIndex = 0
 
173
 
 
174
 
 
175
    def handle_EOF(self):
 
176
        if self.lineBuffer:
 
177
            self.terminal.write('\a')
 
178
        else:
 
179
            self.handle_QUIT()
 
180
 
 
181
 
 
182
    def handle_FF(self):
 
183
        """
 
184
        Handle a 'form feed' byte - generally used to request a screen
 
185
        refresh/redraw.
 
186
        """
 
187
        self.terminal.eraseDisplay()
 
188
        self.terminal.cursorHome()
 
189
        self.drawInputLine()
 
190
 
 
191
 
 
192
    def handle_QUIT(self):
 
193
        self.terminal.loseConnection()
 
194
 
 
195
 
 
196
    def _needsNewline(self):
 
197
        w = self.terminal.lastWrite
 
198
        return not w.endswith('\n') and not w.endswith('\x1bE')
 
199
 
 
200
    def addOutput(self, bytes, async=False):
 
201
        if async:
 
202
            self.terminal.eraseLine()
 
203
            self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
 
204
 
 
205
        self.terminal.write(bytes)
 
206
 
 
207
        if async:
 
208
            if self._needsNewline():
 
209
                self.terminal.nextLine()
 
210
 
 
211
            self.terminal.write(self.ps[self.pn])
 
212
 
 
213
            if self.lineBuffer:
 
214
                oldBuffer = self.lineBuffer
 
215
                self.lineBuffer = []
 
216
                self.lineBufferIndex = 0
 
217
 
 
218
                self._deliverBuffer(oldBuffer)
 
219
 
 
220
    def lineReceived(self, line):
 
221
        more = self.interpreter.push(line)
 
222
        self.pn = bool(more)
 
223
        if self._needsNewline():
 
224
            self.terminal.nextLine()
 
225
        self.terminal.write(self.ps[self.pn])
 
226
 
 
227
class VT102Writer:
 
228
    """Colorizer for Python tokens.
 
229
 
 
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.
 
233
    """
 
234
 
 
235
    typeToColor = {
 
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',
 
242
        'op': '\x1b[37m'}
 
243
 
 
244
    normalColor = '\x1b[0m'
 
245
 
 
246
    def __init__(self):
 
247
        self.written = []
 
248
 
 
249
    def color(self, type):
 
250
        r = self.typeToColor.get(type, '')
 
251
        return r
 
252
 
 
253
    def write(self, token, type=None):
 
254
        if token and token != '\r':
 
255
            c = self.color(type)
 
256
            if c:
 
257
                self.written.append(c)
 
258
            self.written.append(token)
 
259
            if c:
 
260
                self.written.append(self.normalColor)
 
261
 
 
262
    def __str__(self):
 
263
        s = ''.join(self.written)
 
264
        return s.strip('\n').splitlines()[-1]
 
265
 
 
266
def lastColorizedLine(source):
 
267
    """Tokenize and colorize the given Python source.
 
268
 
 
269
    Returns a VT102-format colorized version of the last line of C{source}.
 
270
    """
 
271
    w = VT102Writer()
 
272
    p = TokenPrinter(w.write).printtoken
 
273
    s = StringIO.StringIO(source)
 
274
 
 
275
    tokenize.tokenize(s.readline, p)
 
276
 
 
277
    return str(w)
 
278
 
 
279
class ColoredManhole(Manhole):
 
280
    """A REPL which syntax colors input as users type it.
 
281
    """
 
282
 
 
283
    def getSource(self):
 
284
        """Return a string containing the currently entered source.
 
285
 
 
286
        This is only the code which will be considered for execution
 
287
        next.
 
288
        """
 
289
        return ('\n'.join(self.interpreter.buffer) +
 
290
                '\n' +
 
291
                ''.join(self.lineBuffer))
 
292
 
 
293
 
 
294
    def characterReceived(self, ch, moreCharactersComing):
 
295
        if self.mode == 'insert':
 
296
            self.lineBuffer.insert(self.lineBufferIndex, ch)
 
297
        else:
 
298
            self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
 
299
        self.lineBufferIndex += 1
 
300
 
 
301
        if moreCharactersComing:
 
302
            # Skip it all, we'll get called with another character in
 
303
            # like 2 femtoseconds.
 
304
            return
 
305
 
 
306
        if ch == ' ':
 
307
            # Don't bother to try to color whitespace
 
308
            self.terminal.write(ch)
 
309
            return
 
310
 
 
311
        source = self.getSource()
 
312
 
 
313
        # Try to write some junk
 
314
        try:
 
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)
 
319
        else:
 
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)
 
323
 
 
324
            # And write a new, colorized one.
 
325
            self.terminal.write(self.ps[self.pn] + coloredLine)
 
326
 
 
327
            # And move the cursor to where it belongs
 
328
            n = len(self.lineBuffer) - self.lineBufferIndex
 
329
            if n:
 
330
                self.terminal.cursorBackward(n)