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()

Cookbook/EmbeddingInTkinter (last edited 2008-07-16 13:19:34 by JensThomas)