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

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:

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:

   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)

Cookbook/EmbeddingInGTK (last edited 2007-06-11 09:59:30 by JohnPye)