diff options
author | Georg Brandl <georg@python.org> | 2010-12-30 21:33:07 (GMT) |
---|---|---|
committer | Georg Brandl <georg@python.org> | 2010-12-30 21:33:07 (GMT) |
commit | 7fafbc95c0c963438197c9a43fe893c4ea6fe759 (patch) | |
tree | 5c9ceda4bdc5260236a230554b9ed56b8c0cdbd3 /Tools/demo | |
parent | 6f17e2df29a865a29447531e89fb22be710e382d (diff) | |
download | cpython-7fafbc95c0c963438197c9a43fe893c4ea6fe759.zip cpython-7fafbc95c0c963438197c9a43fe893c4ea6fe759.tar.gz cpython-7fafbc95c0c963438197c9a43fe893c4ea6fe759.tar.bz2 |
More cleanup: Move some demos into a dedicated Tools/demo dir, move 2to3 demo to Tools, and remove all the other Demo content.
Diffstat (limited to 'Tools/demo')
-rw-r--r-- | Tools/demo/Eiffel.py | 141 | ||||
-rw-r--r-- | Tools/demo/Vec.py | 68 | ||||
-rwxr-xr-x | Tools/demo/beer.py | 20 | ||||
-rw-r--r-- | Tools/demo/hanoi.py | 154 | ||||
-rwxr-xr-x | Tools/demo/life.py | 250 | ||||
-rwxr-xr-x | Tools/demo/markov.py | 121 | ||||
-rwxr-xr-x | Tools/demo/mcast.py | 80 | ||||
-rwxr-xr-x | Tools/demo/queens.py | 85 | ||||
-rwxr-xr-x | Tools/demo/rpython.py | 36 | ||||
-rwxr-xr-x | Tools/demo/rpythond.py | 55 | ||||
-rw-r--r-- | Tools/demo/sortvisu.py | 636 | ||||
-rw-r--r-- | Tools/demo/ss1.py | 842 |
12 files changed, 2488 insertions, 0 deletions
diff --git a/Tools/demo/Eiffel.py b/Tools/demo/Eiffel.py new file mode 100644 index 0000000..15fa58a --- /dev/null +++ b/Tools/demo/Eiffel.py @@ -0,0 +1,141 @@ +"""Support Eiffel-style preconditions and postconditions.""" + +from types import FunctionType as function + +class EiffelBaseMetaClass(type): + + def __new__(meta, name, bases, dict): + meta.convert_methods(dict) + return super(EiffelBaseMetaClass, meta).__new__(meta, name, bases, + dict) + + @classmethod + def convert_methods(cls, dict): + """Replace functions in dict with EiffelMethod wrappers. + + The dict is modified in place. + + If a method ends in _pre or _post, it is removed from the dict + regardless of whether there is a corresponding method. + """ + # find methods with pre or post conditions + methods = [] + for k, v in dict.items(): + if k.endswith('_pre') or k.endswith('_post'): + assert isinstance(v, function) + elif isinstance(v, function): + methods.append(k) + for m in methods: + pre = dict.get("%s_pre" % m) + post = dict.get("%s_post" % m) + if pre or post: + dict[k] = cls.make_eiffel_method(dict[m], pre, post) + +class EiffelMetaClass1(EiffelBaseMetaClass): + # an implementation of the "eiffel" meta class that uses nested functions + + @staticmethod + def make_eiffel_method(func, pre, post): + def method(self, *args, **kwargs): + if pre: + pre(self, *args, **kwargs) + x = func(self, *args, **kwargs) + if post: + post(self, x, *args, **kwargs) + return x + + if func.__doc__: + method.__doc__ = func.__doc__ + + return method + +class EiffelMethodWrapper: + + def __init__(self, inst, descr): + self._inst = inst + self._descr = descr + + def __call__(self, *args, **kwargs): + return self._descr.callmethod(self._inst, args, kwargs) + +class EiffelDescriptor(object): + + def __init__(self, func, pre, post): + self._func = func + self._pre = pre + self._post = post + + self.__name__ = func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, obj, cls): + return EiffelMethodWrapper(obj, self) + + def callmethod(self, inst, args, kwargs): + if self._pre: + self._pre(inst, *args, **kwargs) + x = self._func(inst, *args, **kwargs) + if self._post: + self._post(inst, x, *args, **kwargs) + return x + +class EiffelMetaClass2(EiffelBaseMetaClass): + # an implementation of the "eiffel" meta class that uses descriptors + + make_eiffel_method = EiffelDescriptor + +def _test(metaclass): + class Eiffel(metaclass=metaclass): + pass + + class Test(Eiffel): + + def m(self, arg): + """Make it a little larger""" + return arg + 1 + + def m2(self, arg): + """Make it a little larger""" + return arg + 1 + + def m2_pre(self, arg): + assert arg > 0 + + def m2_post(self, result, arg): + assert result > arg + + class Sub(Test): + def m2(self, arg): + return arg**2 + def m2_post(self, Result, arg): + super(Sub, self).m2_post(Result, arg) + assert Result < 100 + + t = Test() + t.m(1) + t.m2(1) + try: + t.m2(0) + except AssertionError: + pass + else: + assert False + + s = Sub() + try: + s.m2(1) + except AssertionError: + pass # result == arg + else: + assert False + try: + s.m2(10) + except AssertionError: + pass # result == 100 + else: + assert False + s.m2(5) + +if __name__ == "__main__": + _test(EiffelMetaClass1) + _test(EiffelMetaClass2) diff --git a/Tools/demo/Vec.py b/Tools/demo/Vec.py new file mode 100644 index 0000000..787af69 --- /dev/null +++ b/Tools/demo/Vec.py @@ -0,0 +1,68 @@ +class Vec: + """ A simple vector class + + Instances of the Vec class can be constructed from numbers + + >>> a = Vec(1, 2, 3) + >>> b = Vec(3, 2, 1) + + added + >>> a + b + Vec(4, 4, 4) + + subtracted + >>> a - b + Vec(-2, 0, 2) + + and multiplied by a scalar on the left + >>> 3.0 * a + Vec(3.0, 6.0, 9.0) + + or on the right + >>> a * 3.0 + Vec(3.0, 6.0, 9.0) + """ + def __init__(self, *v): + self.v = list(v) + + @classmethod + def fromlist(cls, v): + if not isinstance(v, list): + raise TypeError + inst = cls() + inst.v = v + return inst + + def __repr__(self): + args = ', '.join(repr(x) for x in self.v) + return 'Vec({})'.format(args) + + def __len__(self): + return len(self.v) + + def __getitem__(self, i): + return self.v[i] + + def __add__(self, other): + # Element-wise addition + v = [x + y for x, y in zip(self.v, other.v)] + return Vec.fromlist(v) + + def __sub__(self, other): + # Element-wise subtraction + v = [x - y for x, y in zip(self.v, other.v)] + return Vec.fromlist(v) + + def __mul__(self, scalar): + # Multiply by scalar + v = [x * scalar for x in self.v] + return Vec.fromlist(v) + + __rmul__ = __mul__ + + +def test(): + import doctest + doctest.testmod() + +test() diff --git a/Tools/demo/beer.py b/Tools/demo/beer.py new file mode 100755 index 0000000..56eec7b --- /dev/null +++ b/Tools/demo/beer.py @@ -0,0 +1,20 @@ +#! /usr/bin/env python3 + +# By GvR, demystified after a version by Fredrik Lundh. + +import sys + +n = 100 +if sys.argv[1:]: + n = int(sys.argv[1]) + +def bottle(n): + if n == 0: return "no more bottles of beer" + if n == 1: return "one bottle of beer" + return str(n) + " bottles of beer" + +for i in range(n, 0, -1): + print(bottle(i), "on the wall,") + print(bottle(i) + ".") + print("Take one down, pass it around,") + print(bottle(i-1), "on the wall.") diff --git a/Tools/demo/hanoi.py b/Tools/demo/hanoi.py new file mode 100644 index 0000000..34a0bba --- /dev/null +++ b/Tools/demo/hanoi.py @@ -0,0 +1,154 @@ +# Animated Towers of Hanoi using Tk with optional bitmap file in +# background. +# +# Usage: tkhanoi [n [bitmapfile]] +# +# n is the number of pieces to animate; default is 4, maximum 15. +# +# The bitmap file can be any X11 bitmap file (look in +# /usr/include/X11/bitmaps for samples); it is displayed as the +# background of the animation. Default is no bitmap. + +# This uses Steen Lumholt's Tk interface +from tkinter import * + + +# Basic Towers-of-Hanoi algorithm: move n pieces from a to b, using c +# as temporary. For each move, call report() +def hanoi(n, a, b, c, report): + if n <= 0: return + hanoi(n-1, a, c, b, report) + report(n, a, b) + hanoi(n-1, c, b, a, report) + + +# The graphical interface +class Tkhanoi: + + # Create our objects + def __init__(self, n, bitmap = None): + self.n = n + self.tk = tk = Tk() + self.canvas = c = Canvas(tk) + c.pack() + width, height = tk.getint(c['width']), tk.getint(c['height']) + + # Add background bitmap + if bitmap: + self.bitmap = c.create_bitmap(width//2, height//2, + bitmap=bitmap, + foreground='blue') + + # Generate pegs + pegwidth = 10 + pegheight = height//2 + pegdist = width//3 + x1, y1 = (pegdist-pegwidth)//2, height*1//3 + x2, y2 = x1+pegwidth, y1+pegheight + self.pegs = [] + p = c.create_rectangle(x1, y1, x2, y2, fill='black') + self.pegs.append(p) + x1, x2 = x1+pegdist, x2+pegdist + p = c.create_rectangle(x1, y1, x2, y2, fill='black') + self.pegs.append(p) + x1, x2 = x1+pegdist, x2+pegdist + p = c.create_rectangle(x1, y1, x2, y2, fill='black') + self.pegs.append(p) + self.tk.update() + + # Generate pieces + pieceheight = pegheight//16 + maxpiecewidth = pegdist*2//3 + minpiecewidth = 2*pegwidth + self.pegstate = [[], [], []] + self.pieces = {} + x1, y1 = (pegdist-maxpiecewidth)//2, y2-pieceheight-2 + x2, y2 = x1+maxpiecewidth, y1+pieceheight + dx = (maxpiecewidth-minpiecewidth) // (2*max(1, n-1)) + for i in range(n, 0, -1): + p = c.create_rectangle(x1, y1, x2, y2, fill='red') + self.pieces[i] = p + self.pegstate[0].append(i) + x1, x2 = x1 + dx, x2-dx + y1, y2 = y1 - pieceheight-2, y2-pieceheight-2 + self.tk.update() + self.tk.after(25) + + # Run -- never returns + def run(self): + while 1: + hanoi(self.n, 0, 1, 2, self.report) + hanoi(self.n, 1, 2, 0, self.report) + hanoi(self.n, 2, 0, 1, self.report) + hanoi(self.n, 0, 2, 1, self.report) + hanoi(self.n, 2, 1, 0, self.report) + hanoi(self.n, 1, 0, 2, self.report) + + # Reporting callback for the actual hanoi function + def report(self, i, a, b): + if self.pegstate[a][-1] != i: raise RuntimeError # Assertion + del self.pegstate[a][-1] + p = self.pieces[i] + c = self.canvas + + # Lift the piece above peg a + ax1, ay1, ax2, ay2 = c.bbox(self.pegs[a]) + while 1: + x1, y1, x2, y2 = c.bbox(p) + if y2 < ay1: break + c.move(p, 0, -1) + self.tk.update() + + # Move it towards peg b + bx1, by1, bx2, by2 = c.bbox(self.pegs[b]) + newcenter = (bx1+bx2)//2 + while 1: + x1, y1, x2, y2 = c.bbox(p) + center = (x1+x2)//2 + if center == newcenter: break + if center > newcenter: c.move(p, -1, 0) + else: c.move(p, 1, 0) + self.tk.update() + + # Move it down on top of the previous piece + pieceheight = y2-y1 + newbottom = by2 - pieceheight*len(self.pegstate[b]) - 2 + while 1: + x1, y1, x2, y2 = c.bbox(p) + if y2 >= newbottom: break + c.move(p, 0, 1) + self.tk.update() + + # Update peg state + self.pegstate[b].append(i) + + +# Main program +def main(): + import sys + + # First argument is number of pegs, default 4 + if sys.argv[1:]: + n = int(sys.argv[1]) + else: + n = 4 + + # Second argument is bitmap file, default none + if sys.argv[2:]: + bitmap = sys.argv[2] + # Reverse meaning of leading '@' compared to Tk + if bitmap[0] == '@': bitmap = bitmap[1:] + else: bitmap = '@' + bitmap + else: + bitmap = None + + # Create the graphical objects... + h = Tkhanoi(n, bitmap) + + # ...and run! + h.run() + + +# Call main when run as script +if __name__ == '__main__': + main() diff --git a/Tools/demo/life.py b/Tools/demo/life.py new file mode 100755 index 0000000..3cbc053 --- /dev/null +++ b/Tools/demo/life.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# life.py -- A curses-based version of Conway's Game of Life. +# Contributed by AMK +# Mouse support and color by Dafydd Crosby +# +# An empty board will be displayed, and the following commands are available: +# E : Erase the board +# R : Fill the board randomly +# S : Step for a single generation +# C : Update continuously until a key is struck +# Q : Quit +# Cursor keys : Move the cursor around the board +# Space or Enter : Toggle the contents of the cursor's position +# +# TODO : +# Make board updates faster +# + +import random, string, traceback +import curses + +class LifeBoard: + """Encapsulates a Life board + + Attributes: + X,Y : horizontal and vertical size of the board + state : dictionary mapping (x,y) to 0 or 1 + + Methods: + display(update_board) -- If update_board is true, compute the + next generation. Then display the state + of the board and refresh the screen. + erase() -- clear the entire board + makeRandom() -- fill the board randomly + set(y,x) -- set the given cell to Live; doesn't refresh the screen + toggle(y,x) -- change the given cell from live to dead, or vice + versa, and refresh the screen display + + """ + def __init__(self, scr, char=ord('*')): + """Create a new LifeBoard instance. + + scr -- curses screen object to use for display + char -- character used to render live cells (default: '*') + """ + self.state = {} + self.scr = scr + Y, X = self.scr.getmaxyx() + self.X, self.Y = X-2, Y-2-1 + self.char = char + self.scr.clear() + + # Draw a border around the board + border_line = '+'+(self.X*'-')+'+' + self.scr.addstr(0, 0, border_line) + self.scr.addstr(self.Y+1,0, border_line) + for y in range(0, self.Y): + self.scr.addstr(1+y, 0, '|') + self.scr.addstr(1+y, self.X+1, '|') + self.scr.refresh() + + def set(self, y, x): + """Set a cell to the live state""" + if x<0 or self.X<=x or y<0 or self.Y<=y: + raise ValueError("Coordinates out of range %i,%i"% (y,x)) + self.state[x,y] = 1 + + def toggle(self, y, x): + """Toggle a cell's state between live and dead""" + if x<0 or self.X<=x or y<0 or self.Y<=y: + raise ValueError("Coordinates out of range %i,%i"% (y,x)) + if (x,y) in self.state: + del self.state[x,y] + self.scr.addch(y+1, x+1, ' ') + else: + self.state[x,y] = 1 + if curses.has_colors(): + #Let's pick a random color! + self.scr.attrset(curses.color_pair(random.randrange(1,7))) + self.scr.addch(y+1, x+1, self.char) + self.scr.attrset(0) + self.scr.refresh() + + def erase(self): + """Clear the entire board and update the board display""" + self.state = {} + self.display(update_board=False) + + def display(self, update_board=True): + """Display the whole board, optionally computing one generation""" + M,N = self.X, self.Y + if not update_board: + for i in range(0, M): + for j in range(0, N): + if (i,j) in self.state: + self.scr.addch(j+1, i+1, self.char) + else: + self.scr.addch(j+1, i+1, ' ') + self.scr.refresh() + return + + d = {} + self.boring = 1 + for i in range(0, M): + L = range( max(0, i-1), min(M, i+2) ) + for j in range(0, N): + s = 0 + live = (i,j) in self.state + for k in range( max(0, j-1), min(N, j+2) ): + for l in L: + if (l,k) in self.state: + s += 1 + s -= live + if s == 3: + # Birth + d[i,j] = 1 + if curses.has_colors(): + #Let's pick a random color! + self.scr.attrset(curses.color_pair(random.randrange(1,7))) + self.scr.addch(j+1, i+1, self.char) + self.scr.attrset(0) + if not live: self.boring = 0 + elif s == 2 and live: d[i,j] = 1 # Survival + elif live: + # Death + self.scr.addch(j+1, i+1, ' ') + self.boring = 0 + self.state = d + self.scr.refresh() + + def makeRandom(self): + "Fill the board with a random pattern" + self.state = {} + for i in range(0, self.X): + for j in range(0, self.Y): + if random.random() > 0.5: + self.set(j,i) + + +def erase_menu(stdscr, menu_y): + "Clear the space where the menu resides" + stdscr.move(menu_y, 0) + stdscr.clrtoeol() + stdscr.move(menu_y+1, 0) + stdscr.clrtoeol() + +def display_menu(stdscr, menu_y): + "Display the menu of possible keystroke commands" + erase_menu(stdscr, menu_y) + + # If color, then light the menu up :-) + if curses.has_colors(): + stdscr.attrset(curses.color_pair(1)) + stdscr.addstr(menu_y, 4, + 'Use the cursor keys to move, and space or Enter to toggle a cell.') + stdscr.addstr(menu_y+1, 4, + 'E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit') + stdscr.attrset(0) + +def keyloop(stdscr): + # Clear the screen and display the menu of keys + stdscr.clear() + stdscr_y, stdscr_x = stdscr.getmaxyx() + menu_y = (stdscr_y-3)-1 + display_menu(stdscr, menu_y) + + # If color, then initialize the color pairs + if curses.has_colors(): + curses.init_pair(1, curses.COLOR_BLUE, 0) + curses.init_pair(2, curses.COLOR_CYAN, 0) + curses.init_pair(3, curses.COLOR_GREEN, 0) + curses.init_pair(4, curses.COLOR_MAGENTA, 0) + curses.init_pair(5, curses.COLOR_RED, 0) + curses.init_pair(6, curses.COLOR_YELLOW, 0) + curses.init_pair(7, curses.COLOR_WHITE, 0) + + # Set up the mask to listen for mouse events + curses.mousemask(curses.BUTTON1_CLICKED) + + # Allocate a subwindow for the Life board and create the board object + subwin = stdscr.subwin(stdscr_y-3, stdscr_x, 0, 0) + board = LifeBoard(subwin, char=ord('*')) + board.display(update_board=False) + + # xpos, ypos are the cursor's position + xpos, ypos = board.X//2, board.Y//2 + + # Main loop: + while (1): + stdscr.move(1+ypos, 1+xpos) # Move the cursor + c = stdscr.getch() # Get a keystroke + if 0<c<256: + c = chr(c) + if c in ' \n': + board.toggle(ypos, xpos) + elif c in 'Cc': + erase_menu(stdscr, menu_y) + stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously ' + 'updating the screen.') + stdscr.refresh() + # Activate nodelay mode; getch() will return -1 + # if no keystroke is available, instead of waiting. + stdscr.nodelay(1) + while (1): + c = stdscr.getch() + if c != -1: + break + stdscr.addstr(0,0, '/') + stdscr.refresh() + board.display() + stdscr.addstr(0,0, '+') + stdscr.refresh() + + stdscr.nodelay(0) # Disable nodelay mode + display_menu(stdscr, menu_y) + + elif c in 'Ee': + board.erase() + elif c in 'Qq': + break + elif c in 'Rr': + board.makeRandom() + board.display(update_board=False) + elif c in 'Ss': + board.display() + else: pass # Ignore incorrect keys + elif c == curses.KEY_UP and ypos>0: ypos -= 1 + elif c == curses.KEY_DOWN and ypos<board.Y-1: ypos += 1 + elif c == curses.KEY_LEFT and xpos>0: xpos -= 1 + elif c == curses.KEY_RIGHT and xpos<board.X-1: xpos += 1 + elif c == curses.KEY_MOUSE: + (mouse_id, mouse_x, mouse_y, mouse_z, button_state) = curses.getmouse() + if (mouse_x>0 and mouse_x<board.X+1) and (mouse_y>0 and mouse_y<board.Y+1): + xpos = mouse_x - 1 + ypos = mouse_y - 1 + board.toggle(ypos, xpos) + else: + # They've clicked outside the board + curses.flash() + else: + # Ignore incorrect keys + pass + + +def main(stdscr): + keyloop(stdscr) # Enter the main loop + + +if __name__ == '__main__': + curses.wrapper(main) diff --git a/Tools/demo/markov.py b/Tools/demo/markov.py new file mode 100755 index 0000000..7c08bdb --- /dev/null +++ b/Tools/demo/markov.py @@ -0,0 +1,121 @@ +#! /usr/bin/env python3 + +class Markov: + def __init__(self, histsize, choice): + self.histsize = histsize + self.choice = choice + self.trans = {} + + def add(self, state, next): + self.trans.setdefault(state, []).append(next) + + def put(self, seq): + n = self.histsize + add = self.add + add(None, seq[:0]) + for i in range(len(seq)): + add(seq[max(0, i-n):i], seq[i:i+1]) + add(seq[len(seq)-n:], None) + + def get(self): + choice = self.choice + trans = self.trans + n = self.histsize + seq = choice(trans[None]) + while True: + subseq = seq[max(0, len(seq)-n):] + options = trans[subseq] + next = choice(options) + if not next: + break + seq += next + return seq + + +def test(): + import sys, random, getopt + args = sys.argv[1:] + try: + opts, args = getopt.getopt(args, '0123456789cdwq') + except getopt.error: + print('Usage: %s [-#] [-cddqw] [file] ...' % sys.argv[0]) + print('Options:') + print('-#: 1-digit history size (default 2)') + print('-c: characters (default)') + print('-w: words') + print('-d: more debugging output') + print('-q: no debugging output') + print('Input files (default stdin) are split in paragraphs') + print('separated blank lines and each paragraph is split') + print('in words by whitespace, then reconcatenated with') + print('exactly one space separating words.') + print('Output consists of paragraphs separated by blank') + print('lines, where lines are no longer than 72 characters.') + sys.exit(2) + histsize = 2 + do_words = False + debug = 1 + for o, a in opts: + if '-0' <= o <= '-9': histsize = int(o[1:]) + if o == '-c': do_words = False + if o == '-d': debug += 1 + if o == '-q': debug = 0 + if o == '-w': do_words = True + if not args: + args = ['-'] + + m = Markov(histsize, random.choice) + try: + for filename in args: + if filename == '-': + f = sys.stdin + if f.isatty(): + print('Sorry, need stdin from file') + continue + else: + f = open(filename, 'r') + if debug: print('processing', filename, '...') + text = f.read() + f.close() + paralist = text.split('\n\n') + for para in paralist: + if debug > 1: print('feeding ...') + words = para.split() + if words: + if do_words: + data = tuple(words) + else: + data = ' '.join(words) + m.put(data) + except KeyboardInterrupt: + print('Interrupted -- continue with data read so far') + if not m.trans: + print('No valid input files') + return + if debug: print('done.') + + if debug > 1: + for key in m.trans.keys(): + if key is None or len(key) < histsize: + print(repr(key), m.trans[key]) + if histsize == 0: print(repr(''), m.trans['']) + print() + while True: + data = m.get() + if do_words: + words = data + else: + words = data.split() + n = 0 + limit = 72 + for w in words: + if n + len(w) > limit: + print() + n = 0 + print(w, end=' ') + n += len(w) + 1 + print() + print() + +if __name__ == "__main__": + test() diff --git a/Tools/demo/mcast.py b/Tools/demo/mcast.py new file mode 100755 index 0000000..6ce7c6d --- /dev/null +++ b/Tools/demo/mcast.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# +# Send/receive UDP multicast packets. +# Requires that your OS kernel supports IP multicast. +# +# Usage: +# mcast -s (sender, IPv4) +# mcast -s -6 (sender, IPv6) +# mcast (receivers, IPv4) +# mcast -6 (receivers, IPv6) + +MYPORT = 8123 +MYGROUP_4 = '225.0.0.250' +MYGROUP_6 = 'ff15:7079:7468:6f6e:6465:6d6f:6d63:6173' +MYTTL = 1 # Increase to reach other networks + +import time +import struct +import socket +import sys + +def main(): + group = MYGROUP_6 if "-6" in sys.argv[1:] else MYGROUP_4 + + if "-s" in sys.argv[1:]: + sender(group) + else: + receiver(group) + + +def sender(group): + addrinfo = socket.getaddrinfo(group, None)[0] + + s = socket.socket(addrinfo[0], socket.SOCK_DGRAM) + + # Set Time-to-live (optional) + ttl_bin = struct.pack('@i', MYTTL) + if addrinfo[0] == socket.AF_INET: # IPv4 + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl_bin) + else: + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, ttl_bin) + + while True: + data = repr(time.time()).encode('utf-8') + b'\0' + s.sendto(data, (addrinfo[4][0], MYPORT)) + time.sleep(1) + + +def receiver(group): + # Look up multicast group address in name server and find out IP version + addrinfo = socket.getaddrinfo(group, None)[0] + + # Create a socket + s = socket.socket(addrinfo[0], socket.SOCK_DGRAM) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # Bind it to the port + s.bind(('', MYPORT)) + + group_bin = socket.inet_pton(addrinfo[0], addrinfo[4][0]) + # Join group + if addrinfo[0] == socket.AF_INET: # IPv4 + mreq = group_bin + struct.pack('=I', socket.INADDR_ANY) + s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + else: + mreq = group_bin + struct.pack('@I', 0) + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + + # Loop, printing any data we receive + while True: + data, sender = s.recvfrom(1500) + while data[-1:] == '\0': data = data[:-1] # Strip trailing \0's + print(str(sender) + ' ' + repr(data)) + + +if __name__ == '__main__': + main() diff --git a/Tools/demo/queens.py b/Tools/demo/queens.py new file mode 100755 index 0000000..ffd4bea --- /dev/null +++ b/Tools/demo/queens.py @@ -0,0 +1,85 @@ +#! /usr/bin/env python3 + +"""N queens problem. + +The (well-known) problem is due to Niklaus Wirth. + +This solution is inspired by Dijkstra (Structured Programming). It is +a classic recursive backtracking approach. + +""" + +N = 8 # Default; command line overrides + +class Queens: + + def __init__(self, n=N): + self.n = n + self.reset() + + def reset(self): + n = self.n + self.y = [None] * n # Where is the queen in column x + self.row = [0] * n # Is row[y] safe? + self.up = [0] * (2*n-1) # Is upward diagonal[x-y] safe? + self.down = [0] * (2*n-1) # Is downward diagonal[x+y] safe? + self.nfound = 0 # Instrumentation + + def solve(self, x=0): # Recursive solver + for y in range(self.n): + if self.safe(x, y): + self.place(x, y) + if x+1 == self.n: + self.display() + else: + self.solve(x+1) + self.remove(x, y) + + def safe(self, x, y): + return not self.row[y] and not self.up[x-y] and not self.down[x+y] + + def place(self, x, y): + self.y[x] = y + self.row[y] = 1 + self.up[x-y] = 1 + self.down[x+y] = 1 + + def remove(self, x, y): + self.y[x] = None + self.row[y] = 0 + self.up[x-y] = 0 + self.down[x+y] = 0 + + silent = 0 # If true, count solutions only + + def display(self): + self.nfound = self.nfound + 1 + if self.silent: + return + print('+-' + '--'*self.n + '+') + for y in range(self.n-1, -1, -1): + print('|', end=' ') + for x in range(self.n): + if self.y[x] == y: + print("Q", end=' ') + else: + print(".", end=' ') + print('|') + print('+-' + '--'*self.n + '+') + +def main(): + import sys + silent = 0 + n = N + if sys.argv[1:2] == ['-n']: + silent = 1 + del sys.argv[1] + if sys.argv[1:]: + n = int(sys.argv[1]) + q = Queens(n) + q.silent = silent + q.solve() + print("Found", q.nfound, "solutions.") + +if __name__ == "__main__": + main() diff --git a/Tools/demo/rpython.py b/Tools/demo/rpython.py new file mode 100755 index 0000000..e965720 --- /dev/null +++ b/Tools/demo/rpython.py @@ -0,0 +1,36 @@ +#! /usr/bin/env python3 + +# Remote python client. +# Execute Python commands remotely and send output back. + +import sys +from socket import socket, AF_INET, SOCK_STREAM, SHUT_WR + +PORT = 4127 +BUFSIZE = 1024 + +def main(): + if len(sys.argv) < 3: + print("usage: rpython host command") + sys.exit(2) + host = sys.argv[1] + port = PORT + i = host.find(':') + if i >= 0: + port = int(port[i+1:]) + host = host[:i] + command = ' '.join(sys.argv[2:]) + s = socket(AF_INET, SOCK_STREAM) + s.connect((host, port)) + s.send(command.encode()) + s.shutdown(SHUT_WR) + reply = b'' + while True: + data = s.recv(BUFSIZE) + if not data: + break + reply += data + print(reply.decode(), end=' ') + s.close() + +main() diff --git a/Tools/demo/rpythond.py b/Tools/demo/rpythond.py new file mode 100755 index 0000000..fe24b3d --- /dev/null +++ b/Tools/demo/rpythond.py @@ -0,0 +1,55 @@ +#! /usr/bin/env python3 + +# Remote python server. +# Execute Python commands remotely and send output back. +# WARNING: This version has a gaping security hole -- it accepts requests +# from any host on the Internet! + +import sys +from socket import socket, AF_INET, SOCK_STREAM +import io +import traceback + +PORT = 4127 +BUFSIZE = 1024 + +def main(): + if len(sys.argv) > 1: + port = int(sys.argv[1]) + else: + port = PORT + s = socket(AF_INET, SOCK_STREAM) + s.bind(('', port)) + s.listen(1) + while True: + conn, (remotehost, remoteport) = s.accept() + print('connection from', remotehost, remoteport) + request = b'' + while 1: + data = conn.recv(BUFSIZE) + if not data: + break + request += data + reply = execute(request.decode()) + conn.send(reply.encode()) + conn.close() + +def execute(request): + stdout = sys.stdout + stderr = sys.stderr + sys.stdout = sys.stderr = fakefile = io.StringIO() + try: + try: + exec(request, {}, {}) + except: + print() + traceback.print_exc(100) + finally: + sys.stderr = stderr + sys.stdout = stdout + return fakefile.getvalue() + +try: + main() +except KeyboardInterrupt: + pass diff --git a/Tools/demo/sortvisu.py b/Tools/demo/sortvisu.py new file mode 100644 index 0000000..4173121 --- /dev/null +++ b/Tools/demo/sortvisu.py @@ -0,0 +1,636 @@ +#! /usr/bin/env python3 + +"""Sorting algorithms visualizer using Tkinter. + +This module is comprised of three ``components'': + +- an array visualizer with methods that implement basic sorting +operations (compare, swap) as well as methods for ``annotating'' the +sorting algorithm (e.g. to show the pivot element); + +- a number of sorting algorithms (currently quicksort, insertion sort, +selection sort and bubble sort, as well as a randomization function), +all using the array visualizer for its basic operations and with calls +to its annotation methods; + +- and a ``driver'' class which can be used as a Grail applet or as a +stand-alone application. + +""" + +from tkinter import * +import random + + +XGRID = 10 +YGRID = 10 +WIDTH = 6 + + +class Array: + + class Cancelled(BaseException): + pass + + def __init__(self, master, data=None): + self.master = master + self.frame = Frame(self.master) + self.frame.pack(fill=X) + self.label = Label(self.frame) + self.label.pack() + self.canvas = Canvas(self.frame) + self.canvas.pack() + self.report = Label(self.frame) + self.report.pack() + self.left = self.canvas.create_line(0, 0, 0, 0) + self.right = self.canvas.create_line(0, 0, 0, 0) + self.pivot = self.canvas.create_line(0, 0, 0, 0) + self.items = [] + self.size = self.maxvalue = 0 + if data: + self.setdata(data) + + def setdata(self, data): + olditems = self.items + self.items = [] + for item in olditems: + item.delete() + self.size = len(data) + self.maxvalue = max(data) + self.canvas.config(width=(self.size+1)*XGRID, + height=(self.maxvalue+1)*YGRID) + for i in range(self.size): + self.items.append(ArrayItem(self, i, data[i])) + self.reset("Sort demo, size %d" % self.size) + + speed = "normal" + + def setspeed(self, speed): + self.speed = speed + + def destroy(self): + self.frame.destroy() + + in_mainloop = 0 + stop_mainloop = 0 + + def cancel(self): + self.stop_mainloop = 1 + if self.in_mainloop: + self.master.quit() + + def step(self): + if self.in_mainloop: + self.master.quit() + + def wait(self, msecs): + if self.speed == "fastest": + msecs = 0 + elif self.speed == "fast": + msecs = msecs//10 + elif self.speed == "single-step": + msecs = 1000000000 + if not self.stop_mainloop: + self.master.update() + id = self.master.after(msecs, self.master.quit) + self.in_mainloop = 1 + self.master.mainloop() + self.master.after_cancel(id) + self.in_mainloop = 0 + if self.stop_mainloop: + self.stop_mainloop = 0 + self.message("Cancelled") + raise Array.Cancelled + + def getsize(self): + return self.size + + def show_partition(self, first, last): + for i in range(self.size): + item = self.items[i] + if first <= i < last: + self.canvas.itemconfig(item, fill='red') + else: + self.canvas.itemconfig(item, fill='orange') + self.hide_left_right_pivot() + + def hide_partition(self): + for i in range(self.size): + item = self.items[i] + self.canvas.itemconfig(item, fill='red') + self.hide_left_right_pivot() + + def show_left(self, left): + if not 0 <= left < self.size: + self.hide_left() + return + x1, y1, x2, y2 = self.items[left].position() +## top, bot = HIRO + self.canvas.coords(self.left, (x1 - 2, 0, x1 - 2, 9999)) + self.master.update() + + def show_right(self, right): + if not 0 <= right < self.size: + self.hide_right() + return + x1, y1, x2, y2 = self.items[right].position() + self.canvas.coords(self.right, (x2 + 2, 0, x2 + 2, 9999)) + self.master.update() + + def hide_left_right_pivot(self): + self.hide_left() + self.hide_right() + self.hide_pivot() + + def hide_left(self): + self.canvas.coords(self.left, (0, 0, 0, 0)) + + def hide_right(self): + self.canvas.coords(self.right, (0, 0, 0, 0)) + + def show_pivot(self, pivot): + x1, y1, x2, y2 = self.items[pivot].position() + self.canvas.coords(self.pivot, (0, y1 - 2, 9999, y1 - 2)) + + def hide_pivot(self): + self.canvas.coords(self.pivot, (0, 0, 0, 0)) + + def swap(self, i, j): + if i == j: return + self.countswap() + item = self.items[i] + other = self.items[j] + self.items[i], self.items[j] = other, item + item.swapwith(other) + + def compare(self, i, j): + self.countcompare() + item = self.items[i] + other = self.items[j] + return item.compareto(other) + + def reset(self, msg): + self.ncompares = 0 + self.nswaps = 0 + self.message(msg) + self.updatereport() + self.hide_partition() + + def message(self, msg): + self.label.config(text=msg) + + def countswap(self): + self.nswaps = self.nswaps + 1 + self.updatereport() + + def countcompare(self): + self.ncompares = self.ncompares + 1 + self.updatereport() + + def updatereport(self): + text = "%d cmps, %d swaps" % (self.ncompares, self.nswaps) + self.report.config(text=text) + + +class ArrayItem: + + def __init__(self, array, index, value): + self.array = array + self.index = index + self.value = value + self.canvas = array.canvas + x1, y1, x2, y2 = self.position() + self.item_id = array.canvas.create_rectangle(x1, y1, x2, y2, + fill='red', outline='black', width=1) + self.canvas.tag_bind(self.item_id, '<Button-1>', self.mouse_down) + self.canvas.tag_bind(self.item_id, '<Button1-Motion>', self.mouse_move) + self.canvas.tag_bind(self.item_id, '<ButtonRelease-1>', self.mouse_up) + + def delete(self): + item_id = self.item_id + self.array = None + self.item_id = None + self.canvas.delete(item_id) + + def mouse_down(self, event): + self.lastx = event.x + self.lasty = event.y + self.origx = event.x + self.origy = event.y + self.canvas.tag_raise(self.item_id) + + def mouse_move(self, event): + self.canvas.move(self.item_id, + event.x - self.lastx, event.y - self.lasty) + self.lastx = event.x + self.lasty = event.y + + def mouse_up(self, event): + i = self.nearestindex(event.x) + if i >= self.array.getsize(): + i = self.array.getsize() - 1 + if i < 0: + i = 0 + other = self.array.items[i] + here = self.index + self.array.items[here], self.array.items[i] = other, self + self.index = i + x1, y1, x2, y2 = self.position() + self.canvas.coords(self.item_id, (x1, y1, x2, y2)) + other.setindex(here) + + def setindex(self, index): + nsteps = steps(self.index, index) + if not nsteps: return + if self.array.speed == "fastest": + nsteps = 0 + oldpts = self.position() + self.index = index + newpts = self.position() + trajectory = interpolate(oldpts, newpts, nsteps) + self.canvas.tag_raise(self.item_id) + for pts in trajectory: + self.canvas.coords(self.item_id, pts) + self.array.wait(50) + + def swapwith(self, other): + nsteps = steps(self.index, other.index) + if not nsteps: return + if self.array.speed == "fastest": + nsteps = 0 + myoldpts = self.position() + otheroldpts = other.position() + self.index, other.index = other.index, self.index + mynewpts = self.position() + othernewpts = other.position() + myfill = self.canvas.itemcget(self.item_id, 'fill') + otherfill = self.canvas.itemcget(other.item_id, 'fill') + self.canvas.itemconfig(self.item_id, fill='green') + self.canvas.itemconfig(other.item_id, fill='yellow') + self.array.master.update() + if self.array.speed == "single-step": + self.canvas.coords(self.item_id, mynewpts) + self.canvas.coords(other.item_id, othernewpts) + self.array.master.update() + self.canvas.itemconfig(self.item_id, fill=myfill) + self.canvas.itemconfig(other.item_id, fill=otherfill) + self.array.wait(0) + return + mytrajectory = interpolate(myoldpts, mynewpts, nsteps) + othertrajectory = interpolate(otheroldpts, othernewpts, nsteps) + if self.value > other.value: + self.canvas.tag_raise(self.item_id) + self.canvas.tag_raise(other.item_id) + else: + self.canvas.tag_raise(other.item_id) + self.canvas.tag_raise(self.item_id) + try: + for i in range(len(mytrajectory)): + mypts = mytrajectory[i] + otherpts = othertrajectory[i] + self.canvas.coords(self.item_id, mypts) + self.canvas.coords(other.item_id, otherpts) + self.array.wait(50) + finally: + mypts = mytrajectory[-1] + otherpts = othertrajectory[-1] + self.canvas.coords(self.item_id, mypts) + self.canvas.coords(other.item_id, otherpts) + self.canvas.itemconfig(self.item_id, fill=myfill) + self.canvas.itemconfig(other.item_id, fill=otherfill) + + def compareto(self, other): + myfill = self.canvas.itemcget(self.item_id, 'fill') + otherfill = self.canvas.itemcget(other.item_id, 'fill') + if self.value < other.value: + myflash = 'white' + otherflash = 'black' + outcome = -1 + elif self.value > other.value: + myflash = 'black' + otherflash = 'white' + outcome = 1 + else: + myflash = otherflash = 'grey' + outcome = 0 + try: + self.canvas.itemconfig(self.item_id, fill=myflash) + self.canvas.itemconfig(other.item_id, fill=otherflash) + self.array.wait(500) + finally: + self.canvas.itemconfig(self.item_id, fill=myfill) + self.canvas.itemconfig(other.item_id, fill=otherfill) + return outcome + + def position(self): + x1 = (self.index+1)*XGRID - WIDTH//2 + x2 = x1+WIDTH + y2 = (self.array.maxvalue+1)*YGRID + y1 = y2 - (self.value)*YGRID + return x1, y1, x2, y2 + + def nearestindex(self, x): + return int(round(float(x)/XGRID)) - 1 + + +# Subroutines that don't need an object + +def steps(here, there): + nsteps = abs(here - there) + if nsteps <= 3: + nsteps = nsteps * 3 + elif nsteps <= 5: + nsteps = nsteps * 2 + elif nsteps > 10: + nsteps = 10 + return nsteps + +def interpolate(oldpts, newpts, n): + if len(oldpts) != len(newpts): + raise ValueError("can't interpolate arrays of different length") + pts = [0]*len(oldpts) + res = [tuple(oldpts)] + for i in range(1, n): + for k in range(len(pts)): + pts[k] = oldpts[k] + (newpts[k] - oldpts[k])*i//n + res.append(tuple(pts)) + res.append(tuple(newpts)) + return res + + +# Various (un)sorting algorithms + +def uniform(array): + size = array.getsize() + array.setdata([(size+1)//2] * size) + array.reset("Uniform data, size %d" % size) + +def distinct(array): + size = array.getsize() + array.setdata(range(1, size+1)) + array.reset("Distinct data, size %d" % size) + +def randomize(array): + array.reset("Randomizing") + n = array.getsize() + for i in range(n): + j = random.randint(0, n-1) + array.swap(i, j) + array.message("Randomized") + +def insertionsort(array): + size = array.getsize() + array.reset("Insertion sort") + for i in range(1, size): + j = i-1 + while j >= 0: + if array.compare(j, j+1) <= 0: + break + array.swap(j, j+1) + j = j-1 + array.message("Sorted") + +def selectionsort(array): + size = array.getsize() + array.reset("Selection sort") + try: + for i in range(size): + array.show_partition(i, size) + for j in range(i+1, size): + if array.compare(i, j) > 0: + array.swap(i, j) + array.message("Sorted") + finally: + array.hide_partition() + +def bubblesort(array): + size = array.getsize() + array.reset("Bubble sort") + for i in range(size): + for j in range(1, size): + if array.compare(j-1, j) > 0: + array.swap(j-1, j) + array.message("Sorted") + +def quicksort(array): + size = array.getsize() + array.reset("Quicksort") + try: + stack = [(0, size)] + while stack: + first, last = stack[-1] + del stack[-1] + array.show_partition(first, last) + if last-first < 5: + array.message("Insertion sort") + for i in range(first+1, last): + j = i-1 + while j >= first: + if array.compare(j, j+1) <= 0: + break + array.swap(j, j+1) + j = j-1 + continue + array.message("Choosing pivot") + j, i, k = first, (first+last) // 2, last-1 + if array.compare(k, i) < 0: + array.swap(k, i) + if array.compare(k, j) < 0: + array.swap(k, j) + if array.compare(j, i) < 0: + array.swap(j, i) + pivot = j + array.show_pivot(pivot) + array.message("Pivot at left of partition") + array.wait(1000) + left = first + right = last + while 1: + array.message("Sweep right pointer") + right = right-1 + array.show_right(right) + while right > first and array.compare(right, pivot) >= 0: + right = right-1 + array.show_right(right) + array.message("Sweep left pointer") + left = left+1 + array.show_left(left) + while left < last and array.compare(left, pivot) <= 0: + left = left+1 + array.show_left(left) + if left > right: + array.message("End of partition") + break + array.message("Swap items") + array.swap(left, right) + array.message("Swap pivot back") + array.swap(pivot, right) + n1 = right-first + n2 = last-left + if n1 > 1: stack.append((first, right)) + if n2 > 1: stack.append((left, last)) + array.message("Sorted") + finally: + array.hide_partition() + +def demosort(array): + while 1: + for alg in [quicksort, insertionsort, selectionsort, bubblesort]: + randomize(array) + alg(array) + + +# Sort demo class -- usable as a Grail applet + +class SortDemo: + + def __init__(self, master, size=15): + self.master = master + self.size = size + self.busy = 0 + self.array = Array(self.master) + + self.botframe = Frame(master) + self.botframe.pack(side=BOTTOM) + self.botleftframe = Frame(self.botframe) + self.botleftframe.pack(side=LEFT, fill=Y) + self.botrightframe = Frame(self.botframe) + self.botrightframe.pack(side=RIGHT, fill=Y) + + self.b_qsort = Button(self.botleftframe, + text="Quicksort", command=self.c_qsort) + self.b_qsort.pack(fill=X) + self.b_isort = Button(self.botleftframe, + text="Insertion sort", command=self.c_isort) + self.b_isort.pack(fill=X) + self.b_ssort = Button(self.botleftframe, + text="Selection sort", command=self.c_ssort) + self.b_ssort.pack(fill=X) + self.b_bsort = Button(self.botleftframe, + text="Bubble sort", command=self.c_bsort) + self.b_bsort.pack(fill=X) + + # Terrible hack to overcome limitation of OptionMenu... + class MyIntVar(IntVar): + def __init__(self, master, demo): + self.demo = demo + IntVar.__init__(self, master) + def set(self, value): + IntVar.set(self, value) + if str(value) != '0': + self.demo.resize(value) + + self.v_size = MyIntVar(self.master, self) + self.v_size.set(size) + sizes = [1, 2, 3, 4] + list(range(5, 55, 5)) + if self.size not in sizes: + sizes.append(self.size) + sizes.sort() + self.m_size = OptionMenu(self.botleftframe, self.v_size, *sizes) + self.m_size.pack(fill=X) + + self.v_speed = StringVar(self.master) + self.v_speed.set("normal") + self.m_speed = OptionMenu(self.botleftframe, self.v_speed, + "single-step", "normal", "fast", "fastest") + self.m_speed.pack(fill=X) + + self.b_step = Button(self.botleftframe, + text="Step", command=self.c_step) + self.b_step.pack(fill=X) + + self.b_randomize = Button(self.botrightframe, + text="Randomize", command=self.c_randomize) + self.b_randomize.pack(fill=X) + self.b_uniform = Button(self.botrightframe, + text="Uniform", command=self.c_uniform) + self.b_uniform.pack(fill=X) + self.b_distinct = Button(self.botrightframe, + text="Distinct", command=self.c_distinct) + self.b_distinct.pack(fill=X) + self.b_demo = Button(self.botrightframe, + text="Demo", command=self.c_demo) + self.b_demo.pack(fill=X) + self.b_cancel = Button(self.botrightframe, + text="Cancel", command=self.c_cancel) + self.b_cancel.pack(fill=X) + self.b_cancel.config(state=DISABLED) + self.b_quit = Button(self.botrightframe, + text="Quit", command=self.c_quit) + self.b_quit.pack(fill=X) + + def resize(self, newsize): + if self.busy: + self.master.bell() + return + self.size = newsize + self.array.setdata(range(1, self.size+1)) + + def c_qsort(self): + self.run(quicksort) + + def c_isort(self): + self.run(insertionsort) + + def c_ssort(self): + self.run(selectionsort) + + def c_bsort(self): + self.run(bubblesort) + + def c_demo(self): + self.run(demosort) + + def c_randomize(self): + self.run(randomize) + + def c_uniform(self): + self.run(uniform) + + def c_distinct(self): + self.run(distinct) + + def run(self, func): + if self.busy: + self.master.bell() + return + self.busy = 1 + self.array.setspeed(self.v_speed.get()) + self.b_cancel.config(state=NORMAL) + try: + func(self.array) + except Array.Cancelled: + pass + self.b_cancel.config(state=DISABLED) + self.busy = 0 + + def c_cancel(self): + if not self.busy: + self.master.bell() + return + self.array.cancel() + + def c_step(self): + if not self.busy: + self.master.bell() + return + self.v_speed.set("single-step") + self.array.setspeed("single-step") + self.array.step() + + def c_quit(self): + if self.busy: + self.array.cancel() + self.master.after_idle(self.master.quit) + + +# Main program -- for stand-alone operation outside Grail + +def main(): + root = Tk() + demo = SortDemo(root) + root.protocol('WM_DELETE_WINDOW', demo.c_quit) + root.mainloop() + +if __name__ == '__main__': + main() diff --git a/Tools/demo/ss1.py b/Tools/demo/ss1.py new file mode 100644 index 0000000..8b07489 --- /dev/null +++ b/Tools/demo/ss1.py @@ -0,0 +1,842 @@ +"""SS1 -- a spreadsheet.""" + +import os +import re +import sys +import html +from xml.parsers import expat + +LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT" + +def ljust(x, n): + return x.ljust(n) +def center(x, n): + return x.center(n) +def rjust(x, n): + return x.rjust(n) +align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust} + +align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"} +xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT} + +align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"} + +def sum(seq): + total = 0 + for x in seq: + if x is not None: + total += x + return total + +class Sheet: + + def __init__(self): + self.cells = {} # {(x, y): cell, ...} + self.ns = dict( + cell = self.cellvalue, + cells = self.multicellvalue, + sum = sum, + ) + + def cellvalue(self, x, y): + cell = self.getcell(x, y) + if hasattr(cell, 'recalc'): + return cell.recalc(self.ns) + else: + return cell + + def multicellvalue(self, x1, y1, x2, y2): + if x1 > x2: + x1, x2 = x2, x1 + if y1 > y2: + y1, y2 = y2, y1 + seq = [] + for y in range(y1, y2+1): + for x in range(x1, x2+1): + seq.append(self.cellvalue(x, y)) + return seq + + def getcell(self, x, y): + return self.cells.get((x, y)) + + def setcell(self, x, y, cell): + assert x > 0 and y > 0 + assert isinstance(cell, BaseCell) + self.cells[x, y] = cell + + def clearcell(self, x, y): + try: + del self.cells[x, y] + except KeyError: + pass + + def clearcells(self, x1, y1, x2, y2): + for xy in self.selectcells(x1, y1, x2, y2): + del self.cells[xy] + + def clearrows(self, y1, y2): + self.clearcells(0, y1, sys.maxint, y2) + + def clearcolumns(self, x1, x2): + self.clearcells(x1, 0, x2, sys.maxint) + + def selectcells(self, x1, y1, x2, y2): + if x1 > x2: + x1, x2 = x2, x1 + if y1 > y2: + y1, y2 = y2, y1 + return [(x, y) for x, y in self.cells + if x1 <= x <= x2 and y1 <= y <= y2] + + def movecells(self, x1, y1, x2, y2, dx, dy): + if dx == 0 and dy == 0: + return + if x1 > x2: + x1, x2 = x2, x1 + if y1 > y2: + y1, y2 = y2, y1 + assert x1+dx > 0 and y1+dy > 0 + new = {} + for x, y in self.cells: + cell = self.cells[x, y] + if hasattr(cell, 'renumber'): + cell = cell.renumber(x1, y1, x2, y2, dx, dy) + if x1 <= x <= x2 and y1 <= y <= y2: + x += dx + y += dy + new[x, y] = cell + self.cells = new + + def insertrows(self, y, n): + assert n > 0 + self.movecells(0, y, sys.maxint, sys.maxint, 0, n) + + def deleterows(self, y1, y2): + if y1 > y2: + y1, y2 = y2, y1 + self.clearrows(y1, y2) + self.movecells(0, y2+1, sys.maxint, sys.maxint, 0, y1-y2-1) + + def insertcolumns(self, x, n): + assert n > 0 + self.movecells(x, 0, sys.maxint, sys.maxint, n, 0) + + def deletecolumns(self, x1, x2): + if x1 > x2: + x1, x2 = x2, x1 + self.clearcells(x1, x2) + self.movecells(x2+1, 0, sys.maxint, sys.maxint, x1-x2-1, 0) + + def getsize(self): + maxx = maxy = 0 + for x, y in self.cells: + maxx = max(maxx, x) + maxy = max(maxy, y) + return maxx, maxy + + def reset(self): + for cell in self.cells.values(): + if hasattr(cell, 'reset'): + cell.reset() + + def recalc(self): + self.reset() + for cell in self.cells.values(): + if hasattr(cell, 'recalc'): + cell.recalc(self.ns) + + def display(self): + maxx, maxy = self.getsize() + width, height = maxx+1, maxy+1 + colwidth = [1] * width + full = {} + # Add column heading labels in row 0 + for x in range(1, width): + full[x, 0] = text, alignment = colnum2name(x), RIGHT + colwidth[x] = max(colwidth[x], len(text)) + # Add row labels in column 0 + for y in range(1, height): + full[0, y] = text, alignment = str(y), RIGHT + colwidth[0] = max(colwidth[0], len(text)) + # Add sheet cells in columns with x>0 and y>0 + for (x, y), cell in self.cells.items(): + if x <= 0 or y <= 0: + continue + if hasattr(cell, 'recalc'): + cell.recalc(self.ns) + if hasattr(cell, 'format'): + text, alignment = cell.format() + assert isinstance(text, str) + assert alignment in (LEFT, CENTER, RIGHT) + else: + text = str(cell) + if isinstance(cell, str): + alignment = LEFT + else: + alignment = RIGHT + full[x, y] = (text, alignment) + colwidth[x] = max(colwidth[x], len(text)) + # Calculate the horizontal separator line (dashes and dots) + sep = "" + for x in range(width): + if sep: + sep += "+" + sep += "-"*colwidth[x] + # Now print The full grid + for y in range(height): + line = "" + for x in range(width): + text, alignment = full.get((x, y)) or ("", LEFT) + text = align2action[alignment](text, colwidth[x]) + if line: + line += '|' + line += text + print(line) + if y == 0: + print(sep) + + def xml(self): + out = ['<spreadsheet>'] + for (x, y), cell in self.cells.items(): + if hasattr(cell, 'xml'): + cellxml = cell.xml() + else: + cellxml = '<value>%s</value>' % html.escape(cell) + out.append('<cell row="%s" col="%s">\n %s\n</cell>' % + (y, x, cellxml)) + out.append('</spreadsheet>') + return '\n'.join(out) + + def save(self, filename): + text = self.xml() + f = open(filename, "w") + f.write(text) + if text and not text.endswith('\n'): + f.write('\n') + f.close() + + def load(self, filename): + f = open(filename, 'rb') + SheetParser(self).parsefile(f) + f.close() + +class SheetParser: + + def __init__(self, sheet): + self.sheet = sheet + + def parsefile(self, f): + parser = expat.ParserCreate() + parser.StartElementHandler = self.startelement + parser.EndElementHandler = self.endelement + parser.CharacterDataHandler = self.data + parser.ParseFile(f) + + def startelement(self, tag, attrs): + method = getattr(self, 'start_'+tag, None) + if method: + for key, value in attrs.items(): + attrs[key] = str(value) # XXX Convert Unicode to 8-bit + method(attrs) + self.texts = [] + + def data(self, text): + text = str(text) # XXX Convert Unicode to 8-bit + self.texts.append(text) + + def endelement(self, tag): + method = getattr(self, 'end_'+tag, None) + if method: + method("".join(self.texts)) + + def start_cell(self, attrs): + self.y = int(attrs.get("row")) + self.x = int(attrs.get("col")) + + def start_value(self, attrs): + self.fmt = attrs.get('format') + self.alignment = xml2align.get(attrs.get('align')) + + start_formula = start_value + + def end_int(self, text): + try: + self.value = int(text) + except: + self.value = None + + def end_long(self, text): + try: + self.value = int(text) + except: + self.value = None + + def end_double(self, text): + try: + self.value = float(text) + except: + self.value = None + + def end_complex(self, text): + try: + self.value = complex(text) + except: + self.value = None + + def end_string(self, text): + try: + self.value = text + except: + self.value = None + + def end_value(self, text): + if isinstance(self.value, BaseCell): + self.cell = self.value + elif isinstance(self.value, str): + self.cell = StringCell(self.value, + self.fmt or "%s", + self.alignment or LEFT) + else: + self.cell = NumericCell(self.value, + self.fmt or "%s", + self.alignment or RIGHT) + + def end_formula(self, text): + self.cell = FormulaCell(text, + self.fmt or "%s", + self.alignment or RIGHT) + + def end_cell(self, text): + self.sheet.setcell(self.x, self.y, self.cell) + +class BaseCell: + __init__ = None # Must provide + """Abstract base class for sheet cells. + + Subclasses may but needn't provide the following APIs: + + cell.reset() -- prepare for recalculation + cell.recalc(ns) -> value -- recalculate formula + cell.format() -> (value, alignment) -- return formatted value + cell.xml() -> string -- return XML + """ + +class NumericCell(BaseCell): + + def __init__(self, value, fmt="%s", alignment=RIGHT): + assert isinstance(value, (int, int, float, complex)) + assert alignment in (LEFT, CENTER, RIGHT) + self.value = value + self.fmt = fmt + self.alignment = alignment + + def recalc(self, ns): + return self.value + + def format(self): + try: + text = self.fmt % self.value + except: + text = str(self.value) + return text, self.alignment + + def xml(self): + method = getattr(self, '_xml_' + type(self.value).__name__) + return '<value align="%s" format="%s">%s</value>' % ( + align2xml[self.alignment], + self.fmt, + method()) + + def _xml_int(self): + if -2**31 <= self.value < 2**31: + return '<int>%s</int>' % self.value + else: + return self._xml_long() + + def _xml_long(self): + return '<long>%s</long>' % self.value + + def _xml_float(self): + return '<double>%s</double>' % repr(self.value) + + def _xml_complex(self): + return '<complex>%s</double>' % repr(self.value) + +class StringCell(BaseCell): + + def __init__(self, text, fmt="%s", alignment=LEFT): + assert isinstance(text, (str, str)) + assert alignment in (LEFT, CENTER, RIGHT) + self.text = text + self.fmt = fmt + self.alignment = alignment + + def recalc(self, ns): + return self.text + + def format(self): + return self.text, self.alignment + + def xml(self): + s = '<value align="%s" format="%s"><string>%s</string></value>' + return s % ( + align2xml[self.alignment], + self.fmt, + html.escape(self.text)) + +class FormulaCell(BaseCell): + + def __init__(self, formula, fmt="%s", alignment=RIGHT): + assert alignment in (LEFT, CENTER, RIGHT) + self.formula = formula + self.translated = translate(self.formula) + self.fmt = fmt + self.alignment = alignment + self.reset() + + def reset(self): + self.value = None + + def recalc(self, ns): + if self.value is None: + try: + # A hack to evaluate expressions using true division + self.value = eval(self.translated, ns) + except: + exc = sys.exc_info()[0] + if hasattr(exc, "__name__"): + self.value = exc.__name__ + else: + self.value = str(exc) + return self.value + + def format(self): + try: + text = self.fmt % self.value + except: + text = str(self.value) + return text, self.alignment + + def xml(self): + return '<formula align="%s" format="%s">%s</formula>' % ( + align2xml[self.alignment], + self.fmt, + self.formula) + + def renumber(self, x1, y1, x2, y2, dx, dy): + out = [] + for part in re.split('(\w+)', self.formula): + m = re.match('^([A-Z]+)([1-9][0-9]*)$', part) + if m is not None: + sx, sy = m.groups() + x = colname2num(sx) + y = int(sy) + if x1 <= x <= x2 and y1 <= y <= y2: + part = cellname(x+dx, y+dy) + out.append(part) + return FormulaCell("".join(out), self.fmt, self.alignment) + +def translate(formula): + """Translate a formula containing fancy cell names to valid Python code. + + Examples: + B4 -> cell(2, 4) + B4:Z100 -> cells(2, 4, 26, 100) + """ + out = [] + for part in re.split(r"(\w+(?::\w+)?)", formula): + m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part) + if m is None: + out.append(part) + else: + x1, y1, x2, y2 = m.groups() + x1 = colname2num(x1) + if x2 is None: + s = "cell(%s, %s)" % (x1, y1) + else: + x2 = colname2num(x2) + s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2) + out.append(s) + return "".join(out) + +def cellname(x, y): + "Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')." + assert x > 0 # Column 0 has an empty name, so can't use that + return colnum2name(x) + str(y) + +def colname2num(s): + "Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)." + s = s.upper() + n = 0 + for c in s: + assert 'A' <= c <= 'Z' + n = n*26 + ord(c) - ord('A') + 1 + return n + +def colnum2name(n): + "Translate a column number to name (e.g. 1->'A', etc.)." + assert n > 0 + s = "" + while n: + n, m = divmod(n-1, 26) + s = chr(m+ord('A')) + s + return s + +import tkinter as Tk + +class SheetGUI: + + """Beginnings of a GUI for a spreadsheet. + + TO DO: + - clear multiple cells + - Insert, clear, remove rows or columns + - Show new contents while typing + - Scroll bars + - Grow grid when window is grown + - Proper menus + - Undo, redo + - Cut, copy and paste + - Formatting and alignment + """ + + def __init__(self, filename="sheet1.xml", rows=10, columns=5): + """Constructor. + + Load the sheet from the filename argument. + Set up the Tk widget tree. + """ + # Create and load the sheet + self.filename = filename + self.sheet = Sheet() + if os.path.isfile(filename): + self.sheet.load(filename) + # Calculate the needed grid size + maxx, maxy = self.sheet.getsize() + rows = max(rows, maxy) + columns = max(columns, maxx) + # Create the widgets + self.root = Tk.Tk() + self.root.wm_title("Spreadsheet: %s" % self.filename) + self.beacon = Tk.Label(self.root, text="A1", + font=('helvetica', 16, 'bold')) + self.entry = Tk.Entry(self.root) + self.savebutton = Tk.Button(self.root, text="Save", + command=self.save) + self.cellgrid = Tk.Frame(self.root) + # Configure the widget lay-out + self.cellgrid.pack(side="bottom", expand=1, fill="both") + self.beacon.pack(side="left") + self.savebutton.pack(side="right") + self.entry.pack(side="left", expand=1, fill="x") + # Bind some events + self.entry.bind("<Return>", self.return_event) + self.entry.bind("<Shift-Return>", self.shift_return_event) + self.entry.bind("<Tab>", self.tab_event) + self.entry.bind("<Shift-Tab>", self.shift_tab_event) + self.entry.bind("<Delete>", self.delete_event) + self.entry.bind("<Escape>", self.escape_event) + # Now create the cell grid + self.makegrid(rows, columns) + # Select the top-left cell + self.currentxy = None + self.cornerxy = None + self.setcurrent(1, 1) + # Copy the sheet cells to the GUI cells + self.sync() + + def delete_event(self, event): + if self.cornerxy != self.currentxy and self.cornerxy is not None: + self.sheet.clearcells(*(self.currentxy + self.cornerxy)) + else: + self.sheet.clearcell(*self.currentxy) + self.sync() + self.entry.delete(0, 'end') + return "break" + + def escape_event(self, event): + x, y = self.currentxy + self.load_entry(x, y) + + def load_entry(self, x, y): + cell = self.sheet.getcell(x, y) + if cell is None: + text = "" + elif isinstance(cell, FormulaCell): + text = '=' + cell.formula + else: + text, alignment = cell.format() + self.entry.delete(0, 'end') + self.entry.insert(0, text) + self.entry.selection_range(0, 'end') + + def makegrid(self, rows, columns): + """Helper to create the grid of GUI cells. + + The edge (x==0 or y==0) is filled with labels; the rest is real cells. + """ + self.rows = rows + self.columns = columns + self.gridcells = {} + # Create the top left corner cell (which selects all) + cell = Tk.Label(self.cellgrid, relief='raised') + cell.grid_configure(column=0, row=0, sticky='NSWE') + cell.bind("<ButtonPress-1>", self.selectall) + # Create the top row of labels, and confiure the grid columns + for x in range(1, columns+1): + self.cellgrid.grid_columnconfigure(x, minsize=64) + cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised') + cell.grid_configure(column=x, row=0, sticky='WE') + self.gridcells[x, 0] = cell + cell.__x = x + cell.__y = 0 + cell.bind("<ButtonPress-1>", self.selectcolumn) + cell.bind("<B1-Motion>", self.extendcolumn) + cell.bind("<ButtonRelease-1>", self.extendcolumn) + cell.bind("<Shift-Button-1>", self.extendcolumn) + # Create the leftmost column of labels + for y in range(1, rows+1): + cell = Tk.Label(self.cellgrid, text=str(y), relief='raised') + cell.grid_configure(column=0, row=y, sticky='WE') + self.gridcells[0, y] = cell + cell.__x = 0 + cell.__y = y + cell.bind("<ButtonPress-1>", self.selectrow) + cell.bind("<B1-Motion>", self.extendrow) + cell.bind("<ButtonRelease-1>", self.extendrow) + cell.bind("<Shift-Button-1>", self.extendrow) + # Create the real cells + for x in range(1, columns+1): + for y in range(1, rows+1): + cell = Tk.Label(self.cellgrid, relief='sunken', + bg='white', fg='black') + cell.grid_configure(column=x, row=y, sticky='NSWE') + self.gridcells[x, y] = cell + cell.__x = x + cell.__y = y + # Bind mouse events + cell.bind("<ButtonPress-1>", self.press) + cell.bind("<B1-Motion>", self.motion) + cell.bind("<ButtonRelease-1>", self.release) + cell.bind("<Shift-Button-1>", self.release) + + def selectall(self, event): + self.setcurrent(1, 1) + self.setcorner(sys.maxint, sys.maxint) + + def selectcolumn(self, event): + x, y = self.whichxy(event) + self.setcurrent(x, 1) + self.setcorner(x, sys.maxint) + + def extendcolumn(self, event): + x, y = self.whichxy(event) + if x > 0: + self.setcurrent(self.currentxy[0], 1) + self.setcorner(x, sys.maxint) + + def selectrow(self, event): + x, y = self.whichxy(event) + self.setcurrent(1, y) + self.setcorner(sys.maxint, y) + + def extendrow(self, event): + x, y = self.whichxy(event) + if y > 0: + self.setcurrent(1, self.currentxy[1]) + self.setcorner(sys.maxint, y) + + def press(self, event): + x, y = self.whichxy(event) + if x > 0 and y > 0: + self.setcurrent(x, y) + + def motion(self, event): + x, y = self.whichxy(event) + if x > 0 and y > 0: + self.setcorner(x, y) + + release = motion + + def whichxy(self, event): + w = self.cellgrid.winfo_containing(event.x_root, event.y_root) + if w is not None and isinstance(w, Tk.Label): + try: + return w.__x, w.__y + except AttributeError: + pass + return 0, 0 + + def save(self): + self.sheet.save(self.filename) + + def setcurrent(self, x, y): + "Make (x, y) the current cell." + if self.currentxy is not None: + self.change_cell() + self.clearfocus() + self.beacon['text'] = cellname(x, y) + self.load_entry(x, y) + self.entry.focus_set() + self.currentxy = x, y + self.cornerxy = None + gridcell = self.gridcells.get(self.currentxy) + if gridcell is not None: + gridcell['bg'] = 'yellow' + + def setcorner(self, x, y): + if self.currentxy is None or self.currentxy == (x, y): + self.setcurrent(x, y) + return + self.clearfocus() + self.cornerxy = x, y + x1, y1 = self.currentxy + x2, y2 = self.cornerxy or self.currentxy + if x1 > x2: + x1, x2 = x2, x1 + if y1 > y2: + y1, y2 = y2, y1 + for (x, y), cell in self.gridcells.items(): + if x1 <= x <= x2 and y1 <= y <= y2: + cell['bg'] = 'lightBlue' + gridcell = self.gridcells.get(self.currentxy) + if gridcell is not None: + gridcell['bg'] = 'yellow' + self.setbeacon(x1, y1, x2, y2) + + def setbeacon(self, x1, y1, x2, y2): + if x1 == y1 == 1 and x2 == y2 == sys.maxint: + name = ":" + elif (x1, x2) == (1, sys.maxint): + if y1 == y2: + name = "%d" % y1 + else: + name = "%d:%d" % (y1, y2) + elif (y1, y2) == (1, sys.maxint): + if x1 == x2: + name = "%s" % colnum2name(x1) + else: + name = "%s:%s" % (colnum2name(x1), colnum2name(x2)) + else: + name1 = cellname(*self.currentxy) + name2 = cellname(*self.cornerxy) + name = "%s:%s" % (name1, name2) + self.beacon['text'] = name + + + def clearfocus(self): + if self.currentxy is not None: + x1, y1 = self.currentxy + x2, y2 = self.cornerxy or self.currentxy + if x1 > x2: + x1, x2 = x2, x1 + if y1 > y2: + y1, y2 = y2, y1 + for (x, y), cell in self.gridcells.items(): + if x1 <= x <= x2 and y1 <= y <= y2: + cell['bg'] = 'white' + + def return_event(self, event): + "Callback for the Return key." + self.change_cell() + x, y = self.currentxy + self.setcurrent(x, y+1) + return "break" + + def shift_return_event(self, event): + "Callback for the Return key with Shift modifier." + self.change_cell() + x, y = self.currentxy + self.setcurrent(x, max(1, y-1)) + return "break" + + def tab_event(self, event): + "Callback for the Tab key." + self.change_cell() + x, y = self.currentxy + self.setcurrent(x+1, y) + return "break" + + def shift_tab_event(self, event): + "Callback for the Tab key with Shift modifier." + self.change_cell() + x, y = self.currentxy + self.setcurrent(max(1, x-1), y) + return "break" + + def change_cell(self): + "Set the current cell from the entry widget." + x, y = self.currentxy + text = self.entry.get() + cell = None + if text.startswith('='): + cell = FormulaCell(text[1:]) + else: + for cls in int, int, float, complex: + try: + value = cls(text) + except: + continue + else: + cell = NumericCell(value) + break + if cell is None and text: + cell = StringCell(text) + if cell is None: + self.sheet.clearcell(x, y) + else: + self.sheet.setcell(x, y, cell) + self.sync() + + def sync(self): + "Fill the GUI cells from the sheet cells." + self.sheet.recalc() + for (x, y), gridcell in self.gridcells.items(): + if x == 0 or y == 0: + continue + cell = self.sheet.getcell(x, y) + if cell is None: + gridcell['text'] = "" + else: + if hasattr(cell, 'format'): + text, alignment = cell.format() + else: + text, alignment = str(cell), LEFT + gridcell['text'] = text + gridcell['anchor'] = align2anchor[alignment] + + +def test_basic(): + "Basic non-gui self-test." + import os + a = Sheet() + for x in range(1, 11): + for y in range(1, 11): + if x == 1: + cell = NumericCell(y) + elif y == 1: + cell = NumericCell(x) + else: + c1 = cellname(x, 1) + c2 = cellname(1, y) + formula = "%s*%s" % (c1, c2) + cell = FormulaCell(formula) + a.setcell(x, y, cell) +## if os.path.isfile("sheet1.xml"): +## print "Loading from sheet1.xml" +## a.load("sheet1.xml") + a.display() + a.save("sheet1.xml") + +def test_gui(): + "GUI test." + if sys.argv[1:]: + filename = sys.argv[1] + else: + filename = "sheet1.xml" + g = SheetGUI(filename) + g.root.mainloop() + +if __name__ == '__main__': + #test_basic() + test_gui() |