Package SimPy :: Module tkconsole |
[hide private]
[frames] | no frames]

# Source Code for Module SimPy.tkconsole

      1  """A Tkinter - based console for conversing with the Python interpreter,
      2  featuring more tolerant pasting of code from other interactive sessions,
      3  better handling of continuations than the standard Python interpreter,
      4  highlighting of the most recently - executed code block, the ability to
      5  edit and reexecute previously entered code, a history of recently - entered
      6  lines, automatic multi - level completion with pop - up menus, and pop - up help.
      7
      8  Ka - Ping Yee < ping@lfw.org>, 18 April 1999.  This software is in the public
      9  domain and is provided without express or implied warranty.  Permission to
     10  use, modify, or distribute the software for any purpose is hereby granted."""
     11
     12  # TODO: autoindent to matching bracket after an unbalanced line (hard)
     13  # TODO: outdent after line starting with 'break', 'raise', 'return', etc.
     14  # TODO: keep a stack of indent levels for backspace to jump back to
     15  # TODO: blink or highlight matching brackets
     16  # TODO: delete the prompt when joining lines; allow a way to break lines
     17
     18  from Tkinter import *
     19  import sys, string, traceback, types, __builtin__
     20
     21  REVISION = '$Revision: 163 $'
     22  VERSION = string.split(REVISION)[1]
     23
     24 -class OutputPipe:
     25      """A substitute file object for redirecting output to a function."""
     26
     27 -    def __init__(self, writer):
     28          self.writer = writer
     29          self.closed = 0
     30
     31 -    def __repr__(self):
     32          return ' < OutputPipe to %s > ' % repr(self.writer)
     33
     34 -    def read(self, length):
     35          return ""
     36
     37 -    def write(self, data):
     38          if not self.closed: self.writer(data)
     39
     40 -    def close(self):
     41          self.closed = 1
     42
     43
     44 -class Console(Frame):
     45 -    def __init__(self, parent = None, dict={}, **options):
     46          """Construct from a parent widget, an optional dictionary to use
     47          as the namespace for execution, and any configuration options."""
     48          Frame.__init__(self, parent)
     49
     50          # Continuation state.
     51
     52          self.continuation = 0
     53          self.error = 0
     54          self.intraceback = 0
     55          self.pasted = 0
     56
     57          # The command history.
     58
     59          self.history = []
     60          self.historyindex = None
     61          self.current = ''
     62
     63          # Completion state.
     64
     65          self.compmenus = []
     66          self.compindex = None
     67          self.compfinish = ''
     68
     69          # Redirection.
     70
     71          self.stdout = OutputPipe(lambda data, w = self.write: w(data, 'stdout'))
     72          self.stderr = OutputPipe(lambda data, w = self.write: w(data, 'stderr'))
     73
     74          # Interpreter state.
     75
     76          if not hasattr(sys, 'ps1'): sys.ps1 = '>>> '
     77          if not hasattr(sys, 'ps2'): sys.ps2 = '... '
     78          self.prefixes = [sys.ps1, sys.ps2, ' >> ', ' > ']
     79          self.startup = 'Python %s\n%s\n' % (sys.version, sys.copyright) + \
     80              'Python Console v%s by Ka - Ping Yee < ping@lfw.org>\n' % VERSION
     81          self.dict = dict
     82
     83          # The text box.
     84
     85          self.text = Text(self, insertontime = 200, insertofftime = 150)
     86          self.text.insert('end', self.startup)
     87          self.text.insert('end', sys.ps1)
     88          self.text.bind('<Return>', self.cb_return)
     89          self.text.bind('<Button-1>', self.cb_select)
     90          self.text.bind('<ButtonRelease-1>', self.cb_position)
     91          self.text.bind('<ButtonRelease-2>', self.cb_paste)
     92          self.text.bind('<Home>', self.cb_home)
     93          self.text.bind('<Control-Home> ', self.cb_ctrlhome)
     94          self.text.bind('<Up>', self.cb_back)
     95          self.text.bind('<Down>', self.cb_forward)
     96          self.text.bind('<Configure>', self.cb_cleanup)
     97          self.text.bind('<Expose>', self.cb_cleanup)
     98          self.text.bind('<Key>', self.cb_cleanup)
     99          self.text.bind('<Tab>', self.cb_complete)
    100          self.text.bind('<Left>', self.cb_position)
    101          self.text.bind('<space>', self.cb_space)
    102          self.text.bind('<BackSpace>', self.cb_backspace)
    103          self.text.bind('<KeyRelease-BackSpace>', self.cb_nothing)
    104          self.text.bind('<F1>', self.cb_help)
    105          self.text.bind('<Control-slash>', self.cb_help)
    106          self.text.bind('<Alt-h>', self.cb_help)
    107
    108          # The scroll bar.
    109
    110          self.scroll = Scrollbar(self, command = self.text.yview)
    111          self.text.config(yscrollcommand = self.scroll.set)
    112          self.scroll.pack(side = RIGHT, fill = Y)
    113          self.text.pack(fill = BOTH, expand = 1)
    114          self.text.focus()
    115
    116          # Configurable options.
    117
    118          self.options = {'stdoutcolour': '#7020c0',
    119                          'stderrcolour': '#c03020',
    120                          'morecolour': '#a0d0f0',
    121                          'badcolour': '#e0b0b0',
    122                          'runcolour': '#90d090'}
    123          apply(self.config, (), self.options)
    124          apply(self.config, (), options)
    125
    126 -    def __getitem__(self, key):
    127          return self.options[key]
    128
    129 -    def __setitem__(self, key, value):
    130          if not self.options.has_key(key):
    131              raise KeyError, 'no such configuration option \'%s\'' % key
    132          self.options[key] = value
    133          if key == 'stdoutcolour':
    134              self.text.tag_configure('stdout', foreground = value)
    135          if key == 'stderrcolour':
    136              self.text.tag_configure('stderr', foreground = value)
    137
    138 -    def config(self, *args, **dict):
    139          """Get or set configuration options in a Tkinter - like style."""
    140          if args == () and dict == {}:
    141              return self.options
    142          if len(args) == 1:
    143              return self.options[args[0]]
    144          for key, value in dict.items():
    145              self[key] = value
    146
    147      # Text box routines.
    148
    149 -    def trim(self, command):
    150          """Trim any matching prefix from the given command line, returning
    151          the amount trimmed and the trimmed result."""
    152          for prefix in self.prefixes:
    153              if command[:len(prefix)] == prefix:
    154                  return len(prefix), command[len(prefix):]
    155          return 0, command
    156
    157 -    def getline(self, line = None, trim = 0):
    158          """Return the command on the current line."""
    159          if line is None:
    160              line, pos = self.cursor()
    161          command = self.text.get('%d.0' % line, '%d.end' % line)
    162          if trim:
    163              trimmed, command = self.trim(command)
    164          return command
    165
    166 -    def cursor(self):
    167          """Get the current line and position of the cursor."""
    168          cursor = self.text.index('insert')
    169          [line, pos] = map(string.atoi, string.split(cursor, '.'))
    170          return line, pos
    171
    172 -    def write(self, data, tag = None):
    173          """Show output from stdout or stderr in the console."""
    174          if self.intraceback and data[-2:] == '\n ': data = data[:-1]
    175          start = self.text.index('insert')
    176          self.text.insert('insert', data)
    177          end = self.text.index('insert')
    178          if tag: self.text.tag_add(tag, start, end)
    179
    180      # History mechanism.
    181
    182 -    def cb_back(self, event):
    183          """Step back in the history."""
    184          if self.history:
    185              if self.historyindex == None:
    186                  self.current = self.getline(trim = 1)
    187                  self.historyindex = len(self.history) - 1
    188              elif self.historyindex > 0:
    189                  self.historyindex = self.historyindex - 1
    190              self.recall()
    191
    192          return 'break'
    193
    194 -    def cb_forward(self, event):
    195          """Step forward in the history."""
    196          if self.history and self.historyindex is not None:
    197              self.historyindex = self.historyindex + 1
    198              if self.historyindex < len(self.history):
    199                  self.recall()
    200              else:
    201                  self.historyindex = None
    202                  self.recall(self.current)
    203
    204          return 'break'
    205
    206 -    def recall(self, command = None):
    207          """Show a command from the history on the current line."""
    208          if command is None:
    209              command = self.history[self.historyindex]
    210          line, pos = self.cursor()
    211          current = self.getline(line)
    212          trimmed, trimmedline = self.trim(current)
    213          cutpos = '%d.%d' % (line, trimmed)
    214          self.text.delete(cutpos, '%d.end' % line)
    215          self.text.insert(cutpos, command)
    216          self.text.mark_set('insert', '%d.end' % line)
    217
    218      # Completion mechanism.
    219
    220 -    def precontext(self):
    221          # Scan back for the identifier currently being typed.
    222          line, pos = self.cursor()
    223          command = self.getline()
    224          preceding = command[:pos]
    225          startchars = string.letters + '_'
    226          identchars = string.letters + string.digits + '_'
    227          while pos > 0 and preceding[pos - 1] in identchars:
    228              pos = pos - 1
    229          preceding, ident = preceding[:pos], preceding[pos:]
    230          start = '%d.%d' % (line, pos)
    231
    232          preceding = string.strip(preceding)
    233          context = ''
    234          if not ident or ident[0] in startchars:
    235              # Look for context before the start of the identifier.
    236              while preceding[-1:] == '.':
    237                  preceding = string.strip(preceding[:-1])
    238                  if preceding[-1] in identchars:
    239                      pos = len(preceding) - 1
    240                      while pos > 0 and preceding[pos - 1] in identchars:
    241                          pos = pos - 1
    242                      if preceding[pos] in startchars:
    243                          context = preceding[pos:] + '.' + context
    244                          preceding = string.strip(preceding[:pos])
    245                      else: break
    246                  else: break
    247
    248          line, pos = self.cursor()
    249          endpos = pos
    250          while endpos < len(command) and command[endpos] in identchars:
    251              endpos = endpos + 1
    252          end = '%d.%d' % (line, endpos)
    253
    254          return command, context, ident, start, end
    255
    256 -    def cb_complete(self, event):
    257          """Attempt to complete the identifier currently being typed."""
    258          if self.compmenus:
    259              if self.cursor() == self.compindex:
    260                  # Second attempt to complete: add finishing char and continue.
    261                  self.text.insert('insert', self.compfinish)
    262                  self.compindex = None
    263                  self.unpostmenus()
    264              return 'break'
    265
    266          command, context, ident, start, end = self.precontext()
    267
    268          # Get the list of possible choices.
    269          if context:
    270              try:
    271                  object = eval(context[:-1], self.dict)
    272                  keys = members(object)
    273              except:
    274                  object = None
    275                  keys = []
    276          else:
    277              class Lookup:
    278                  def __init__(self, dicts):
    279                      self.dicts = dicts
    280
    281                  def __getattr__(self, key):
    282                      for dict in self.dicts:
    283                          if dict.has_key(key): return dict[key]
    284                      return None
    285              object = Lookup([self.dict, __builtin__.__dict__])
    286              keys = self.dict.keys() + dir(__builtin__)
    287
    288          keys = matchingkeys(keys, ident)
    289          if not ident:
    290              public = []
    291              for key in keys:
    292                  if key[:1] != '_': public.append(key)
    293              keys = public
    294          skip = len(ident)
    295
    296          # Produce the completion.
    297          if len(keys) == 1:
    298              # Complete with the single possible choice.
    299              if self.cursor() == self.compindex:
    300                  # Second attempt to complete: add finisher and continue.
    301                  self.text.insert('insert', self.compfinish)
    302                  self.compindex = None
    303              else:
    304                  self.text.delete('insert', end)
    305                  self.text.insert('insert', keys[0][skip:])
    306                  try: self.compfinish = finisher(getattr(object, keys[0]))
    307                  except: self.compfinish = ' '
    308                  if self.compfinish == ' ':
    309                      # Object has no members; stop here.
    310                      self.text.insert('insert', ' ')
    311                  else:
    312                      self.compindex = self.cursor()
    313          elif len(keys) > 1:
    314              # Present a menu.
    315              prefix = commonprefix(keys)
    316              keys.sort()
    317              if len(prefix) > skip:
    318                  self.text.delete('insert', end)
    319                  self.text.insert('insert', keys[0][skip:len(prefix)])
    320                  skip = len(prefix)
    321
    322              if len(keys[0]) == skip:
    323                  # Common prefix is a valid choice; next try can finish.
    324                  self.compindex = self.cursor()
    325                  try: self.compfinish = finisher(getattr(object, keys[0]))
    326                  except: self.compfinish = ' '
    327
    328              self.postmenus(keys, skip, end, object)
    329
    330          return 'break'
    331
    332 -    def postmenus(self, keys, skip, cut, object):
    333          """Post a series of menus listing all the given keys, given the
    334          length of the existing part so we can position the menus under the
    335          cursor, and the index at which to insert the completion."""
    336          width = self.winfo_screenwidth()
    337          height = self.winfo_screenheight()
    338          bbox = self.text.bbox('insert - %d c' % skip)
    339          x = self.text.winfo_rootx() + bbox[0] - 4
    340          y = self.text.winfo_rooty() + bbox[1] + bbox[3]
    341
    342          self.compmenus = []
    343          menufont = self.text.cget('font')
    344          menu = Menu(font = menufont, bd = 1, tearoff = 0)
    345          self.compmenus.append(menu)
    346          while keys:
    347              try: finishchar = finisher(getattr(object, keys[0]))
    348              except: finishchar = ' '
    349              def complete(s = self, k = keys[0][skip:], c = cut, f = finishchar):
    350                  if f == ' ': k = k + f
    351                  s.text.delete('insert', c)
    352                  s.text.insert('insert', k)
    353                  s.unpostmenus()
    354                  if f != ' ':
    355                      s.compfinish = f
    356                      s.compindex = s.cursor()
    357              menu.add_command(label = keys[0], command = complete)
    358              menu.update()
    359              if y + menu.winfo_reqheight() >= height:
    360                  menu.delete('end')
    361                  x = x + menu.winfo_reqwidth()
    362                  y = 0
    363                  menu = Menu(font = menufont, bd = 1, tearoff = 0)
    364                  self.compmenus.append(menu)
    365              else:
    366                  keys = keys[1:]
    367              if x + menu.winfo_reqwidth() > width:
    368                  menu.destroy()
    369                  self.compmenus = self.compmenus[:-1]
    370                  self.compmenus[-1].delete('end')
    371                  self.compmenus[-1].add_command(label = '...')
    372                  break
    373
    374          x = self.text.winfo_rootx() + bbox[0] - 4
    375          y = self.text.winfo_rooty() + bbox[1] + bbox[3]
    376          for menu in self.compmenus:
    377              maxtop = height - menu.winfo_reqheight()
    378              if y > maxtop: y = maxtop
    379              menu.post(x, y)
    380              x = x + menu.winfo_reqwidth()
    381          self.text.focus()
    382          self.text.grab_set()
    383
    384 -    def unpostmenus(self):
    385          """Unpost the completion menus."""
    386          for menu in self.compmenus:
    387              menu.destroy()
    388          self.compmenus = []
    389          self.text.grab_release()
    390
    391 -    def cb_cleanup(self, event = None):
    392          if self.compmenus:
    393              self.unpostmenus()
    394          if self.pasted:
    395              self.text.tag_remove('sel', '1.0', 'end')
    396              self.pasted = 0
    397
    398 -    def cb_select(self, event):
    399          """Handle a menu selection event.  We have to check and invoke the
    400          completion menus manually because we are grabbing events to give the
    401          text box keyboard focus."""
    402          if self.compmenus:
    403              for menu in self.compmenus:
    404                  x, y = menu.winfo_rootx(), menu.winfo_rooty()
    405                  w, h = menu.winfo_width(), menu.winfo_height()
    406                  if x < event.x_root < x + w and \
    407                     y < event.y_root < y + h:
    408                      item = menu.index('@%d' % (event.y_root - y))
    409                      menu.invoke(item)
    410                      break
    411              else:
    412                  self.unpostmenus()
    413              return 'break'
    414
    415      # Help mechanism.
    416
    417 -    def cb_help(self, event):
    418          command, context, ident, start, end = self.precontext()
    419          word = self.text.get(start, end)
    420
    421          object = parent = doc = None
    422          skip = 0
    423
    424          try:
    425              parent = eval(context[:-1], self.dict)
    426          except: pass
    427
    428          # Go merrily searching for the help string.
    429          if not object:
    430              try:
    431                  object = getattr(parent, word)
    432                  skip = len(word) - len(ident)
    433              except: pass
    434
    435          if not object:
    436              try:
    437                  object = getattr(parent, ident)
    438              except: pass
    439
    440          if not object:
    441              try:
    442                  object = self.dict[word]
    443                  skip = len(word) - len(ident)
    444              except: pass
    445
    446          if not object:
    447              try:
    448                  object = self.dict[ident]
    449              except: pass
    450
    451          if not object:
    452              try:
    453                  object = __builtin__.__dict__[word]
    454                  skip = len(word) - len(ident)
    455              except: pass
    456
    457          if not object:
    458              try:
    459                  object = __builtins__.__dict__[ident]
    460              except: pass
    461
    462          if not object:
    463              if not ident:
    464                  object = parent
    465
    466          try:
    467              doc = object.__doc__
    468          except: pass
    469
    470          try:
    471              if hasattr(object, '__bases__'):
    472                  doc = object.__init__.__doc__ or doc
    473          except: pass
    474
    475          if doc:
    476              doc = string.rstrip(string.expandtabs(doc))
    477              leftmargin = 99
    478              for line in string.split(doc, '\n')[1:]:
    479                  spaces = len(line) - len(string.lstrip(line))
    480                  if line and spaces < leftmargin: leftmargin = spaces
    481
    482              bbox = self.text.bbox('insert + %d c' % skip)
    483              width = self.winfo_screenwidth()
    484              height = self.winfo_screenheight()
    485              menufont = self.text.cget('font')
    486
    487              help = Menu(font = menufont, bd = 1, tearoff = 0)
    488              try:
    489                  classname = object.__class__.__name__
    490                  help.add_command(label = ' < object of class %s > ' % classname)
    491                  help.add_command(label = '')
    492              except: pass
    493              for line in string.split(doc, '\n'):
    494                  if string.strip(line[:leftmargin]) == '':
    495                      line = line[leftmargin:]
    496                  help.add_command(label = line)
    497              self.compmenus.append(help)
    498
    499              x = self.text.winfo_rootx() + bbox[0] - 4
    500              y = self.text.winfo_rooty() + bbox[1] + bbox[3]
    501              maxtop = height - help.winfo_reqheight()
    502              if y > maxtop: y = maxtop
    503              help.post(x, y)
    504              self.text.focus()
    505              self.text.grab_set()
    506
    507          return 'break'
    508
    509      # Entering commands.
    510
    511 -    def cb_position(self, event):
    512          """Avoid moving into the prompt area."""
    513          self.cb_cleanup()
    514          line, pos = self.cursor()
    515          trimmed, command = self.trim(self.getline())
    516          if pos <= trimmed:
    517              self.text.mark_set('insert', '%d.%d' % (line, trimmed))
    518              return 'break'
    519
    520 -    def cb_backspace(self, event):
    521          self.cb_cleanup()
    522          if self.text.tag_ranges('sel'): return
    523
    524          # Avoid backspacing over the prompt.
    525          line, pos = self.cursor()
    526          trimmed, command = self.trim(self.getline())
    527          if pos <= trimmed: return 'break'
    528
    529          # Extremely basic outdenting.  Needs more work here.
    530          if not string.strip(command[:pos - trimmed]):
    531              step = (pos - trimmed) % 4
    532              cut = pos - (step or 4)
    533              if cut < trimmed: cut = trimmed
    534              self.text.delete('%d.%d' % (line, cut), '%d.%d' % (line, pos))
    535              return 'break'
    536
    537 -    def cb_space(self, event):
    538          self.cb_cleanup()
    539          line, pos = self.cursor()
    540          trimmed, command = self.trim(self.getline())
    541
    542          # Extremely basic indenting.  Needs more work here.
    543          if not string.strip(command[:pos - trimmed]):
    544              start = trimmed + len(command) - len(string.lstrip(command))
    545              self.text.delete('insert', '%d.%d' % (line, start))
    546              step = 4 - (pos - trimmed) % 4
    547              self.text.insert('insert', ' ' * step)
    548              return 'break'
    549
    550 -    def cb_home(self, event):
    551          """Go to the first non - whitespace character in the line."""
    552          self.cb_cleanup()
    553          line, pos = self.cursor()
    554          trimmed, command = self.trim(self.getline())
    555          indent = len(command) - len(string.lstrip(command))
    556          self.text.mark_set('insert', '%d.%d' % (line, trimmed + indent))
    557          return 'break'
    558
    559 -    def cb_ctrlhome(self, event):
    560          """Go to the beginning of the line just after the prompt."""
    561          self.cb_cleanup()
    562          line, pos = self.cursor()
    563          trimmed, command = self.trim(self.getline())
    564          self.text.mark_set('insert', '%d.%d' % (line, trimmed))
    565          return 'break'
    566
    567 -    def cb_nothing(self, event):
    568          return 'break'
    569
    570 -    def cb_return(self, event, doindent = 1):
    571          """Handle a < Return > keystroke by running from the current line
    572          and generating a new prompt."""
    573          self.cb_cleanup()
    574          self.text.tag_delete('compiled')
    575          self.historyindex = None
    576          command = self.getline(trim = 1)
    577          if string.strip(command):
    578              self.history.append(command)
    579
    580          line, pos = self.cursor()
    581          self.text.mark_set('insert', '%d.end' % line)
    582          self.text.insert('insert', '\n')
    583          self.runline(line)
    584
    585          line, pos = self.cursor()
    586          self.text.mark_set('insert', '%d.end' % line)
    587          prompt = self.continuation and sys.ps2 or sys.ps1
    588          if pos > 0:
    589              self.text.insert('insert', '\n' + prompt)
    590          else:
    591              self.text.insert('insert', prompt)
    592
    593          if doindent and not self.error:
    594              self.autoindent(command)
    595          self.error = 0
    596          self.text.see('insert')
    597          return 'break'
    598
    599 -    def autoindent(self, command):
    600          # Extremely basic autoindenting.  Needs more work here.
    601          indent = len(command) - len(string.lstrip(command))
    602          if string.lstrip(command):
    603              self.text.insert('insert', command[:indent])
    604              if string.rstrip(command)[-1] == ':':
    605                  self.text.insert('insert', '    ')
    606
    607 -    def cb_paste(self, event):
    608          """Handle a paste event (middle - click) in the text box.  Pasted
    609          text has any leading Python prompts stripped (at last!!)."""
    610          self.text.tag_delete('compiled')
    611          self.error = 0
    612          self.pasted = 1
    613
    614          try: lines = string.split(self.selection_get(), '\n')
    615          except: return
    616
    617          for i in range(len(lines)):
    618              trimmed, line = self.trim(lines[i])
    619              line = string.rstrip(line)
    620              if not line: continue
    621
    622              self.text.insert('end', line)
    623              self.text.mark_set('insert', 'end')
    624              if i == len(lines) - 2 and lines[i + 1] == '':
    625                  # Indent the last line if it's blank.
    626                  self.cb_return(None, doindent = 1)
    627              elif i < len(lines) - 1:
    628                  self.cb_return(None, doindent = 0)
    629
    630              if self.error: break
    631
    632          return 'break'
    633
    634      # Executing commands.
    635
    636 -    def runline(self, line):
    637          """Run some source code given the number of the last line in the
    638          text box.  Scan backwards to get the entire piece of code to run
    639          if the line is a continuation of previous lines.  Tag the compiled
    640          code so that it can be highlighted according to whether it is
    641          complete, incomplete, or illegal."""
    642          lastline = line
    643          lines = [self.getline(line)]
    644          while lines[0][:len(sys.ps2)] == sys.ps2:
    645              trimmed, lines[0] = self.trim(lines[0])
    646              self.text.tag_add(
    647                  'compiled', '%d.%d' % (line, trimmed), '%d.0' % (line + 1))
    648              line = line - 1
    649              if line < 0: break
    650              lines[:0] = [self.getline(line)]
    651          if lines[0][:len(sys.ps1)] == sys.ps1:
    652              trimmed, lines[0] = self.trim(lines[0])
    653              self.text.tag_add(
    654                  'compiled', '%d.%d' % (line, trimmed), '%d.0' % (line + 1))
    655          else:
    656              self.text.tag_add('compiled', '%d.0' % line, '%d.0' % (line + 1))
    657
    658          source = string.join(lines, '\n')
    659          if not source:
    660              self.continuation = 0
    661              return
    662
    663          status, code = self.compile(source)
    664
    665          if status == 'more':
    666              self.text.tag_configure('compiled', background = self['morecolour'])
    667              self.continuation = 1
    668
    669          elif status == 'bad':
    670              self.text.tag_configure('compiled', background = self['badcolour'])
    671              self.error = 1
    672              self.continuation = 0
    673              self.intraceback = 1
    674              oldout, olderr = sys.stdout, sys.stderr
    675              sys.stdout, sys.stderr = self.stdout, self.stderr
    676              traceback.print_exception(SyntaxError, code, None)
    677              self.stdout, self.stderr = sys.stdout, sys.stderr
    678              sys.stdout, sys.stderr = oldout, olderr
    679              self.intraceback = 0
    680
    681          elif status == 'okay':
    682              if self.getline(lastline) == sys.ps2:
    683                  self.text.tag_remove('compiled', '%d.0' % lastline, 'end')
    684              self.text.tag_configure('compiled', background = self['runcolour'])
    685              self.continuation = 0
    686              self.run(code)
    687
    688 -    def compile(self, source):
    689          """Try to compile a piece of source code, returning a status code
    690          and the compiled result.  If the status code is 'okay' the code is
    691          complete and compiled successfully; if it is 'more' then the code
    692          can be compiled, but an interactive session should wait for more
    693          input; if it is 'bad' then there is a syntax error in the code and
    694          the second returned value is the error message."""
    695          err = err1 = err2 = None
    696          code = code1 = code2 = None
    697
    698          try:
    699              code = compile(source, ' < console > ', 'single')
    700          except SyntaxError, err:
    701              pass
    702          else:
    703              return 'okay', code
    704
    705          try:
    706              code1 = compile(source + '\n', ' < console > ', 'single')
    707          except SyntaxError, err1:
    708              pass
    709          else:
    710              return 'more', code1
    711
    712          try:
    713              code2 = compile(source + '\n\n', ' < console > ', 'single')
    714          except SyntaxError, err2:
    715              pass
    716
    717          try:
    718              code3 = compile(source + '\n', ' < console > ', 'exec')
    719          except SyntaxError, err3:
    720              pass
    721          else:
    722              return 'okay', code3
    723
    724          try:
    725              code4 = compile(source + '\n\n', ' < console > ', 'exec')
    726          except SyntaxError, err4:
    727              pass
    728
    729          if err3[1][2] != err4[1][2]:
    730              return 'more', None
    731
    732          if err1[1][2] != err2[1][2]:
    733              return 'more', None
    734
    735          return 'bad', err1
    736
    737 -    def run(self, code):
    738          """Run a code object within the sandbox for this console.  The
    739          sandbox redirects stdout and stderr to the console, and executes
    740          within the namespace associated with the console."""
    741          oldout, olderr = sys.stdout, sys.stderr
    742          sys.stdout, sys.stderr = self.stdout, self.stderr
    743
    744          try:
    745              exec code in self.dict
    746          except:
    747              self.error = 1
    748              sys.last_type = sys.exc_type
    749              sys.last_value = sys.exc_value
    750              sys.last_traceback = sys.exc_traceback.tb_next
    751              self.intraceback = 1
    752              traceback.print_exception(
    753                  sys.last_type, sys.last_value, sys.last_traceback)
    754              self.intraceback = 0
    755
    756          self.stdout, self.stderr = sys.stdout, sys.stderr
    757          sys.stdout, sys.stderr = oldout, olderr
    758
    759
    760  # Helpers for the completion mechanism.
    761
    762 -def scanclass(klass, result):
    763      for key in klass.__dict__.keys(): result[key] = 1
    764      for base in klass.__bases__: scanclass(base, result)
    765
    766 -def members(object):
    767      result = {}
    768      try:
    769          for key in object.__members__: result[key] = 1
    770          result['__members__'] = 1
    771      except: pass
    772      try:
    773          for key in object.__methods__: result[key] = 1
    774          result['__methods__'] = 1
    775      except: pass
    776      try:
    777          for key in object.__dict__.keys(): result[key] = 1
    778          result['__dict__'] = 1
    779      except: pass
    780      if type(object) is types.ClassType:
    781          scanclass(object, result)
    782          result['__name__'] = 1
    783          result['__bases__'] = 1
    784      if type(object) is types.InstanceType:
    785          scanclass(object.__class__, result)
    786          result['__class__'] = 1
    787      return result.keys()
    788
    789 -def matchingkeys(keys, prefix):
    790      prefixmatch = lambda key, l = len(prefix), p = prefix: key[:l] == p
    791      return filter(prefixmatch, keys)
    792
    793 -def commonprefix(keys):
    794      if not keys: return ''
    795      max = len(keys[0])
    796      prefixes = map(lambda i, key = keys[0]: key[:i], range(max + 1))
    797      for key in keys:
    798          while key[:max] != prefixes[max]:
    799              max = max - 1
    800              if max == 0: return ''
    801      return prefixes[max]
    802
    803  callabletypes = [types.FunctionType, types.MethodType, types.ClassType,
    804                   types.BuiltinFunctionType, types.BuiltinMethodType]
    805  sequencetypes = [types.TupleType, types.ListType]
    806  mappingtypes = [types.DictType]
    807
    808  try:
    809      import ExtensionClass
    810      callabletypes.append(ExtensionClass.ExtensionClassType)
    811  except: pass
    812  try:
    813      import curve
    814      c = curve.Curve()
    815      callabletypes.append(type(c.read))
    816  except: pass
    817
    818 -def finisher(object):
    819      if type(object) in callabletypes:
    820          return '('
    821      elif type(object) in sequencetypes:
    822          return '['
    823      elif type(object) in mappingtypes:
    824          return '{'
    825      elif members(object):
    826          return '.'
    827      return ' '
    828
    829
    830  # Main program.
    831
    832  if __name__ == '__main__':
    833      c = Console(dict={})
    834      c.dict['console'] = c
    835      c.pack(fill = BOTH, expand = 1)
    836      c.master.title('Python Console v%s' % VERSION)
    837      mainloop()
    838