From 7a1ebadb529714d8b4fca02a7bf461f06468a325 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sun, 27 Apr 2008 22:05:23 +0100 Subject: [PATCH] Whoosh, I've switched over to an installable package. Pascal Bleser was kind enough to package bpython for OpenSUSE and sent me a patch to have it working with distutils, which is always nice, so thanks a lot for that, here's the new dir structure. --- CHANGELOG | 48 ++ LICENSE | 22 + README | 110 +++ bpython.py | 1281 ++++++++++++++++++++++++++++++++ bpython/__init__.py | 21 + bpython/formatter.py | 99 +++ build/lib/bpython/__init__.py | 21 + build/lib/bpython/formatter.py | 99 +++ build/scripts-2.5/bpython.py | 1281 ++++++++++++++++++++++++++++++++ setup.py | 48 ++ 10 files changed, 3030 insertions(+) create mode 100644 CHANGELOG create mode 100644 LICENSE create mode 100644 README create mode 100644 bpython.py create mode 100644 bpython/__init__.py create mode 100644 bpython/formatter.py create mode 100644 build/lib/bpython/__init__.py create mode 100644 build/lib/bpython/formatter.py create mode 100755 build/scripts-2.5/bpython.py create mode 100644 setup.py diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..29f9736 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,48 @@ +v0.2.4 +====== +Thanks a lot to Angus Gibson for submitting a patch to fix a problem +I was having with initialising the keyboard stuff in curses properly. + +Also a big thanks to John Beisley for providing the patch that shows +a class __init__ method's argspec on class instantiation. + +I've fixed up the argspec display so it handles really long argspecs +(e.g. subprocess.Popen()) and doesn't crash if something horrible +happens (rather, it avoids letting something horrible happen). + +I decided to add a key that will get rid of the autocomplete window, +since it can get in the way. C-l seemed like a good choice, since +it would work well as a side-effect of redrawing the screen (at +least that makes sense to me). In so doing I also cleaned up a lot +of the reevaluating and resizing code so that a lot of the strange +output seen on Rewind/resize seems to be gone. + +v0.2.3 +====== +The fix for the last bug broke the positioning of the autocomplete +box, whoops. + +v0.2.2 +====== +That pesky bug keeps coming up. I think it's finally nailed but +it's just a matter of testing and hoping. I hate numbers. + +v0.2.1 +====== +I'm having a bit of trouble with some integer division that's +causing trouble when a certain set of circumstances arise, +and I think I've taken care of that little bug, since it's +a real pain in the ass and only creeps up when I'm actually +doing something useful, so I'll test it for a bit and release +it as hopefully a bug fixed version. + +v0.2.0 +====== +A little late in the day to start a changelog, but here goes... +This version fixed another annoying little bug that was causing +crashes given certain exact circumstances. I always find it's the +way with curses and sizing of windows and things... + +I've also got bpython to try looking into pydoc if no matches +are found for the argspec, which means the builtins have argspecs +too now, hooray. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..57ce59d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2008 Bob Farrell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/README b/README new file mode 100644 index 0000000..30df031 --- /dev/null +++ b/README @@ -0,0 +1,110 @@ +bpython - A fancy curses interface to the Python interactive interpreter +======= + +Dependencies +============ +Pygments +pyparsing +(apt-get install python-pyparsing python-pygments) + +Introduction +============ +A few people asked for stuff like syntax highlighting +and autocomplete for the Python interactive interpreter. +IPython seems to offer this (plus you can get readline +behaviour in the vanilla interpreter) but I tried +IPython a couple of times. Perhaps I didn't really get +it, but I get the feeling that the ideas behind IPython +are pretty different to bpython. I didn't want to create +a whole development environment; I simply wanted to +provide a couple of neat features that already exist +and turn them into something a little more interactive. + +The idea is to provide the user with all the features +in-line, much like modern IDEs, but in a simple, +lightweight package that can be run in a terminal +window, so curses seemed like the best choice. +Sorry if you use Windows. + +bpython doesn't attempt to create anything new or +groundbreaking, it simply brings together a few neat +ideas and focuses on practicality and usefulness. +For this reason, the "Rewind" function should be +taken with a pinch of salt, but personally I have +found it to be very useful. I use bpython now +whenever I would normally use the vanilla interpreter, +e.g. for testing out solutions to people's problems +on IRC, quickly testing a method of doing something +without creating a temporary file, etc.. + +I hope you find it useful and please feel free to +submit any bugs/patches (yeah right)/suggestions +to: +robertanthonyfarrell@gmail.com + +Features +======== + +* In-line syntax highlighting. + This uses Pygments for lexing the code as you type, + and colours appropriately. Pygments does a great job + of doing all of the tricky stuff and really leaving + me with very little to do except format the tokens + in all my favourite colours. + +* Readline-like autocomplete with suggestions displayed +as you type. + Thanks to Python's readline interface to libreadline + and a ready-made class for using a Python interpreter's + scope as the dataset, the only work here was displaying + the readline matches as you type in a separate curses + window below/above the cursor. + +* Expected parameter list. + As in a lot of modern IDEs, bpython will attempt to + display a list of parameters for any function you + call. The inspect module is tried first, which works + with any Python function, and then pydoc if that fails, + which seems to be pretty adequate, but obviously + in some cases it's simply not possible. I used pyparsing + to cure my nested parentheses woes; again, it was + nice and easy. + +* Rewind. + I didn't call this "Undo" because I thought that would + be misleading, but "Rewind" is probably as bad. The + idea is that the code entered is kept in memory and + when the Rewind function is called, the last line is + popped and the entire code is re-evaluated. As you can + imagine, this has a lot of potential problems, but for + defining classes and functions, I've found it to be + nothing but useful. + +* Pastebin code/write to file. + I don't really use the save thing much, but the pastebin + thing's great. Hit a key and what you see on the screen + will be sent to a pastebin and a URL is returned for you + to do what you like with. I've hardcoded rafb.net/paste + in for now, that needs to be fixed so it's configurable. + Next release, I promise. + +* Flush curses screen to stdout. + A featurette, perhaps, but I thought it was worth noting. + I can't personally recall a curses app that does this, + perhaps it's often not useful, but when you quit bpython, + the screen data will be flushed to stdout, so it basically + looks the same as if you had quit the vanilla interpreter. + +Known Bugs +========== + +* There's some weirdness when you use rewind sometimes with how +it displays the text afterwards, I haven't had enough time to +sit down and look at exactly what's causing it, as it doesn't +seem to happen a lot, and it fixes itself pretty quickly anyway. + +* Triple quoted strings over multiple lines work, but they're +not highlighted properly. + +* There is no way to configure the keys or the pastebin options, +this is a definite for the next release. diff --git a/bpython.py b/bpython.py new file mode 100644 index 0000000..5ffb837 --- /dev/null +++ b/bpython.py @@ -0,0 +1,1281 @@ +#!/usr/bin/env python +# bpython 0.2.4::fancy curses interface to the Python repl::Bob Farrell 2008 +# +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Requires at least Python 2.5 and pygments (apt-get install python-pyments) +# +import os +import sys +import curses +import code +import codeop +import traceback +import re +import time +import math +import urllib +import rlcompleter +import inspect +import signal +import struct +import termios +import fcntl +from bpython.formatter import BPythonFormatter + +class Opts( object ): + pass +OPTS = Opts() + +try: + from pygments import highlight + from pygments.lexers import PythonLexer +except ImportError: + OPTS.syntax = False +else: + OPTS.syntax = True + +try: + from pyparsing import Forward, Suppress, QuotedString, dblQuotedString, \ + Group, OneOrMore, ZeroOrMore, Literal, Optional, Word, \ + alphas, alphanums, printables, ParseException +except ImportError: + OPTS.argspec = False +else: + import pydoc + OPTS.argspec = True + +# TODO: +# +# Triple-quoted strings over multiple lines are not colourised correctly. +# +# Numerous optimisations can be made but it seems to do all the lookup stuff +# fast enough on even my crappy server so I'm not too bothered about that +# at the moment. +# +# The popup window that displays the argspecs and completion suggestions +# needs to be an instance of a ListWin class or something so I can wrap +# the addstr stuff to a higher level. +# +def DEBUG(s): + """This shouldn't ever be called in any release of bpython, so + beat me up if you find anything calling it.""" + open('/home/bob/tmp/plonker','a').write( "%s\n" % str( s ) ) + +def make_colours(): + """Init all the colours in curses and bang them into a dictionary""" + + for i in range( 63 ): + if i > 7: j = i / 8 + else: j = -1 + curses.init_pair( i+1, i % 8, j ) + + c = {} + # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: + c["k"] = 0 + c["r"] = 1 + c["g"] = 2 + c["y"] = 3 + c["b"] = 4 + c["m"] = 5 + c["c"] = 6 + c["w"] = 7 + c["d"] = -1 + + return c + +class Interpreter( code.InteractiveInterpreter ): + def showtraceback( self ): + """This needs to override the default traceback thing + so it can put it into a pretty colour and maybe other + stuff, I don't know""" + + try: + t, v, tb = sys.exc_info() + sys.last_type = t + sys.last_value = v + sys.last_traceback = tb + tblist = traceback.extract_tb( tb ) + del tblist[:1] + + l = traceback.format_list( tblist ) + if l: + l.insert( 0, "Traceback (most recent call last):\n" ) + l[len(l):] = traceback.format_exception_only( t, v ) + finally: + tblist = tb = None + + self.writetb( l ) + + def writetb( self, l ): + """This outputs the traceback and should be overridden for anything + fancy.""" + map( self.write, [ "\x01y\x03%s" % i for i in l ] ) + + +class Repl: + """Implements the necessary guff for a Python-repl-alike interface + + The execution of the code entered and all that stuff was taken from the + Python code module, I had to copy it instead of inheriting it, I can't + remember why. The rest of the stuff is basically what makes it fancy. + + It reads what you type, passes it to a lexer and highlighter which + returns a formatted string. This then gets passed to echo() which + parses that string and prints to the curses screen in appropriate + colours and/or bold attribute. + + The Repl class also keeps two stacks of lines that the user has typed in: + One to be used for the undo feature. I am not happy with the way this works. + The only way I have been able to think of is to keep the code that's been + typed in in memory and re-evaluate it in its entirety for each "undo" + operation. Obviously this means some operations could be extremely slow. + I'm not even by any means certain that this truly represents a genuine "undo" + implementation, but it does seem to be generally pretty effective. + + If anyone has any suggestions for how this could be improved, I'd be happy + to hear them and implement it/accept a patch. I researched a bit into + the idea of keeping the entire Python state in memory, but this really + seems very difficult (I believe it may actually be impossible to work) + and has its own problems too. + + The other stack is for keeping a history for pressing the up/down keys + to go back and forth between lines. + """#TODO: Split the class up a bit so the curses stuff isn't so integrated. + """ + + """ + + def __init__( self, scr, interp, statusbar=None, idle=None): + """Initialise the repl with, unfortunately, a curses screen passed to it. + This needs to be split up so the curses crap isn't in here. + + interp is a Python code.InteractiveInterpreter instance + + The optional 'idle' parameter is a function that the repl call while + it's blocking (waiting for keypresses). This, again, should be in a + different class""" + + self.buffer = [] + self.scr = scr + self.interp = interp + self.match = False + self.rl_hist = [] + self.stdout_hist = [] + self.s_hist = [] + self.history = [] + self.h_i = 0 + self.in_hist = False + self.evaluating = False + self.do_exit = False + self.cpos = 0 +# Use the interpreter's namespace only for the readline stuff: + self.completer = rlcompleter.Completer( self.interp.locals ) + self.statusbar = statusbar + self.list_win = curses.newwin( 1, 1, 1, 1 ) + self.idle = idle + self.f_string = '' + self.matches = [] + self.argspec = None + + if not OPTS.argspec: + return + + pexp = Forward() + chars = printables.replace('(', '') + chars = chars.replace(')', '') + pexpnest = Optional( Word( chars ) ) + Literal( "(" ) + Optional( Group( pexp ) ) + Optional( Literal( ")" ) ) + pexp << ( OneOrMore( Word( chars ) | pexpnest ) ) + self.pparser = pexp + + def cw( self ): + """Return the current word, i.e. the (incomplete) word + directly to the left of the cursor""" + + if self.cpos: # I don't know if autocomplete should be disabled +# if the cursor isn't at the end of the line, but that's what this does for now. + return + + l = len( self.s ) + + if not self.s or ( not self.s[ l-1 ].isalnum() and self.s[ l-1 ] not in ( '.', '_' ) ): + return + + i = 1 + while i < l+1: + if not self.s[ -i ].isalnum() and self.s[ -i ] not in ( '.', '_' ): + break + i += 1 + return self.s[ -i +1: ] + + + def get_args( self ): + """Check if an unclosed parenthesis exists, then attempt to get the argspec() + for it. On success, update self.argspec and return True, otherwise set + self.argspec to None and return False""" + + def getpydocspec( f, func ): + try: + argspec = pydoc.getdoc( f ) + except NameError: + return None + + rx = re.compile( r'([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)' ) + s = rx.search( argspec ) + if s is None: + return None + + if s.groups()[0] != func: + return None + + args = [ i.strip() for i in s.groups()[1].split(',') ] + return [func, (args, None, None, None)]#None, None, None] + + + def getargspec( func ): + try: + if func in self.interp.locals: + f = self.interp.locals[ func ] + except TypeError: + return None + + else: + try: + f = eval( func, self.interp.locals ) + except Exception: # Same deal with the exceptions :( + return None + + try: + if inspect.isclass(f): + argspec = inspect.getargspec( f.__init__ ) + else: + argspec = inspect.getargspec( f ) + self.argspec = [func, argspec]#[0]]#"Args for %s: " + ", ".join( argspec[0] ) + #self.argspec = self.argspec % func + return True + + except (NameError, TypeError, KeyError), x: + t = getpydocspec( f, func ) + if t is None: + return None + self.argspec = t + return True + + def parse_parens( s ): + """Run a string through the pyparsing pattern for paren + counting.""" + + try: + parsed = self.pparser.parseString( s ).asList() + except ParseException: + return False + + return parsed + + def walk( seq ): + """Walk a nested list and return the last list found that + doesn't have a close paren in it (i.e. the active function)""" + r = None + if isinstance( seq, list ): + if ")" not in seq and "(" in seq: + r = seq[ seq.index('(') - 1 ] + for i in seq: + t = walk( i ) + if t: + r = t + return r + + if not OPTS.argspec: + return False + + t = parse_parens( self.s ) + if not t: + return False + + func = walk( t ) + if not func: + return False + + return getargspec( func ) + + def complete( self ): + """Construct a full list of possible completions and construct and + display them in a window. Also check if there's an available argspec + (via the inspect module) and bang that on top of the completions too.""" + + words = [] + i = 0 + + if not self.get_args(): + self.argspec = None + + cw = self.cw() + if not (cw or self.argspec): + self.scr.redrawwin() + self.scr.refresh() + return None + + if not cw: + self.matches = [] + + try: + self.completer.complete( cw, 0 ) + except Exception: # This sucks, but it's either that or list all the +# exceptions that could possibly be raised here, so if anyone wants to do that, +# feel free to send me a patch. + e = True + else: + e = False + + if (e or not self.completer.matches) and not self.argspec: + self.scr.redrawwin() + return + + if not e and self.completer.matches: + self.matches = sorted( set( self.completer.matches ) ) # remove duplicates and +# restore order + self.show_list( self.matches, self.argspec ) + + def show_list( self, items, topline=None ): + """Display a list of options on the screen.""" + + y, x = self.scr.getyx() + + def calc_lsize(r): + """Calculate the size required on screen to display the list. + Unfortunately Python 2.x doesn't allow assigning to the scope + one level up of a nested function, so the return list on this + function is horrible. I could have used an object and mutated + it from here but it doesn't seem like such a big deal. Hopefully + I can make this prettier when py3k shows up. :)""" + + menu = self.make_list( items ) + if menu: + wl = max( len( i ) for i in items ) + 1 + else: + wl = 1 + l = len( items ) + h, w = self.scr.getmaxyx() + optw = int( w * r ) + opth = ( l / ( optw / wl ) ) + 1 + return menu, wl, l, h, w, optw, opth # uuuurgh + + menu, wl, l, h, w, optw, opth = calc_lsize( 0.6 ) # blllluuurgh + down = ( y < h / 2 ) + + trunc = False + if down: + max_h = h - y + else: + max_h = y + 1 + + if topline: + max_h -= 1 + + while opth + 3 >= max_h: + trunc = True + items = items[ : -1 ] + menu, wl, l, h, w, optw, opth = calc_lsize( 0.6 ) # bllllaaarrgghhh + + if topline and menu: opth += 1 + + if trunc: + menu.append('...') + l += 1 + + + self.list_win.erase() + self.scr.touchwin() + self.scr.noutrefresh() + self.list_win.resize( opth+3, optw+3 ) + + + if down: + self.list_win.mvwin( y + 1, 1 ) + else: + self.list_win.mvwin( y - opth - 3, 1 ) + + rows = opth + 1 + cols = optw / wl + + if topline: + l -= cols * self.mkargspec( topline, down ) + + if menu: + self.list_win.addstr( '\n ' ) + for i in range( 0, l ): + if i+1 >= cols and not i % cols: + self.list_win.addstr( '\n ' ) + self.list_win.addstr( menu[ i ] + ( " " * (wl - len(menu[ i ]))), curses.color_pair( self._C["c"]+1 ) ) + + + self.list_win.border() + self.list_win.noutrefresh() + + self.scr.move( y, x ) + self.scr.refresh() + + def mkargspec( self, topline, down ): + """This figures out what to do with the argspec and puts it nicely into + the list window. It returns the number of lines used to display the argspec. + It's also kind of messy due to it having to call so many addstr() to get + the colouring right, but it seems to be pretty sturdy.""" + + r = 0 + fn = topline[0] + args = topline[1][0] + kwargs = topline[1][3] + _args = topline[1][1] + _kwargs = topline[1][2] + + self.list_win.addstr( '\n ') + self.list_win.addstr( fn, curses.color_pair( self._C["b"]+1 ) | curses.A_BOLD ) + self.list_win.addstr( ': ( ', curses.color_pair( self._C["y"]+1 ) ) + h, w = self.list_win.getmaxyx() + maxh = self.scr.getmaxyx()[0] + + + for k, i in enumerate( args ): + y, x = self.list_win.getyx() + ln = len( str(i) ) + kw = None + if kwargs and k+1 > len(args) - len(kwargs): + kw = '%s' % str(kwargs[ k - (len(args) - len(kwargs))]) + ln += len( kw ) + 1 + + if ln + x >= w: + ty = self.list_win.getbegyx()[0] + if not down and ty > 0: + h +=1 + self.list_win.mvwin( ty-1, 1 ) + self.list_win.resize(h,w) + elif down and h + r < maxh-ty: + h +=1 + self.list_win.resize(h,w) + else: + r += 1 + self.list_win.addstr('\n\t') + + self.list_win.addstr( str(i), curses.color_pair( self._C["g"]+1 ) | curses.A_BOLD ) + if kw: + self.list_win.addstr( '=', curses.color_pair( self._C["c"]+1 ) ) + self.list_win.addstr( kw, curses.color_pair( self._C["g"]+1) ) + if k != len(args) -1: + self.list_win.addstr( ', ', curses.color_pair( self._C["g"]+1 ) ) + + if _args: + self.list_win.addstr( ', ', curses.color_pair( self._C["g"]+1 ) ) + self.list_win.addstr( '*%s' % _args, curses.color_pair( self._C["m"]+1 ) ) + if _kwargs: + self.list_win.addstr( ', ', curses.color_pair( self._C["g"]+1 ) ) + self.list_win.addstr( '**%s' % _kwargs, curses.color_pair( self._C["m"]+1 ) ) + self.list_win.addstr( ' )', curses.color_pair( self._C["y"]+1 ) ) + + return r + + def getstdout( self ): + """This method returns the 'spoofed' stdout buffer, for writing to a file + or sending to a pastebin or whatever.""" + + return "\n".join( self.stdout_hist ) + + def write2file( self ): + """Prompt for a filename and write the current contents of the stdout buffer + to disk.""" + + fn = self.statusbar.prompt( 'Save to file: ' ) + + if '~' in fn: + fn = fn.replace( '~', os.path.expanduser('~') ) + + s = self.getstdout() + + try: + f = open( fn, 'w' ) + f.write( s ) + f.close() + except IOError: + self.statusbar.message("Disk write error for file '%s'." % fn ) + else: + self.statusbar.message( 'Saved to %s' % fn ) + + def pastebin( self ): + """Upload to a pastebin and display the URL in the status bar.""" + + s = self.getstdout() + url = 'http://rafb.net/paste/paste.php' + pdata = { 'lang' : 'Python', + 'cvt_tabs' : 'No', + 'text' : s } + pdata = urllib.urlencode( pdata ) + + self.statusbar.message( 'Posting data to pastebin...' ) + u = urllib.urlopen( url, data=pdata ) + d = u.read() + + rx = re.search( '(http://rafb.net/p/[0-9a-zA-Z]+\.html)', d ) + if not rx: + self.statusbar.message( 'Error parsing pastebin URL! Please report a bug.' ) + return + + + r_url = rx.groups()[ 0 ] + self.statusbar.message( 'Pastebin URL: %s' % r_url, 10 ) + + + def make_list( self, items ): + """Compile a list of items. At the moment this simply returns + the list; it's here in case I decide to add any more functionality. + I originally had this method return a list of items where each item + was prepended with a number/letter so the user could choose an option + but it doesn't seem appropriate for readline-like behaviour.""" + + return items + + + def push( self, s ): + """Push a line of code onto the buffer so it can process it all + at once when a code block ends""" + s = s.rstrip('\n') + self.buffer.append( s ) + + more = self.interp.runsource( "\n".join( self.buffer ) ) + + if not more: + self.buffer = [] + + return more + + def undo( self, n=1 ): + """Go back in the undo history n steps and call reeavluate() + Note that in the program this is called "Rewind" because I + want it to be clear that this is by no means a true undo + implementation, it is merely a convenience bonus.""" + if not self.history: + return None + + if len( self.history ) < n: + n = len( self.history ) + + self.history = self.history[ : -n ] + self.reevaluate() + + def enter_hist( self ): + """Set flags for entering into the history by pressing up/down""" + if not self.in_hist: + self.in_hist = True + self.ts = self.s + + def back( self ): + """Replace the active line with previous line in history and + increment the index to keep track""" + + if not self.rl_hist: + return None + + self.cpos = 0 + self.enter_hist() + + if self.h_i < len( self.rl_hist ): + self.h_i += 1 + + self.s = self.rl_hist[ -self.h_i ].rstrip('\n') + self.print_line( self.s, clr=True ) + + def fwd( self ): + """Same as back() but, well, forward""" + + self.enter_hist() + + self.cpos = 0 + + if self.h_i > 1: + self.h_i -= 1 + self.s = self.rl_hist[ -self.h_i ] + else: + self.h_i = 0 + self.s = self.ts + self.ts = '' + self.in_hist = False + + self.print_line( self.s, clr=True ) + + def redraw( self ): + """Redraw the screen.""" + self.scr.erase() + for k, s in enumerate( self.s_hist ): + DEBUG(s) + if not s: + continue + self.iy, self.ix = self.scr.getyx() + for i in s.split('\x04'): + self.echo( i, redraw=False ) + if k < len( self.s_hist ) -1: + self.scr.addstr( '\n' ) + self.iy, self.ix = self.scr.getyx() + self.print_line( self.s ) + self.scr.refresh() + self.statusbar.refresh() + + def reevaluate( self ): + """Clear the buffer, redraw the screen and re-evaluate the history""" + + self.evaluating = True + self.stdout_hist = [] + self.f_string = '' + self.stdout_hist = [] + self.buffer = [] + self.scr.erase() + self.s_hist = [] + + self.prompt( False ) + + self.iy, self.ix = self.scr.getyx() + for line in self.history: + self.stdout_hist[-1] += line.rstrip('\n') + self.print_line( line ) + self.s_hist[-1] += self.f_string + self.scr.addstr( '\n' ) # I decided it was easier to just do this manually +# than to make the print_line and history stuff more flexible. + more = self.push( line ) + self.prompt( more ) + self.iy, self.ix = self.scr.getyx() + + self.s = '' + self.scr.refresh() + + self.evaluating = False + #map( self.push, self.history ) # <-- That's how simple this function was at first :( + + def prompt( self, more ): + """Show the appropriate Python prompt""" + if not more: + self.echo( "\x01g\x03>>> " ) + self.stdout_hist.append('>>> ') + self.s_hist.append( '\x01g\x03>>> \x04' ) + else: + self.echo( "\x01r\x03... " ) + self.stdout_hist.append('... ') + self.s_hist.append( '\x01r\x03... \x04' ) + + def repl( self ): + """Initialise the repl and jump into the loop. This method also + has to keep a stack of lines entered for the horrible "undo" + feature. It also tracks everything that would normally go to stdout + in the normal Python interpreter so it can quickly write it to + stdout on exit after curses.endwin(), as well as a history of lines + entered for using up/down to go back and forth (which has to be separate + to the evaluation history, which will be truncated when undoing.""" + + self.iy, self.ix = self.scr.getyx() + more = False + while not self.do_exit: + self.f_string = '' + self.prompt( more ) + try: + inp = self.get_line() + except KeyboardInterrupt: + self.statusbar.message('KeyboardInterrupt') + self.scr.addstr('\n') + continue + + self.scr.redrawwin() + if self.do_exit: + return + self.h_i = 0 + self.history.append( inp ) + self.s_hist[-1] += self.f_string + self.stdout_hist[-1] += inp.rstrip('\n') + self.rl_hist.append( inp ) # Keep two copies so you can go up and down in the hist + more = self.push( inp ) + + def size( self, scr ): + """Set instance attributes for x and y top left corner coordinates + and width and heigth for the window.""" + h, w = stdscr.getmaxyx() + self.y = 0 + self.w = w + self.h = h-1 + self.x = 0 + + def resize( self ): + """This method exists simply to keep it straight forward when initialising + a window and resizing it.""" + self.size( self.scr ) + self.scr.erase() + self.scr.resize( self.h, self.w ) + self.scr.mvwin( self.y, self.x ) + self.redraw() + + def write( self, s ): + """For overriding stdout defaults""" + if s.rstrip('\n'): + if '\x03' in s: + t = s.split('\x03')[1].rstrip('\n') + else: + t = s.rstrip('\n') + self.stdout_hist.append( t ) + self.echo( s ) + self.s_hist.append( s.rstrip('\n') ) + + def echo( self, s, redraw=True ): + """Parse and echo a formatted string with appropriate attributes. It uses the + formatting method as defined in formatter.py to parse the srings. It won't update + the screen if it's reevaluating the code (as it does with undo).""" + + a = curses.color_pair( 0 ) + if '\x01' in s: + rx = re.search( '\x01([a-z])([a-z]?)', s ) + if rx: + p = self._C[ rx.groups()[ 0 ] ] + if rx.groups()[ 1 ]: + p *= self._C[ rx.groups()[ 1 ] ] + + a = curses.color_pair( int( p ) + 1 ) + s = re.sub( '\x01[a-z][a-z]?', '', s ) + + if '\x02' in s: + a = a | curses.A_BOLD + s = s.replace( '\x02', '' ) + s = s.replace( '\x03', '' ) + s = s.replace( '\x01', '' ) + + + self.scr.addstr( s, a ) + + if redraw and not self.evaluating: + self.scr.refresh() + + def mvc( self, i, refresh=True ): + """This method moves the cursor relatively from the current + position, where: + 0 == (right) end of current line + length of current line len(self.s) == beginning of current line + and: + current cursor position + i + for positive values of i the cursor will move towards the beginning + of the line, negative values the opposite.""" + y, x = self.scr.getyx() + + if self.cpos == 0 and i < 0: + return + + if x == self.ix and y == self.iy and i >= 1: + return + + h, w = gethw() + if x - i < 0: + y -= 1 + x = w + + if x - i >= w: + y += 1 + x = 0 + i + + self.cpos += i + self.scr.move( y, x - i ) + if refresh: + self.scr.refresh() + + def bs( self ): + """Process a backspace""" +#TODO: All this curses code really ought to be somewhere else. :( + + y, x = self.scr.getyx() + + if x == self.ix and y == self.iy: + return + + if x == 0: + y -= 1 + x = gethw()[1] + + if not self.cpos: + self.s = self.s[ : -1 ] + else: + self.s = self.s[ : -self.cpos-1 ] + self.s[ -self.cpos : ] + self.scr.delch( y, x - 1 ) + self.scr.refresh() + + def clrtobol( self ): + """Clear from cursor to beginning of line; usual C-u behaviour""" + if not self.cpos: + self.s = '' + else: + self.s = self.s[ self.cpos : ] + + self.print_line( self.s, clr=True ) + self.scr.redrawwin() + self.scr.refresh() + + def p_key( self ): + """Process a keypress""" + + + if self.c is None: + return '' + + if self.c in ( chr(127), 'KEY_BACKSPACE' ): + self.bs() + self.complete() + return '' + + elif self.c == chr(18):# C-r + self.undo() + return '' + + elif self.c == 'KEY_UP': # Cursor Up + self.back() + return '' + + elif self.c == 'KEY_DOWN': # Cursor Down + self.fwd() + return '' + + elif self.c == 'KEY_LEFT': # Cursor Left + self.mvc( 1 ) + + elif self.c == 'KEY_RIGHT': # Cursor Right + self.mvc( -1 ) + + elif self.c in ('^U', chr(21) ): # C-u + self.clrtobol() + return '' + + elif self.c in ('^L', chr(12) ): # C-l + self.redraw() + return '' + + elif self.c in ( chr(4), '^D' ): # C-d + self.do_exit = True + return None + + elif self.c == 'KEY_F(2)': + self.write2file() + return '' + + elif self.c == 'KEY_F(8)': + self.pastebin() + return '' + + elif self.c == '\n': + self.lf() + return None + + elif self.c == '\t': + return self.tab() + + elif len( self.c ) == 1 and ord( self.c ) <= 127: + self.addc( self.c ) + self.print_line( self.s ) + + else: + return '' + + + return True + + def tab( self ): + """Process the tab key being hit. If there's only whitespace + in the line or the line is blank then process a normal tab, + otherwise attempt to autocomplete to the best match of possible + choices in the match list.""" + + if self.atbol(): + self.addc( self.c ) + self.print_line( self.s ) + return True + + cw = self.cw() + if cw: + b = self.strbase( self.matches ) + if b: + self.s += b[ len( cw ) : ] + self.print_line( self.s ) + return True + + def strbase( self, l ): + """Probably not the best way of doing it but this function returns + a common base string in a list of strings (for tab completion).""" + + if len( l ) == 1: + return l[0] + + sl = sorted( l, key=str.__len__ ) + for i, c in enumerate( l[-1] ): + if not all( k.startswith( l[-1][:i] ) for k in sl ): + break + + return l[-1][:i-1] + + def atbol( self ): + """Return True or False accordingly if the cursor is at the beginning + of the line (whitespace is ignored). This exists so that p_key() knows + how to handle the tab key being pressed - if there is nothing but white + space before the cursor then process it as a normal tab otherwise attempt + tab completion.""" + + if not self.s.lstrip(): + return True + + def lf( self ): + """Process a linefeed character; it only needs to check the + cursor position and move appropriately so it doesn't clear + the current line after the cursor.""" + if self.cpos: + for i in range( self.cpos ): + self.mvc( -1 ) + + self.echo( "\n" ) + + def addc( self, c ): + """Add a character to the current input line and figure out + where it should go, depending on the cursor position.""" + if not self.cpos: + self.s += self.c + else: + l = len( self.s ) + self.s = self.s[ : l - self.cpos ] + self.c + self.s[ l - self.cpos : ] + + self.complete() + + def print_line( self, s, clr=False ): + """Chuck a line of text through the highlighter, move the cursor + to the beginning of the line and output it to the screen.""" + + if not s: + clr = True + + if OPTS.syntax: + o = highlight( s, PythonLexer(), BPythonFormatter() ) + else: + o = s + + self.f_string = o + self.scr.move( self.iy, self.ix ) + + if clr: + self.scr.clrtoeol() + + if clr and not s: + self.scr.refresh() + + if o: + for t in o.split('\x04'): + self.echo( t.rstrip('\n') ) + + if self.cpos: + t = self.cpos + for i in range( self.cpos ): + self.mvc( 1 ) + self.cpos = t + + def get_line( self ): + """Get a line of text and return it + This function initialises an empty string and gets the + curses cursor position on the screen and stores it + for the echo() function to use later (I think). + Then it waits for key presses and passes them to p_key(), + which returns None if Enter is pressed (that means "Return", + idiot).""" + + + self.ts = '' + self.s = '' + self.iy, self.ix = self.scr.getyx() + self.c = None + self.cpos = 0 + while True: + if self.idle: + self.idle( self ) + try: + self.c = self.scr.getkey() + except curses.error: # I'm quite annoyed with the ambiguity of +# this exception handler. I previously caught "curses.error, x" and accessed +# x.message and checked that it was "no input", which seemed a crappy way of +# doing it. But then I ran it on a different computer and the exception +# seems to have entirely different attributes. So let's hope getkey() doesn't +# raise any other crazy curses exceptions. :) + continue + + if self.p_key() is None: + return self.s + +class Statusbar: + """This class provides the status bar at the bottom of the screen. + It has message() and prompt() methods for user interactivity, as + well as settext() and clear() methods for changing its appearance. + + The check() method needs to be called repeatedly if the statusbar is + going to be aware of when it should update its display after a message() + has been called (it'll display for a couple of seconds and then disappear). + + It should be called as: + foo = Statusbar( stdscr, scr, 'Initial text to display' ) + or, for a blank statusbar: + foo = Statusbar( stdscr, scr ) + + It can also receive the argument 'c' which will be an integer referring + to a curses colour pair, e.g.: + foo = Statusbar( stdscr, 'Hello', c=4 ) + + stdscr should be a curses window object in which to put the status bar. + pwin should be the parent window. To be honest, this is only really here + so the cursor can be returned to the window properly. + + """ + + def __init__( self, scr, pwin, s=None, c=None ): + """Initialise the statusbar and display the initial (text if there is any)""" + self.size( scr ) + self.win = curses.newwin( self.h, self.w, self.y, self.x ) + + self.s = s or '' + self._s = self.s + self.c = c + self.timer = 0 + self.pwin = pwin + self.settext( s, c ) + + def size( self, scr ): + """Set instance attributes for x and y top left corner coordinates + and width and heigth for the window.""" + h, w = gethw() + self.y = h-1 + self.w = w + self.h = 1 + self.x = 0 + + def resize( self ): + """This method exists simply to keep it straight forward when initialising + a window and resizing it.""" + self.size( self.win ) + self.win.mvwin( self.y, self.x ) + self.win.resize( self.h, self.w ) + self.refresh() + + def refresh( self ): + """This is here to make sure the status bar text is redraw properly + after a resize.""" + self.settext( self._s ) + + def check( self ): + """This is the method that should be called every half second or so + to see if the status bar needs updating.""" + if not self.timer: + return + + if time.time() < self.timer: + return + + self.settext( self._s ) + + + def message( self, s, n=3 ): + """Display a message for a short n seconds on the statusbar and return + it to its original state.""" + self.timer = time.time() + n + self.settext( s ) + + + def prompt( self, s='' ): + """Prompt the user for some input (with the optional prompt 's') and + return the input text, then restore the statusbar to its original value.""" + + self.settext( s or '? ', p=True ) + iy, ix = self.win.getyx() + + def bs( s ): + y, x = self.win.getyx() + if x == ix: + return s + s = s[:-1] + self.win.delch(y,x-1) + self.win.move(y,x-1) + return s + + o = '' + while True: + c = self.win.getch() + + if c == 127: + o = bs( o ) + continue + + if not c or c > 127: + continue + c = chr( c ) + + if c == '\n': + break + + self.win.addstr( c ) + o += c + + self.settext( self._s ) + return o + + def settext( self, s, c=None, p=False ): + """Set the text on the status bar to a new permanent value; this is the value + that will be set after a prompt or message. c is the optional curses colour + pair to use (if not specified the last specified colour pair will be used). + p is True if the cursor is expected to stay in the status window (e.g. when + prompting).""" + + self.win.erase() + if len( s ) >= self.w: + s = s[ : self.w-1 ] + + self.s = s + if c: + self.c = c + + if s: + if self.c: + self.win.addstr( s, curses.color_pair( self.c ) ) + else: + self.win.addstr( s ) + + if not p: + self.win.noutrefresh() + self.pwin.refresh() + else: + self.win.refresh() + + def clear( self ): + """Clear the status bar.""" + self.win.clear() + +def init_wins( scr, cols ): + """Initialise the two windows (the main repl interface and the + little status bar at the bottom with some stuff in it)""" +#TODO: Document better what stuff is on the status bar. + + h, w = gethw() + + main_win = curses.newwin( h-1, w, 0, 0 ) + main_win.scrollok( True ) + main_win.keypad(1) # Thanks to Angus Gibson for pointing out +# this missing line which was causing problems that needed dirty +# hackery to fix. :) + + statusbar = Statusbar( scr, main_win, ".:: Exit Rewind Save Pastebin ::.", (cols["g"]) *cols["y"] +1 ) + + return main_win, statusbar + +def sigwinch( scr ): + global DO_RESIZE + DO_RESIZE = True + +def gethw(): + """I found this code on a usenet post, and snipped out the bit I needed, + so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're + a great guy. + + It's unfortunately necessary (unless someone has any better ideas) in order + to allow curses and readline to work together. I looked at the code for + libreadline and noticed this comment: + + /* This is the stuff that is hard for me. I never seem to write good + display routines in C. Let's see how I do this time. */ + + So I'm not going to ask any questions. + + """ + h, w = struct.unpack( + "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000"*8))[0:2] + return h, w + +def idle( caller ): + """This is called once every iteration through the getkey() + loop (currently in the Repl class, see the get_line() method). + The statusbar check needs to go here to take care of timed + messages and the resize handlers need to be here to make + sure it happens conveniently.""" + + global stdscr + + caller.statusbar.check() + + if DO_RESIZE: + do_resize( caller ) + +def do_resize( caller ): + """This needs to hack around readline and curses not playing + nicely together. See also gethw() above.""" + global DO_RESIZE + h, w = gethw() + if not h: + return # Hopefully this shouldn't happen. :) + + curses.endwin() + os.environ["LINES"] = str( h ) + os.environ["COLUMNS"] = str( w ) + curses.doupdate() + DO_RESIZE = False + + caller.resize() + caller.statusbar.resize() + # The list win resizes itself every time it appears so no need to do it here. + +def main( scr ): + """main function for the curses convenience wrapper + + Initialise the two main objects: the interpreter + and the repl. The repl does what a repl does and lots + of other cool stuff like syntax highlighting and stuff. + I've tried to keep it well factored but it needs some + tidying up, especially in separating the curses stuff + from the rest of the repl. + """ + global stdscr + global DO_RESIZE + DO_RESIZE = False + signal.signal( signal.SIGWINCH, lambda x,y: sigwinch(scr) ) + + stdscr = scr + curses.start_color() + curses.use_default_colors() + cols = make_colours() + + scr.timeout( 300 ) + + main_win, statusbar = init_wins( scr, cols ) + + + interpreter = Interpreter() + + repl = Repl( main_win, interpreter, statusbar, idle ) + repl._C = cols + + sys.stdout = repl + sys.stderr = repl + + + repl.repl() + return repl.getstdout() + +o = curses.wrapper( main ) +sys.stdout = sys.__stdout__ +sys.stdout.write( o ) # Fake stdout data so everything's still visible after exiting +sys.stdout.flush() diff --git a/bpython/__init__.py b/bpython/__init__.py new file mode 100644 index 0000000..f5b9b02 --- /dev/null +++ b/bpython/__init__.py @@ -0,0 +1,21 @@ +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. diff --git a/bpython/formatter.py b/bpython/formatter.py new file mode 100644 index 0000000..46ad1f2 --- /dev/null +++ b/bpython/formatter.py @@ -0,0 +1,99 @@ +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# A simple formatter for bpython to work with Pygments. +# Pygments really kicks ass, it made it really easy to +# get the exact behaviour I wanted, thanks Pygments. :) + +from pygments.formatter import Formatter +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Token, Whitespace, Literal, Punctuation + +"""These format strings are pretty ugly. +\x01 represents a colour marker, which + can be proceded by one or two of + the following letters: + k, r, g, y, b, m, c, w, d + Which represent: + blacK, Red, Green, Yellow, Blue, Magenta, + Cyan, White, Default + e.g. \x01y for yellow, + \x01gb for green on blue background + +\x02 represents the bold attribute + +\x03 represents the start of the actual + text that is output (in this case it's + a %s for substitution) + +\x04 represents the end of the string; this is + necessary because the strings are all joined + together at the end so the parser needs them + as delimeters + +""" + +f_strings = { + Keyword : "\x01y\x03%s\x04", + Name : "\x01w\x02\x03%s\x04", + Comment : "\x01b\x03%s\x04", + String : "\x01m\x03%s\x04", + Error : "\x01r\x03%s\x04", + Literal : "\x01r\x03%s\x04", + Literal.String : "\x01m\x03%s\x04", + Token.Literal.Number.Float : "\x01g\x02\x03%s\x04", + Number : "\x01g\x03%s\x04", + Operator : "\x01c\x02\x03%s\x04", + Operator.Word : "\x01c\x02\x03%s\x04", + Punctuation : "\x01c\x02\x03%s\x04", + Generic : "\x01d\x03%s\x04", + Token : "\x01g\x03%s\x04", + Whitespace : "\x02d\x03%s\x04" +} + +class BPythonFormatter( Formatter ): + """This is the custom formatter for bpython. + Its format() method receives the tokensource + and outfile params passed to it from the + Pygments highlight() method and slops + them into the appropriate format string + as defined above, then writes to the outfile + object the final formatted string. + + See the Pygments source for more info; it's pretty + straightforward.""" + + def __init__(self, **options): + Formatter.__init__(self, **options) + + def format( self, tokensource, outfile ): + o = '' + for token, text in tokensource: + if text == '\n': + continue + + if token in f_strings: + o += f_strings[ token ] % text + else: + o += f_strings[ Token ] % text + outfile.write( o.rstrip() ) + diff --git a/build/lib/bpython/__init__.py b/build/lib/bpython/__init__.py new file mode 100644 index 0000000..f5b9b02 --- /dev/null +++ b/build/lib/bpython/__init__.py @@ -0,0 +1,21 @@ +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. diff --git a/build/lib/bpython/formatter.py b/build/lib/bpython/formatter.py new file mode 100644 index 0000000..46ad1f2 --- /dev/null +++ b/build/lib/bpython/formatter.py @@ -0,0 +1,99 @@ +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# A simple formatter for bpython to work with Pygments. +# Pygments really kicks ass, it made it really easy to +# get the exact behaviour I wanted, thanks Pygments. :) + +from pygments.formatter import Formatter +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Token, Whitespace, Literal, Punctuation + +"""These format strings are pretty ugly. +\x01 represents a colour marker, which + can be proceded by one or two of + the following letters: + k, r, g, y, b, m, c, w, d + Which represent: + blacK, Red, Green, Yellow, Blue, Magenta, + Cyan, White, Default + e.g. \x01y for yellow, + \x01gb for green on blue background + +\x02 represents the bold attribute + +\x03 represents the start of the actual + text that is output (in this case it's + a %s for substitution) + +\x04 represents the end of the string; this is + necessary because the strings are all joined + together at the end so the parser needs them + as delimeters + +""" + +f_strings = { + Keyword : "\x01y\x03%s\x04", + Name : "\x01w\x02\x03%s\x04", + Comment : "\x01b\x03%s\x04", + String : "\x01m\x03%s\x04", + Error : "\x01r\x03%s\x04", + Literal : "\x01r\x03%s\x04", + Literal.String : "\x01m\x03%s\x04", + Token.Literal.Number.Float : "\x01g\x02\x03%s\x04", + Number : "\x01g\x03%s\x04", + Operator : "\x01c\x02\x03%s\x04", + Operator.Word : "\x01c\x02\x03%s\x04", + Punctuation : "\x01c\x02\x03%s\x04", + Generic : "\x01d\x03%s\x04", + Token : "\x01g\x03%s\x04", + Whitespace : "\x02d\x03%s\x04" +} + +class BPythonFormatter( Formatter ): + """This is the custom formatter for bpython. + Its format() method receives the tokensource + and outfile params passed to it from the + Pygments highlight() method and slops + them into the appropriate format string + as defined above, then writes to the outfile + object the final formatted string. + + See the Pygments source for more info; it's pretty + straightforward.""" + + def __init__(self, **options): + Formatter.__init__(self, **options) + + def format( self, tokensource, outfile ): + o = '' + for token, text in tokensource: + if text == '\n': + continue + + if token in f_strings: + o += f_strings[ token ] % text + else: + o += f_strings[ Token ] % text + outfile.write( o.rstrip() ) + diff --git a/build/scripts-2.5/bpython.py b/build/scripts-2.5/bpython.py new file mode 100755 index 0000000..3ebd8d1 --- /dev/null +++ b/build/scripts-2.5/bpython.py @@ -0,0 +1,1281 @@ +#!/usr/local/bin/python +# bpython 0.2.4::fancy curses interface to the Python repl::Bob Farrell 2008 +# +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Requires at least Python 2.5 and pygments (apt-get install python-pyments) +# +import os +import sys +import curses +import code +import codeop +import traceback +import re +import time +import math +import urllib +import rlcompleter +import inspect +import signal +import struct +import termios +import fcntl +from bpython.formatter import BPythonFormatter + +class Opts( object ): + pass +OPTS = Opts() + +try: + from pygments import highlight + from pygments.lexers import PythonLexer +except ImportError: + OPTS.syntax = False +else: + OPTS.syntax = True + +try: + from pyparsing import Forward, Suppress, QuotedString, dblQuotedString, \ + Group, OneOrMore, ZeroOrMore, Literal, Optional, Word, \ + alphas, alphanums, printables, ParseException +except ImportError: + OPTS.argspec = False +else: + import pydoc + OPTS.argspec = True + +# TODO: +# +# Triple-quoted strings over multiple lines are not colourised correctly. +# +# Numerous optimisations can be made but it seems to do all the lookup stuff +# fast enough on even my crappy server so I'm not too bothered about that +# at the moment. +# +# The popup window that displays the argspecs and completion suggestions +# needs to be an instance of a ListWin class or something so I can wrap +# the addstr stuff to a higher level. +# +def DEBUG(s): + """This shouldn't ever be called in any release of bpython, so + beat me up if you find anything calling it.""" + open('/home/bob/tmp/plonker','a').write( "%s\n" % str( s ) ) + +def make_colours(): + """Init all the colours in curses and bang them into a dictionary""" + + for i in range( 63 ): + if i > 7: j = i / 8 + else: j = -1 + curses.init_pair( i+1, i % 8, j ) + + c = {} + # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: + c["k"] = 0 + c["r"] = 1 + c["g"] = 2 + c["y"] = 3 + c["b"] = 4 + c["m"] = 5 + c["c"] = 6 + c["w"] = 7 + c["d"] = -1 + + return c + +class Interpreter( code.InteractiveInterpreter ): + def showtraceback( self ): + """This needs to override the default traceback thing + so it can put it into a pretty colour and maybe other + stuff, I don't know""" + + try: + t, v, tb = sys.exc_info() + sys.last_type = t + sys.last_value = v + sys.last_traceback = tb + tblist = traceback.extract_tb( tb ) + del tblist[:1] + + l = traceback.format_list( tblist ) + if l: + l.insert( 0, "Traceback (most recent call last):\n" ) + l[len(l):] = traceback.format_exception_only( t, v ) + finally: + tblist = tb = None + + self.writetb( l ) + + def writetb( self, l ): + """This outputs the traceback and should be overridden for anything + fancy.""" + map( self.write, [ "\x01y\x03%s" % i for i in l ] ) + + +class Repl: + """Implements the necessary guff for a Python-repl-alike interface + + The execution of the code entered and all that stuff was taken from the + Python code module, I had to copy it instead of inheriting it, I can't + remember why. The rest of the stuff is basically what makes it fancy. + + It reads what you type, passes it to a lexer and highlighter which + returns a formatted string. This then gets passed to echo() which + parses that string and prints to the curses screen in appropriate + colours and/or bold attribute. + + The Repl class also keeps two stacks of lines that the user has typed in: + One to be used for the undo feature. I am not happy with the way this works. + The only way I have been able to think of is to keep the code that's been + typed in in memory and re-evaluate it in its entirety for each "undo" + operation. Obviously this means some operations could be extremely slow. + I'm not even by any means certain that this truly represents a genuine "undo" + implementation, but it does seem to be generally pretty effective. + + If anyone has any suggestions for how this could be improved, I'd be happy + to hear them and implement it/accept a patch. I researched a bit into + the idea of keeping the entire Python state in memory, but this really + seems very difficult (I believe it may actually be impossible to work) + and has its own problems too. + + The other stack is for keeping a history for pressing the up/down keys + to go back and forth between lines. + """#TODO: Split the class up a bit so the curses stuff isn't so integrated. + """ + + """ + + def __init__( self, scr, interp, statusbar=None, idle=None): + """Initialise the repl with, unfortunately, a curses screen passed to it. + This needs to be split up so the curses crap isn't in here. + + interp is a Python code.InteractiveInterpreter instance + + The optional 'idle' parameter is a function that the repl call while + it's blocking (waiting for keypresses). This, again, should be in a + different class""" + + self.buffer = [] + self.scr = scr + self.interp = interp + self.match = False + self.rl_hist = [] + self.stdout_hist = [] + self.s_hist = [] + self.history = [] + self.h_i = 0 + self.in_hist = False + self.evaluating = False + self.do_exit = False + self.cpos = 0 +# Use the interpreter's namespace only for the readline stuff: + self.completer = rlcompleter.Completer( self.interp.locals ) + self.statusbar = statusbar + self.list_win = curses.newwin( 1, 1, 1, 1 ) + self.idle = idle + self.f_string = '' + self.matches = [] + self.argspec = None + + if not OPTS.argspec: + return + + pexp = Forward() + chars = printables.replace('(', '') + chars = chars.replace(')', '') + pexpnest = Optional( Word( chars ) ) + Literal( "(" ) + Optional( Group( pexp ) ) + Optional( Literal( ")" ) ) + pexp << ( OneOrMore( Word( chars ) | pexpnest ) ) + self.pparser = pexp + + def cw( self ): + """Return the current word, i.e. the (incomplete) word + directly to the left of the cursor""" + + if self.cpos: # I don't know if autocomplete should be disabled +# if the cursor isn't at the end of the line, but that's what this does for now. + return + + l = len( self.s ) + + if not self.s or ( not self.s[ l-1 ].isalnum() and self.s[ l-1 ] not in ( '.', '_' ) ): + return + + i = 1 + while i < l+1: + if not self.s[ -i ].isalnum() and self.s[ -i ] not in ( '.', '_' ): + break + i += 1 + return self.s[ -i +1: ] + + + def get_args( self ): + """Check if an unclosed parenthesis exists, then attempt to get the argspec() + for it. On success, update self.argspec and return True, otherwise set + self.argspec to None and return False""" + + def getpydocspec( f, func ): + try: + argspec = pydoc.getdoc( f ) + except NameError: + return None + + rx = re.compile( r'([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)' ) + s = rx.search( argspec ) + if s is None: + return None + + if s.groups()[0] != func: + return None + + args = [ i.strip() for i in s.groups()[1].split(',') ] + return [func, (args, None, None, None)]#None, None, None] + + + def getargspec( func ): + try: + if func in self.interp.locals: + f = self.interp.locals[ func ] + except TypeError: + return None + + else: + try: + f = eval( func, self.interp.locals ) + except Exception: # Same deal with the exceptions :( + return None + + try: + if inspect.isclass(f): + argspec = inspect.getargspec( f.__init__ ) + else: + argspec = inspect.getargspec( f ) + self.argspec = [func, argspec]#[0]]#"Args for %s: " + ", ".join( argspec[0] ) + #self.argspec = self.argspec % func + return True + + except (NameError, TypeError, KeyError), x: + t = getpydocspec( f, func ) + if t is None: + return None + self.argspec = t + return True + + def parse_parens( s ): + """Run a string through the pyparsing pattern for paren + counting.""" + + try: + parsed = self.pparser.parseString( s ).asList() + except ParseException: + return False + + return parsed + + def walk( seq ): + """Walk a nested list and return the last list found that + doesn't have a close paren in it (i.e. the active function)""" + r = None + if isinstance( seq, list ): + if ")" not in seq and "(" in seq: + r = seq[ seq.index('(') - 1 ] + for i in seq: + t = walk( i ) + if t: + r = t + return r + + if not OPTS.argspec: + return False + + t = parse_parens( self.s ) + if not t: + return False + + func = walk( t ) + if not func: + return False + + return getargspec( func ) + + def complete( self ): + """Construct a full list of possible completions and construct and + display them in a window. Also check if there's an available argspec + (via the inspect module) and bang that on top of the completions too.""" + + words = [] + i = 0 + + if not self.get_args(): + self.argspec = None + + cw = self.cw() + if not (cw or self.argspec): + self.scr.redrawwin() + self.scr.refresh() + return None + + if not cw: + self.matches = [] + + try: + self.completer.complete( cw, 0 ) + except Exception: # This sucks, but it's either that or list all the +# exceptions that could possibly be raised here, so if anyone wants to do that, +# feel free to send me a patch. + e = True + else: + e = False + + if (e or not self.completer.matches) and not self.argspec: + self.scr.redrawwin() + return + + if not e and self.completer.matches: + self.matches = sorted( set( self.completer.matches ) ) # remove duplicates and +# restore order + self.show_list( self.matches, self.argspec ) + + def show_list( self, items, topline=None ): + """Display a list of options on the screen.""" + + y, x = self.scr.getyx() + + def calc_lsize(r): + """Calculate the size required on screen to display the list. + Unfortunately Python 2.x doesn't allow assigning to the scope + one level up of a nested function, so the return list on this + function is horrible. I could have used an object and mutated + it from here but it doesn't seem like such a big deal. Hopefully + I can make this prettier when py3k shows up. :)""" + + menu = self.make_list( items ) + if menu: + wl = max( len( i ) for i in items ) + 1 + else: + wl = 1 + l = len( items ) + h, w = self.scr.getmaxyx() + optw = int( w * r ) + opth = ( l / ( optw / wl ) ) + 1 + return menu, wl, l, h, w, optw, opth # uuuurgh + + menu, wl, l, h, w, optw, opth = calc_lsize( 0.6 ) # blllluuurgh + down = ( y < h / 2 ) + + trunc = False + if down: + max_h = h - y + else: + max_h = y + 1 + + if topline: + max_h -= 1 + + while opth + 3 >= max_h: + trunc = True + items = items[ : -1 ] + menu, wl, l, h, w, optw, opth = calc_lsize( 0.6 ) # bllllaaarrgghhh + + if topline and menu: opth += 1 + + if trunc: + menu.append('...') + l += 1 + + + self.list_win.erase() + self.scr.touchwin() + self.scr.noutrefresh() + self.list_win.resize( opth+3, optw+3 ) + + + if down: + self.list_win.mvwin( y + 1, 1 ) + else: + self.list_win.mvwin( y - opth - 3, 1 ) + + rows = opth + 1 + cols = optw / wl + + if topline: + l -= cols * self.mkargspec( topline, down ) + + if menu: + self.list_win.addstr( '\n ' ) + for i in range( 0, l ): + if i+1 >= cols and not i % cols: + self.list_win.addstr( '\n ' ) + self.list_win.addstr( menu[ i ] + ( " " * (wl - len(menu[ i ]))), curses.color_pair( self._C["c"]+1 ) ) + + + self.list_win.border() + self.list_win.noutrefresh() + + self.scr.move( y, x ) + self.scr.refresh() + + def mkargspec( self, topline, down ): + """This figures out what to do with the argspec and puts it nicely into + the list window. It returns the number of lines used to display the argspec. + It's also kind of messy due to it having to call so many addstr() to get + the colouring right, but it seems to be pretty sturdy.""" + + r = 0 + fn = topline[0] + args = topline[1][0] + kwargs = topline[1][3] + _args = topline[1][1] + _kwargs = topline[1][2] + + self.list_win.addstr( '\n ') + self.list_win.addstr( fn, curses.color_pair( self._C["b"]+1 ) | curses.A_BOLD ) + self.list_win.addstr( ': ( ', curses.color_pair( self._C["y"]+1 ) ) + h, w = self.list_win.getmaxyx() + maxh = self.scr.getmaxyx()[0] + + + for k, i in enumerate( args ): + y, x = self.list_win.getyx() + ln = len( str(i) ) + kw = None + if kwargs and k+1 > len(args) - len(kwargs): + kw = '%s' % str(kwargs[ k - (len(args) - len(kwargs))]) + ln += len( kw ) + 1 + + if ln + x >= w: + ty = self.list_win.getbegyx()[0] + if not down and ty > 0: + h +=1 + self.list_win.mvwin( ty-1, 1 ) + self.list_win.resize(h,w) + elif down and h + r < maxh-ty: + h +=1 + self.list_win.resize(h,w) + else: + r += 1 + self.list_win.addstr('\n\t') + + self.list_win.addstr( str(i), curses.color_pair( self._C["g"]+1 ) | curses.A_BOLD ) + if kw: + self.list_win.addstr( '=', curses.color_pair( self._C["c"]+1 ) ) + self.list_win.addstr( kw, curses.color_pair( self._C["g"]+1) ) + if k != len(args) -1: + self.list_win.addstr( ', ', curses.color_pair( self._C["g"]+1 ) ) + + if _args: + self.list_win.addstr( ', ', curses.color_pair( self._C["g"]+1 ) ) + self.list_win.addstr( '*%s' % _args, curses.color_pair( self._C["m"]+1 ) ) + if _kwargs: + self.list_win.addstr( ', ', curses.color_pair( self._C["g"]+1 ) ) + self.list_win.addstr( '**%s' % _kwargs, curses.color_pair( self._C["m"]+1 ) ) + self.list_win.addstr( ' )', curses.color_pair( self._C["y"]+1 ) ) + + return r + + def getstdout( self ): + """This method returns the 'spoofed' stdout buffer, for writing to a file + or sending to a pastebin or whatever.""" + + return "\n".join( self.stdout_hist ) + + def write2file( self ): + """Prompt for a filename and write the current contents of the stdout buffer + to disk.""" + + fn = self.statusbar.prompt( 'Save to file: ' ) + + if '~' in fn: + fn = fn.replace( '~', os.path.expanduser('~') ) + + s = self.getstdout() + + try: + f = open( fn, 'w' ) + f.write( s ) + f.close() + except IOError: + self.statusbar.message("Disk write error for file '%s'." % fn ) + else: + self.statusbar.message( 'Saved to %s' % fn ) + + def pastebin( self ): + """Upload to a pastebin and display the URL in the status bar.""" + + s = self.getstdout() + url = 'http://rafb.net/paste/paste.php' + pdata = { 'lang' : 'Python', + 'cvt_tabs' : 'No', + 'text' : s } + pdata = urllib.urlencode( pdata ) + + self.statusbar.message( 'Posting data to pastebin...' ) + u = urllib.urlopen( url, data=pdata ) + d = u.read() + + rx = re.search( '(http://rafb.net/p/[0-9a-zA-Z]+\.html)', d ) + if not rx: + self.statusbar.message( 'Error parsing pastebin URL! Please report a bug.' ) + return + + + r_url = rx.groups()[ 0 ] + self.statusbar.message( 'Pastebin URL: %s' % r_url, 10 ) + + + def make_list( self, items ): + """Compile a list of items. At the moment this simply returns + the list; it's here in case I decide to add any more functionality. + I originally had this method return a list of items where each item + was prepended with a number/letter so the user could choose an option + but it doesn't seem appropriate for readline-like behaviour.""" + + return items + + + def push( self, s ): + """Push a line of code onto the buffer so it can process it all + at once when a code block ends""" + s = s.rstrip('\n') + self.buffer.append( s ) + + more = self.interp.runsource( "\n".join( self.buffer ) ) + + if not more: + self.buffer = [] + + return more + + def undo( self, n=1 ): + """Go back in the undo history n steps and call reeavluate() + Note that in the program this is called "Rewind" because I + want it to be clear that this is by no means a true undo + implementation, it is merely a convenience bonus.""" + if not self.history: + return None + + if len( self.history ) < n: + n = len( self.history ) + + self.history = self.history[ : -n ] + self.reevaluate() + + def enter_hist( self ): + """Set flags for entering into the history by pressing up/down""" + if not self.in_hist: + self.in_hist = True + self.ts = self.s + + def back( self ): + """Replace the active line with previous line in history and + increment the index to keep track""" + + if not self.rl_hist: + return None + + self.cpos = 0 + self.enter_hist() + + if self.h_i < len( self.rl_hist ): + self.h_i += 1 + + self.s = self.rl_hist[ -self.h_i ].rstrip('\n') + self.print_line( self.s, clr=True ) + + def fwd( self ): + """Same as back() but, well, forward""" + + self.enter_hist() + + self.cpos = 0 + + if self.h_i > 1: + self.h_i -= 1 + self.s = self.rl_hist[ -self.h_i ] + else: + self.h_i = 0 + self.s = self.ts + self.ts = '' + self.in_hist = False + + self.print_line( self.s, clr=True ) + + def redraw( self ): + """Redraw the screen.""" + self.scr.erase() + for k, s in enumerate( self.s_hist ): + DEBUG(s) + if not s: + continue + self.iy, self.ix = self.scr.getyx() + for i in s.split('\x04'): + self.echo( i, redraw=False ) + if k < len( self.s_hist ) -1: + self.scr.addstr( '\n' ) + self.iy, self.ix = self.scr.getyx() + self.print_line( self.s ) + self.scr.refresh() + self.statusbar.refresh() + + def reevaluate( self ): + """Clear the buffer, redraw the screen and re-evaluate the history""" + + self.evaluating = True + self.stdout_hist = [] + self.f_string = '' + self.stdout_hist = [] + self.buffer = [] + self.scr.erase() + self.s_hist = [] + + self.prompt( False ) + + self.iy, self.ix = self.scr.getyx() + for line in self.history: + self.stdout_hist[-1] += line.rstrip('\n') + self.print_line( line ) + self.s_hist[-1] += self.f_string + self.scr.addstr( '\n' ) # I decided it was easier to just do this manually +# than to make the print_line and history stuff more flexible. + more = self.push( line ) + self.prompt( more ) + self.iy, self.ix = self.scr.getyx() + + self.s = '' + self.scr.refresh() + + self.evaluating = False + #map( self.push, self.history ) # <-- That's how simple this function was at first :( + + def prompt( self, more ): + """Show the appropriate Python prompt""" + if not more: + self.echo( "\x01g\x03>>> " ) + self.stdout_hist.append('>>> ') + self.s_hist.append( '\x01g\x03>>> \x04' ) + else: + self.echo( "\x01r\x03... " ) + self.stdout_hist.append('... ') + self.s_hist.append( '\x01r\x03... \x04' ) + + def repl( self ): + """Initialise the repl and jump into the loop. This method also + has to keep a stack of lines entered for the horrible "undo" + feature. It also tracks everything that would normally go to stdout + in the normal Python interpreter so it can quickly write it to + stdout on exit after curses.endwin(), as well as a history of lines + entered for using up/down to go back and forth (which has to be separate + to the evaluation history, which will be truncated when undoing.""" + + self.iy, self.ix = self.scr.getyx() + more = False + while not self.do_exit: + self.f_string = '' + self.prompt( more ) + try: + inp = self.get_line() + except KeyboardInterrupt: + self.statusbar.message('KeyboardInterrupt') + self.scr.addstr('\n') + continue + + self.scr.redrawwin() + if self.do_exit: + return + self.h_i = 0 + self.history.append( inp ) + self.s_hist[-1] += self.f_string + self.stdout_hist[-1] += inp.rstrip('\n') + self.rl_hist.append( inp ) # Keep two copies so you can go up and down in the hist + more = self.push( inp ) + + def size( self, scr ): + """Set instance attributes for x and y top left corner coordinates + and width and heigth for the window.""" + h, w = stdscr.getmaxyx() + self.y = 0 + self.w = w + self.h = h-1 + self.x = 0 + + def resize( self ): + """This method exists simply to keep it straight forward when initialising + a window and resizing it.""" + self.size( self.scr ) + self.scr.erase() + self.scr.resize( self.h, self.w ) + self.scr.mvwin( self.y, self.x ) + self.redraw() + + def write( self, s ): + """For overriding stdout defaults""" + if s.rstrip('\n'): + if '\x03' in s: + t = s.split('\x03')[1].rstrip('\n') + else: + t = s.rstrip('\n') + self.stdout_hist.append( t ) + self.echo( s ) + self.s_hist.append( s.rstrip('\n') ) + + def echo( self, s, redraw=True ): + """Parse and echo a formatted string with appropriate attributes. It uses the + formatting method as defined in formatter.py to parse the srings. It won't update + the screen if it's reevaluating the code (as it does with undo).""" + + a = curses.color_pair( 0 ) + if '\x01' in s: + rx = re.search( '\x01([a-z])([a-z]?)', s ) + if rx: + p = self._C[ rx.groups()[ 0 ] ] + if rx.groups()[ 1 ]: + p *= self._C[ rx.groups()[ 1 ] ] + + a = curses.color_pair( int( p ) + 1 ) + s = re.sub( '\x01[a-z][a-z]?', '', s ) + + if '\x02' in s: + a = a | curses.A_BOLD + s = s.replace( '\x02', '' ) + s = s.replace( '\x03', '' ) + s = s.replace( '\x01', '' ) + + + self.scr.addstr( s, a ) + + if redraw and not self.evaluating: + self.scr.refresh() + + def mvc( self, i, refresh=True ): + """This method moves the cursor relatively from the current + position, where: + 0 == (right) end of current line + length of current line len(self.s) == beginning of current line + and: + current cursor position + i + for positive values of i the cursor will move towards the beginning + of the line, negative values the opposite.""" + y, x = self.scr.getyx() + + if self.cpos == 0 and i < 0: + return + + if x == self.ix and y == self.iy and i >= 1: + return + + h, w = gethw() + if x - i < 0: + y -= 1 + x = w + + if x - i >= w: + y += 1 + x = 0 + i + + self.cpos += i + self.scr.move( y, x - i ) + if refresh: + self.scr.refresh() + + def bs( self ): + """Process a backspace""" +#TODO: All this curses code really ought to be somewhere else. :( + + y, x = self.scr.getyx() + + if x == self.ix and y == self.iy: + return + + if x == 0: + y -= 1 + x = gethw()[1] + + if not self.cpos: + self.s = self.s[ : -1 ] + else: + self.s = self.s[ : -self.cpos-1 ] + self.s[ -self.cpos : ] + self.scr.delch( y, x - 1 ) + self.scr.refresh() + + def clrtobol( self ): + """Clear from cursor to beginning of line; usual C-u behaviour""" + if not self.cpos: + self.s = '' + else: + self.s = self.s[ self.cpos : ] + + self.print_line( self.s, clr=True ) + self.scr.redrawwin() + self.scr.refresh() + + def p_key( self ): + """Process a keypress""" + + + if self.c is None: + return '' + + if self.c in ( chr(127), 'KEY_BACKSPACE' ): + self.bs() + self.complete() + return '' + + elif self.c == chr(18):# C-r + self.undo() + return '' + + elif self.c == 'KEY_UP': # Cursor Up + self.back() + return '' + + elif self.c == 'KEY_DOWN': # Cursor Down + self.fwd() + return '' + + elif self.c == 'KEY_LEFT': # Cursor Left + self.mvc( 1 ) + + elif self.c == 'KEY_RIGHT': # Cursor Right + self.mvc( -1 ) + + elif self.c in ('^U', chr(21) ): # C-u + self.clrtobol() + return '' + + elif self.c in ('^L', chr(12) ): # C-l + self.redraw() + return '' + + elif self.c in ( chr(4), '^D' ): # C-d + self.do_exit = True + return None + + elif self.c == 'KEY_F(2)': + self.write2file() + return '' + + elif self.c == 'KEY_F(8)': + self.pastebin() + return '' + + elif self.c == '\n': + self.lf() + return None + + elif self.c == '\t': + return self.tab() + + elif len( self.c ) == 1 and ord( self.c ) <= 127: + self.addc( self.c ) + self.print_line( self.s ) + + else: + return '' + + + return True + + def tab( self ): + """Process the tab key being hit. If there's only whitespace + in the line or the line is blank then process a normal tab, + otherwise attempt to autocomplete to the best match of possible + choices in the match list.""" + + if self.atbol(): + self.addc( self.c ) + self.print_line( self.s ) + return True + + cw = self.cw() + if cw: + b = self.strbase( self.matches ) + if b: + self.s += b[ len( cw ) : ] + self.print_line( self.s ) + return True + + def strbase( self, l ): + """Probably not the best way of doing it but this function returns + a common base string in a list of strings (for tab completion).""" + + if len( l ) == 1: + return l[0] + + sl = sorted( l, key=str.__len__ ) + for i, c in enumerate( l[-1] ): + if not all( k.startswith( l[-1][:i] ) for k in sl ): + break + + return l[-1][:i-1] + + def atbol( self ): + """Return True or False accordingly if the cursor is at the beginning + of the line (whitespace is ignored). This exists so that p_key() knows + how to handle the tab key being pressed - if there is nothing but white + space before the cursor then process it as a normal tab otherwise attempt + tab completion.""" + + if not self.s.lstrip(): + return True + + def lf( self ): + """Process a linefeed character; it only needs to check the + cursor position and move appropriately so it doesn't clear + the current line after the cursor.""" + if self.cpos: + for i in range( self.cpos ): + self.mvc( -1 ) + + self.echo( "\n" ) + + def addc( self, c ): + """Add a character to the current input line and figure out + where it should go, depending on the cursor position.""" + if not self.cpos: + self.s += self.c + else: + l = len( self.s ) + self.s = self.s[ : l - self.cpos ] + self.c + self.s[ l - self.cpos : ] + + self.complete() + + def print_line( self, s, clr=False ): + """Chuck a line of text through the highlighter, move the cursor + to the beginning of the line and output it to the screen.""" + + if not s: + clr = True + + if OPTS.syntax: + o = highlight( s, PythonLexer(), BPythonFormatter() ) + else: + o = s + + self.f_string = o + self.scr.move( self.iy, self.ix ) + + if clr: + self.scr.clrtoeol() + + if clr and not s: + self.scr.refresh() + + if o: + for t in o.split('\x04'): + self.echo( t.rstrip('\n') ) + + if self.cpos: + t = self.cpos + for i in range( self.cpos ): + self.mvc( 1 ) + self.cpos = t + + def get_line( self ): + """Get a line of text and return it + This function initialises an empty string and gets the + curses cursor position on the screen and stores it + for the echo() function to use later (I think). + Then it waits for key presses and passes them to p_key(), + which returns None if Enter is pressed (that means "Return", + idiot).""" + + + self.ts = '' + self.s = '' + self.iy, self.ix = self.scr.getyx() + self.c = None + self.cpos = 0 + while True: + if self.idle: + self.idle( self ) + try: + self.c = self.scr.getkey() + except curses.error: # I'm quite annoyed with the ambiguity of +# this exception handler. I previously caught "curses.error, x" and accessed +# x.message and checked that it was "no input", which seemed a crappy way of +# doing it. But then I ran it on a different computer and the exception +# seems to have entirely different attributes. So let's hope getkey() doesn't +# raise any other crazy curses exceptions. :) + continue + + if self.p_key() is None: + return self.s + +class Statusbar: + """This class provides the status bar at the bottom of the screen. + It has message() and prompt() methods for user interactivity, as + well as settext() and clear() methods for changing its appearance. + + The check() method needs to be called repeatedly if the statusbar is + going to be aware of when it should update its display after a message() + has been called (it'll display for a couple of seconds and then disappear). + + It should be called as: + foo = Statusbar( stdscr, scr, 'Initial text to display' ) + or, for a blank statusbar: + foo = Statusbar( stdscr, scr ) + + It can also receive the argument 'c' which will be an integer referring + to a curses colour pair, e.g.: + foo = Statusbar( stdscr, 'Hello', c=4 ) + + stdscr should be a curses window object in which to put the status bar. + pwin should be the parent window. To be honest, this is only really here + so the cursor can be returned to the window properly. + + """ + + def __init__( self, scr, pwin, s=None, c=None ): + """Initialise the statusbar and display the initial (text if there is any)""" + self.size( scr ) + self.win = curses.newwin( self.h, self.w, self.y, self.x ) + + self.s = s or '' + self._s = self.s + self.c = c + self.timer = 0 + self.pwin = pwin + self.settext( s, c ) + + def size( self, scr ): + """Set instance attributes for x and y top left corner coordinates + and width and heigth for the window.""" + h, w = gethw() + self.y = h-1 + self.w = w + self.h = 1 + self.x = 0 + + def resize( self ): + """This method exists simply to keep it straight forward when initialising + a window and resizing it.""" + self.size( self.win ) + self.win.mvwin( self.y, self.x ) + self.win.resize( self.h, self.w ) + self.refresh() + + def refresh( self ): + """This is here to make sure the status bar text is redraw properly + after a resize.""" + self.settext( self._s ) + + def check( self ): + """This is the method that should be called every half second or so + to see if the status bar needs updating.""" + if not self.timer: + return + + if time.time() < self.timer: + return + + self.settext( self._s ) + + + def message( self, s, n=3 ): + """Display a message for a short n seconds on the statusbar and return + it to its original state.""" + self.timer = time.time() + n + self.settext( s ) + + + def prompt( self, s='' ): + """Prompt the user for some input (with the optional prompt 's') and + return the input text, then restore the statusbar to its original value.""" + + self.settext( s or '? ', p=True ) + iy, ix = self.win.getyx() + + def bs( s ): + y, x = self.win.getyx() + if x == ix: + return s + s = s[:-1] + self.win.delch(y,x-1) + self.win.move(y,x-1) + return s + + o = '' + while True: + c = self.win.getch() + + if c == 127: + o = bs( o ) + continue + + if not c or c > 127: + continue + c = chr( c ) + + if c == '\n': + break + + self.win.addstr( c ) + o += c + + self.settext( self._s ) + return o + + def settext( self, s, c=None, p=False ): + """Set the text on the status bar to a new permanent value; this is the value + that will be set after a prompt or message. c is the optional curses colour + pair to use (if not specified the last specified colour pair will be used). + p is True if the cursor is expected to stay in the status window (e.g. when + prompting).""" + + self.win.erase() + if len( s ) >= self.w: + s = s[ : self.w-1 ] + + self.s = s + if c: + self.c = c + + if s: + if self.c: + self.win.addstr( s, curses.color_pair( self.c ) ) + else: + self.win.addstr( s ) + + if not p: + self.win.noutrefresh() + self.pwin.refresh() + else: + self.win.refresh() + + def clear( self ): + """Clear the status bar.""" + self.win.clear() + +def init_wins( scr, cols ): + """Initialise the two windows (the main repl interface and the + little status bar at the bottom with some stuff in it)""" +#TODO: Document better what stuff is on the status bar. + + h, w = gethw() + + main_win = curses.newwin( h-1, w, 0, 0 ) + main_win.scrollok( True ) + main_win.keypad(1) # Thanks to Angus Gibson for pointing out +# this missing line which was causing problems that needed dirty +# hackery to fix. :) + + statusbar = Statusbar( scr, main_win, ".:: Exit Rewind Save Pastebin ::.", (cols["g"]) *cols["y"] +1 ) + + return main_win, statusbar + +def sigwinch( scr ): + global DO_RESIZE + DO_RESIZE = True + +def gethw(): + """I found this code on a usenet post, and snipped out the bit I needed, + so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're + a great guy. + + It's unfortunately necessary (unless someone has any better ideas) in order + to allow curses and readline to work together. I looked at the code for + libreadline and noticed this comment: + + /* This is the stuff that is hard for me. I never seem to write good + display routines in C. Let's see how I do this time. */ + + So I'm not going to ask any questions. + + """ + h, w = struct.unpack( + "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000"*8))[0:2] + return h, w + +def idle( caller ): + """This is called once every iteration through the getkey() + loop (currently in the Repl class, see the get_line() method). + The statusbar check needs to go here to take care of timed + messages and the resize handlers need to be here to make + sure it happens conveniently.""" + + global stdscr + + caller.statusbar.check() + + if DO_RESIZE: + do_resize( caller ) + +def do_resize( caller ): + """This needs to hack around readline and curses not playing + nicely together. See also gethw() above.""" + global DO_RESIZE + h, w = gethw() + if not h: + return # Hopefully this shouldn't happen. :) + + curses.endwin() + os.environ["LINES"] = str( h ) + os.environ["COLUMNS"] = str( w ) + curses.doupdate() + DO_RESIZE = False + + caller.resize() + caller.statusbar.resize() + # The list win resizes itself every time it appears so no need to do it here. + +def main( scr ): + """main function for the curses convenience wrapper + + Initialise the two main objects: the interpreter + and the repl. The repl does what a repl does and lots + of other cool stuff like syntax highlighting and stuff. + I've tried to keep it well factored but it needs some + tidying up, especially in separating the curses stuff + from the rest of the repl. + """ + global stdscr + global DO_RESIZE + DO_RESIZE = False + signal.signal( signal.SIGWINCH, lambda x,y: sigwinch(scr) ) + + stdscr = scr + curses.start_color() + curses.use_default_colors() + cols = make_colours() + + scr.timeout( 300 ) + + main_win, statusbar = init_wins( scr, cols ) + + + interpreter = Interpreter() + + repl = Repl( main_win, interpreter, statusbar, idle ) + repl._C = cols + + sys.stdout = repl + sys.stderr = repl + + + repl.repl() + return repl.getstdout() + +o = curses.wrapper( main ) +sys.stdout = sys.__stdout__ +sys.stdout.write( o ) # Fake stdout data so everything's still visible after exiting +sys.stdout.flush() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..923334f --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +from distutils.command.install_data import install_data +from distutils.sysconfig import get_python_lib +from distutils.core import setup, Extension +from distutils.dep_util import newer +from distutils.log import info +from distutils import sysconfig +import distutils.file_util +import distutils.dir_util +import sys, os +import glob +import re + +# Make distutils copy bpython.py to bpython +copy_file_orig = distutils.file_util.copy_file +copy_tree_orig = distutils.dir_util.copy_tree +def copy_file(src, dst, *args, **kwargs): + if dst.endswith("bin/bpython.py"): + dst = dst[:-3] + return copy_file_orig(src, dst, *args, **kwargs) +def copy_tree(*args, **kwargs): + outputs = copy_tree_orig(*args, **kwargs) + for i in range(len(outputs)): + if outputs[i].endswith("bin/bpython.py"): + outputs[i] = outputs[i][:-3] + return outputs +distutils.file_util.copy_file = copy_file +distutils.dir_util.copy_tree = copy_tree + +PYTHONLIB = os.path.join(get_python_lib(standard_lib=1, prefix=""), + "site-packages") + +setup(name="bpython", + version = "0.2.4", + description = "Fancy Interface to the Python Interpreter", + author = "Robert Anthony Farrell", + author_email = "robertanthonyfarrell@gmail.com", + license = "MIT/X", + url = "http://www.noiseforfree.com/bpython/", + long_description = +"""\ +bpython is a fancy interface to the Python interpreter for Unix-like operating systems. +""", + packages = ["bpython"], + scripts = ["bpython.py"], + ) + +