Embedding IPython in a Tkinter application
This is just an extension of the very useful code posted on this cookbook to embed iPython in a GTK text widget to enable IPython to be used within Tkinter.
The original GTK code can be found here.
The GTK code was re-written in Tkinter http://wiki.python.org/moin/TkInter in oder to allow IPython to be embedded in the CCP1GUI chemistry GUI.
The source code is shown below, but can also found in the ipython directory of the CCP1GUI source code, which can be downloaded from sourceforge.
1 """
2 This is a modified version of source code from the Accerciser project
3 (http://live.gnome.org/accerciser).
4
5 Backend to the console plugin.
6
7 @author: Eitan Isaacson
8 @organization: IBM Corporation
9 @copyright: Copyright (c) 2007 IBM Corporation
10 @license: BSD
11
12 All rights reserved. This program and the accompanying materials are made
13 available under the terms of the BSD which accompanies this distribution, and
14 is available at U{http://www.opensource.org/licenses/bsd-license.php}
15 """
16
17 import re
18 import sys
19 import os
20 from StringIO import StringIO
21 import Tkinter
22 import IPython
23
24 class IterableIPShell:
25 def __init__(self,argv=None,user_ns=None,user_global_ns=None,
26 cin=None, cout=None,cerr=None, input_func=None):
27 if input_func:
28 IPython.iplib.raw_input_original = input_func
29 if cin:
30 IPython.Shell.Term.cin = cin
31 if cout:
32 IPython.Shell.Term.cout = cout
33 if cerr:
34 IPython.Shell.Term.cerr = cerr
35
36 if argv is None:
37 argv=[]
38
39 # This is to get rid of the blockage that occurs during
40 # IPython.Shell.InteractiveShell.user_setup()
41 IPython.iplib.raw_input = lambda x: None
42
43 self.term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
44 os.environ['TERM'] = 'dumb'
45 excepthook = sys.excepthook
46 self.IP = IPython.Shell.make_IPython(argv,user_ns=user_ns,
47 user_global_ns=user_global_ns,
48 embedded=True,
49 shell_class=IPython.Shell.InteractiveShell)
50 self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
51 header='IPython system call: ',
52 verbose=self.IP.rc.system_verbose)
53 sys.excepthook = excepthook
54 self.iter_more = 0
55 self.history_level = 0
56 self.complete_sep = re.compile('[\s\{\}\[\]\(\)]')
57
58 def execute(self):
59 self.history_level = 0
60 orig_stdout = sys.stdout
61 sys.stdout = IPython.Shell.Term.cout
62 try:
63 line = self.IP.raw_input(None, self.iter_more)
64 if self.IP.autoindent:
65 self.IP.readline_startup_hook(None)
66 except KeyboardInterrupt:
67 self.IP.write('\nKeyboardInterrupt\n')
68 self.IP.resetbuffer()
69 # keep cache in sync with the prompt counter:
70 self.IP.outputcache.prompt_count -= 1
71
72 if self.IP.autoindent:
73 self.IP.indent_current_nsp = 0
74 self.iter_more = 0
75 except:
76 self.IP.showtraceback()
77 else:
78 self.iter_more = self.IP.push(line)
79 if (self.IP.SyntaxTB.last_syntax_error and
80 self.IP.rc.autoedit_syntax):
81 self.IP.edit_syntax_error()
82 if self.iter_more:
83 self.prompt = str(self.IP.outputcache.prompt2).strip()
84 if self.IP.autoindent:
85 self.IP.readline_startup_hook(self.IP.pre_readline)
86 else:
87 self.prompt = str(self.IP.outputcache.prompt1).strip()
88 sys.stdout = orig_stdout
89
90 def historyBack(self):
91 self.history_level -= 1
92 return self._getHistory()
93
94 def historyForward(self):
95 self.history_level += 1
96 return self._getHistory()
97
98 def _getHistory(self):
99 try:
100 rv = self.IP.user_ns['In'][self.history_level].strip('\n')
101 except IndexError:
102 self.history_level = 0
103 rv = ''
104 return rv
105
106 def updateNamespace(self, ns_dict):
107 self.IP.user_ns.update(ns_dict)
108
109 def complete(self, line):
110 split_line = self.complete_sep.split(line)
111 possibilities = self.IP.complete(split_line[-1])
112 if possibilities:
113 common_prefix = reduce(self._commonPrefix, possibilities)
114 completed = line[:-len(split_line[-1])]+common_prefix
115 else:
116 completed = line
117 return completed, possibilities
118
119 def _commonPrefix(self, str1, str2):
120 for i in range(len(str1)):
121 if not str2.startswith(str1[:i+1]):
122 return str1[:i]
123 return str1
124
125 def shell(self, cmd,verbose=0,debug=0,header=''):
126 stat = 0
127 if verbose or debug: print header+cmd
128 # flush stdout so we don't mangle python's buffering
129 if not debug:
130 input, output = os.popen4(cmd)
131 print output.read()
132 output.close()
133 input.close()
134
135
136 ansi_colors = {'0;30': 'Black',
137 '0;31': 'Red',
138 '0;32': 'Green',
139 '0;33': 'Brown',
140 '0;34': 'Blue',
141 '0;35': 'Purple',
142 '0;36': 'Cyan',
143 '0;37': 'LightGray',
144 '1;30': 'DarkGray',
145 '1;31': 'DarkRed',
146 '1;32': 'SeaGreen',
147 '1;33': 'Yellow',
148 '1;34': 'LightBlue',
149 '1;35': 'MediumPurple',
150 '1;36': 'LightCyan',
151 '1;37': 'White'}
152
153
154 class TkConsoleView(Tkinter.Text):
155 def __init__(self,root):
156 Tkinter.Text.__init__(self,root)
157
158 # As the stdout,stderr etc. get fiddled about with we need to put any
159 # debug output into a file
160 self.debug=0
161 if self.debug:
162 self.o = open('debug.out','w')
163
164 # Keeps track of where the insert cursor should be on the entry line
165 self.mark = 'scroll_mark'
166 self.mark_set(self.mark,Tkinter.END)
167 self.mark_gravity(self.mark,Tkinter.RIGHT)
168
169 # Set the tags for colouring the text
170 for code in ansi_colors:
171 self.tag_config(code,
172 foreground=ansi_colors[code])
173
174 self.tag_config('notouch') # Tag for indicating what areas of the widget aren't editable
175
176
177 # colour_pat matches the colour tags and places these in a group
178 # match character with hex value 01 (start of heading?) zero or more times, followed by
179 # the hex character 1b (escape) then "[" and group ...things.. followed by m (?) and then
180 # hex character 02 (start of text) zero or more times
181 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
182
183 self.line_start = 'line_start' # Tracks start of user input on the line (excluding prompt)
184 self.mark_set(self.line_start,Tkinter.INSERT)
185 self.mark_gravity(self.line_start,Tkinter.LEFT)
186
187 self._setBindings()
188
189 def write(self, text, editable=False):
190
191 segments = self.color_pat.split(text)
192 # First is blank line
193 segment = segments.pop(0)
194
195 # Keep track of where we started entering text so we can set as non-editable
196 self.start_mark = 'start_mark'
197 self.mark_set(self.start_mark,Tkinter.INSERT)
198 self.mark_gravity(self.start_mark,Tkinter.LEFT)
199
200 self.insert(Tkinter.END, segment)
201
202 if segments:
203 # Just return the colour tags
204 ansi_tags = self.color_pat.findall(text)
205
206 for tag in ansi_tags:
207 i = segments.index(tag)
208 self.insert(Tkinter.END,segments[i+1],tag)
209 segments.pop(i)
210
211 if not editable:
212 if self.debug:
213 print "adding notouch between %s : %s" % ( self.index(self.start_mark),\
self.index(Tkinter.INSERT) )
214
215 self.tag_add('notouch',self.start_mark,"%s-1c" % Tkinter.INSERT)
216
217 self.mark_unset(self.start_mark)
218 #jmht self.scroll_mark_onscreen(self.mark)
219
220
221 def showBanner(self,banner):
222 """Print the supplied banner on starting the shell"""
223 self.write(banner)
224
225 def showPrompt(self, prompt):
226 self.write(prompt)
227 self.mark_set(self.line_start,Tkinter.INSERT)
228 self.see(Tkinter.INSERT) #Make sure we can always see the prompt
229
230
231 def changeLine(self, text):
232 self.delete(self.line_start,"%s lineend" % self.line_start)
233 self.write(text, True)
234
235 def getCurrentLine(self):
236
237 rv = self.get(self.line_start,Tkinter.END)
238
239 if self.debug:
240 print >> self.o,"getCurrentline: %s" % rv
241 print >> self.o,"INSERT: %s" % Tkinter.END
242 print >> self.o,"END: %s" % Tkinter.INSERT
243 print >> self.o,"line_start: %s" % self.index(self.line_start)
244
245 return rv
246
247 def showReturned(self, text):
248 self.tag_add('notouch',self.line_start,"%s lineend" % self.line_start )
249 self.write('\n'+text)
250 if text:
251 self.write('\n')
252 self.showPrompt(self.prompt)
253 #self.mark_set(self.line_start,Tkinter.END) #jmht don't need this as showprompt sets mark
254
255 def _setBindings(self):
256 """ Bind the keys we require.
257 REM: if a bound function returns "break" then no other bindings are called
258 If it returns None, then the other default bindings are called.
259 """
260 self.bind("<Key>",self.processKeyPress)
261 self.bind("<Return>",self.processEnterPress)
262 self.bind("<Up>",self.processUpPress)
263 self.bind("<Down>",self.processDownPress)
264 self.bind("<Tab>",self.processTabPress)
265 self.bind("<BackSpace>",self.processBackSpacePress)
266
267 def isEditable(self):
268 """ Scan the notouch tag range in pairs and see if the INSERT index falls
269 between any of them.
270 """
271 ranges = self.tag_ranges('notouch')
272 first=None
273 for idx in ranges:
274 if not first:
275 first=idx
276 continue
277 else:
278
279 if self.debug:
280 print "Comparing %s between %s : %s " % (self.index(Tkinter.INSERT),first,idx)
281
282 if self.compare( Tkinter.INSERT,'>=',first ) and \
self.compare( Tkinter.INSERT,'<=',idx ):
283 return False
284 first=None
285 return True
286
287 def processKeyPress(self,event):
288
289 if self.debug:
290 print >>self.o,"processKeyPress got key: %s" % event.char
291 print >>self.o,"processKeyPress INSERT: %s" % self.index(Tkinter.INSERT)
292 print >>self.o,"processKeyPress END: %s" % self.index(Tkinter.END)
293
294 if not self.isEditable():
295 # Move cursor mark to start of line
296 self.mark_set(Tkinter.INSERT,self.mark)
297
298 # Make sure line_start follows inserted text
299 self.mark_set(self.mark,"%s+1c" % Tkinter.INSERT)
300
301
302 def processBackSpacePress(self,event):
303 if not self.isEditable():
304 return "break"
305
306 def processEnterPress(self,event):
307 self._processLine()
308 return "break" # Need break to stop the other bindings being called
309
310 def processUpPress(self,event):
311 self.changeLine(self.historyBack())
312 return "break"
313
314 def processDownPress(self,event):
315 self.changeLine(self.historyForward())
316 return "break"
317
318 def processTabPress(self,event):
319 if not self.getCurrentLine().strip():
320 return
321 completed, possibilities = self.complete(self.getCurrentLine())
322 if len(possibilities) > 1:
323 slice = self.getCurrentLine()
324 self.write('\n')
325 for symbol in possibilities:
326 self.write(symbol+'\n')
327 self.showPrompt(self.prompt)
328 self.changeLine(completed or slice)
329 return "break"
330
331 class IPythonView(TkConsoleView, IterableIPShell):
332 def __init__(self,root,banner=None):
333 TkConsoleView.__init__(self,root)
334 self.cout = StringIO()
335 IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout,
336 input_func=self.raw_input)
337
338 if banner:
339 self.showBanner(banner)
340 self.execute()
341 self.cout.truncate(0)
342 self.showPrompt(self.prompt)
343 self.interrupt = False
344
345 def raw_input(self, prompt=''):
346 if self.interrupt:
347 self.interrupt = False
348 raise KeyboardInterrupt
349 return self.getCurrentLine()
350
351 def _processLine(self):
352 self.history_pos = 0
353 self.execute()
354 rv = self.cout.getvalue()
355 if self.debug:
356 print >>self.o,"_processLine got rv: %s" % rv
357 if rv: rv = rv.strip('\n')
358 self.showReturned(rv)
359 self.cout.truncate(0)
360
361 if __name__ == "__main__":
362 root = Tkinter.Tk()
363 s=IPythonView(root)
364 s.pack()
365 root.mainloop()