Embedding IPython in a PyGTK application
The Accerciser project has some great code that lets you run an IPython console inside a gtk.TextArea
http://live.gnome.org/Accerciser
The file ipython_view.py seems to be independent in their code, and is licensed under the BSD license. We include a modified version of the file below.
This has been tested and seems to work in Linux (Ubuntu 6.10 and Fedora Core 6) and Windows (XP with Python 2.4.2, IPython 0.7.3, gtk+-devel 2.10.7, PyGTK 2.10.3). It supports the up/down arrows to view history, and tab completion for commands/variables etc.
Suggestions for improvement very much appreciated! If you find bugs in the ipython_view.py file, please report them to GNOME bugzilla, http://bugzilla.gnome.org/browse.cgi?product=accerciser
Known bugs
- The HOME key doesn't return you to the right place when typing a command.
On Windows, if you type a valid magic function, for example %magic then it causes your program to hang.
- On Windows, with IPython 0.8.1, colours don't seem to work and tab-completion is not available.
Example
Here is a simple example showing how the IPython console can be embedded in a short Python/PyGTK script to give a basic scrolling terminal window.
Here is the code:
1 import gtk 2 from ipython_view import * 3 import pango 4 5 import platform 6 if platform.system()=="Windows": 7 FONT = "Lucida Console 9" 8 else: 9 FONT = "Luxi Mono 10" 10 11 W = gtk.Window() 12 W.set_size_request(750,550) 13 W.set_resizable(True) 14 S = gtk.ScrolledWindow() 15 S.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) 16 V = IPythonView() 17 V.modify_font(pango.FontDescription(FONT)) 18 V.set_wrap_mode(gtk.WRAP_CHAR) 19 V.show() 20 S.add(V) 21 S.show() 22 W.add(S) 23 W.show() 24 W.connect('delete_event',lambda x,y:False) 25 W.connect('destroy',lambda x:gtk.main_quit()) 26 gtk.main()
Exposing variables to the shell
If you have variables you want to expose in the shell, use the following syntax. Also look at the files in the Accerciser source code for how to synchronise other aspects of your GUI with the actions inthe IPython prompt.
1 V.updateNamespace({'myvar': myvar})
ipython_view.py
Here is a modified version of the ipython_view.py from the Accerciser subversion repository.
Changes relative to the copy in Subversion:
- throw an exception on failed import, instead of remaining silent.
added if argv is None: argv = [] to fix case of Python being called with commandline args not intended for ipython
1 """
2 Backend to the console plugin.
3
4 @author: Eitan Isaacson
5 @organization: IBM Corporation
6 @copyright: Copyright (c) 2007 IBM Corporation
7 @license: BSD
8
9 All rights reserved. This program and the accompanying materials are made
10 available under the terms of the BSD which accompanies this distribution, and
11 is available at U{http://www.opensource.org/licenses/bsd-license.php}
12 """
13 # this file is a modified version of source code from the Accerciser project
14 # http://live.gnome.org/accerciser
15
16 import gtk
17 import re
18 import sys
19 import os
20 import pango
21 from StringIO import StringIO
22
23 try:
24 import IPython
25 except Exception,e:
26 raise "Error importing IPython (%s)" % str(e)
27
28 ansi_colors = {'0;30': 'Black',
29 '0;31': 'Red',
30 '0;32': 'Green',
31 '0;33': 'Brown',
32 '0;34': 'Blue',
33 '0;35': 'Purple',
34 '0;36': 'Cyan',
35 '0;37': 'LightGray',
36 '1;30': 'DarkGray',
37 '1;31': 'DarkRed',
38 '1;32': 'SeaGreen',
39 '1;33': 'Yellow',
40 '1;34': 'LightBlue',
41 '1;35': 'MediumPurple',
42 '1;36': 'LightCyan',
43 '1;37': 'White'}
44
45 class IterableIPShell:
46 def __init__(self,argv=None,user_ns=None,user_global_ns=None,
47 cin=None, cout=None,cerr=None, input_func=None):
48 if input_func:
49 IPython.iplib.raw_input_original = input_func
50 if cin:
51 IPython.Shell.Term.cin = cin
52 if cout:
53 IPython.Shell.Term.cout = cout
54 if cerr:
55 IPython.Shell.Term.cerr = cerr
56
57 if argv is None:
58 argv=[]
59
60 # This is to get rid of the blockage that occurs during
61 # IPython.Shell.InteractiveShell.user_setup()
62 IPython.iplib.raw_input = lambda x: None
63
64 self.term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
65 os.environ['TERM'] = 'dumb'
66 excepthook = sys.excepthook
67 self.IP = IPython.Shell.make_IPython(argv,user_ns=user_ns,
68 user_global_ns=user_global_ns,
69 embedded=True,
70 shell_class=IPython.Shell.InteractiveShell)
71 self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
72 header='IPython system call: ',
73 verbose=self.IP.rc.system_verbose)
74 sys.excepthook = excepthook
75 self.iter_more = 0
76 self.history_level = 0
77 self.complete_sep = re.compile('[\s\{\}\[\]\(\)]')
78
79 def execute(self):
80 self.history_level = 0
81 orig_stdout = sys.stdout
82 sys.stdout = IPython.Shell.Term.cout
83 try:
84 line = self.IP.raw_input(None, self.iter_more)
85 if self.IP.autoindent:
86 self.IP.readline_startup_hook(None)
87 except KeyboardInterrupt:
88 self.IP.write('\nKeyboardInterrupt\n')
89 self.IP.resetbuffer()
90 # keep cache in sync with the prompt counter:
91 self.IP.outputcache.prompt_count -= 1
92
93 if self.IP.autoindent:
94 self.IP.indent_current_nsp = 0
95 self.iter_more = 0
96 except:
97 self.IP.showtraceback()
98 else:
99 self.iter_more = self.IP.push(line)
100 if (self.IP.SyntaxTB.last_syntax_error and
101 self.IP.rc.autoedit_syntax):
102 self.IP.edit_syntax_error()
103 if self.iter_more:
104 self.prompt = str(self.IP.outputcache.prompt2).strip()
105 if self.IP.autoindent:
106 self.IP.readline_startup_hook(self.IP.pre_readline)
107 else:
108 self.prompt = str(self.IP.outputcache.prompt1).strip()
109 sys.stdout = orig_stdout
110
111 def historyBack(self):
112 self.history_level -= 1
113 return self._getHistory()
114
115 def historyForward(self):
116 self.history_level += 1
117 return self._getHistory()
118
119 def _getHistory(self):
120 try:
121 rv = self.IP.user_ns['In'][self.history_level].strip('\n')
122 except IndexError:
123 self.history_level = 0
124 rv = ''
125 return rv
126
127 def updateNamespace(self, ns_dict):
128 self.IP.user_ns.update(ns_dict)
129
130 def complete(self, line):
131 split_line = self.complete_sep.split(line)
132 possibilities = self.IP.complete(split_line[-1])
133 if possibilities:
134 common_prefix = reduce(self._commonPrefix, possibilities)
135 completed = line[:-len(split_line[-1])]+common_prefix
136 else:
137 completed = line
138 return completed, possibilities
139
140 def _commonPrefix(self, str1, str2):
141 for i in range(len(str1)):
142 if not str2.startswith(str1[:i+1]):
143 return str1[:i]
144 return str1
145
146 def shell(self, cmd,verbose=0,debug=0,header=''):
147 stat = 0
148 if verbose or debug: print header+cmd
149 # flush stdout so we don't mangle python's buffering
150 if not debug:
151 input, output = os.popen4(cmd)
152 print output.read()
153 output.close()
154 input.close()
155
156 class ConsoleView(gtk.TextView):
157 def __init__(self):
158 gtk.TextView.__init__(self)
159 self.modify_font(pango.FontDescription('Mono'))
160 self.set_cursor_visible(True)
161 self.text_buffer = self.get_buffer()
162 self.mark = self.text_buffer.create_mark('scroll_mark',
163 self.text_buffer.get_end_iter(),
164 False)
165 for code in ansi_colors:
166 self.text_buffer.create_tag(code,
167 foreground=ansi_colors[code],
168 weight=700)
169 self.text_buffer.create_tag('0')
170 self.text_buffer.create_tag('notouch', editable=False)
171 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
172 self.line_start = \
self.text_buffer.create_mark('line_start',
173 self.text_buffer.get_end_iter(), True
174 )
175 self.connect('key-press-event', self._onKeypress)
176 self.last_cursor_pos = 0
177
178 def write(self, text, editable=False):
179 segments = self.color_pat.split(text)
180 segment = segments.pop(0)
181 start_mark = self.text_buffer.create_mark(None,
182 self.text_buffer.get_end_iter(),
183 True)
184 self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
185
186 if segments:
187 ansi_tags = self.color_pat.findall(text)
188 for tag in ansi_tags:
189 i = segments.index(tag)
190 self.text_buffer.insert_with_tags_by_name(self.text_buffer.get_end_iter(),
191 segments[i+1], tag)
192 segments.pop(i)
193 if not editable:
194 self.text_buffer.apply_tag_by_name('notouch',
195 self.text_buffer.get_iter_at_mark(start_mark),
196 self.text_buffer.get_end_iter())
197 self.text_buffer.delete_mark(start_mark)
198 self.scroll_mark_onscreen(self.mark)
199
200 def showPrompt(self, prompt):
201 self.write(prompt)
202 self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
203
204 def changeLine(self, text):
205 iter = self.text_buffer.get_iter_at_mark(self.line_start)
206 iter.forward_to_line_end()
207 self.text_buffer.delete(self.text_buffer.get_iter_at_mark(self.line_start), iter)
208 self.write(text, True)
209
210 def getCurrentLine(self):
211 rv = self.text_buffer.get_slice(self.text_buffer.get_iter_at_mark(self.line_start),
212 self.text_buffer.get_end_iter(), False)
213 return rv
214
215 def showReturned(self, text):
216 iter = self.text_buffer.get_iter_at_mark(self.line_start)
217 iter.forward_to_line_end()
218 self.text_buffer.apply_tag_by_name('notouch',
219 self.text_buffer.get_iter_at_mark(self.line_start),
220 iter)
221 self.write('\n'+text)
222 if text:
223 self.write('\n')
224 self.showPrompt(self.prompt)
225 self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
226 self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
227
228 def _onKeypress(self, obj, event):
229 if not event.string:
230 return
231 insert_mark = self.text_buffer.get_insert()
232 insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
233 selection_mark = self.text_buffer.get_selection_bound()
234 selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
235 start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
236 if start_iter.compare(insert_iter) <= 0 and \
start_iter.compare(selection_iter) <= 0:
237 return
238 elif start_iter.compare(insert_iter) > 0 and \
start_iter.compare(selection_iter) > 0:
239 self.text_buffer.place_cursor(start_iter)
240 elif insert_iter.compare(selection_iter) < 0:
241 self.text_buffer.move_mark(insert_mark, start_iter)
242 elif insert_iter.compare(selection_iter) > 0:
243 self.text_buffer.move_mark(selection_mark, start_iter)
244
245
246 class IPythonView(ConsoleView, IterableIPShell):
247 def __init__(self):
248 ConsoleView.__init__(self)
249 self.cout = StringIO()
250 IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout,
251 input_func=self.raw_input)
252 self.connect('key_press_event', self.keyPress)
253 self.execute()
254 self.cout.truncate(0)
255 self.showPrompt(self.prompt)
256 self.interrupt = False
257
258 def raw_input(self, prompt=''):
259 if self.interrupt:
260 self.interrupt = False
261 raise KeyboardInterrupt
262 return self.getCurrentLine()
263
264 def keyPress(self, widget, event):
265 if event.state & gtk.gdk.CONTROL_MASK and event.keyval == 99:
266 self.interrupt = True
267 self._processLine()
268 return True
269 elif event.keyval == gtk.keysyms.Return:
270 self._processLine()
271 return True
272 elif event.keyval == gtk.keysyms.Up:
273 self.changeLine(self.historyBack())
274 return True
275 elif event.keyval == gtk.keysyms.Down:
276 self.changeLine(self.historyForward())
277 return True
278 elif event.keyval == gtk.keysyms.Tab:
279 if not self.getCurrentLine().strip():
280 return False
281 completed, possibilities = self.complete(self.getCurrentLine())
282 if len(possibilities) > 1:
283 slice = self.getCurrentLine()
284 self.write('\n')
285 for symbol in possibilities:
286 self.write(symbol+'\n')
287 self.showPrompt(self.prompt)
288 self.changeLine(completed or slice)
289 return True
290
291 def _processLine(self):
292 self.history_pos = 0
293 self.execute()
294 rv = self.cout.getvalue()
295 if rv: rv = rv.strip('\n')
296 self.showReturned(rv)
297 self.cout.truncate(0)