Commits

Friedrich Weber committed 2b9b6a5

Initial import. It works.

Comments (0)

Files changed (12)

+jambalah/command.py: command.g
+	python yapps2/yapps2.py command.g jambalah/command.py
+
+%%
+
+parser Command:
+    ignore: "\\s+"
+    token END: "$"
+    token SYMBOL: "[a-zA-Z0-9_.:#/][a-zA-Z0-9_.:#/-]*"
+    token HYPHEN: "-"
+    token DHYPHEN: "--"
+    token GT: ">"
+
+    rule goal: subject [parameters<<[], {}>>] [additional<<{}>>] END
+                {{ try: parameters }}
+                {{ except NameError: parameters = [], {} # WAH! That is UGLY!! }}
+                {{ try: additional }}
+                {{ except NameError: additional = {} # That, too. }}
+                {{ return (subject, parameters[0], parameters[1], additional) }}
+
+    rule parameters<<positional, keywords>>: (parameter<<positional, keywords>>)+ {{ return positional, keywords}}
+    rule parameter<<positional, keywords>>: SYMBOL {{ positional.append(SYMBOL) }} # positional
+                                          | DHYPHEN SYMBOL ["="] value {{ keywords[SYMBOL] = value }} # keyword
+                                          | HYPHEN SYMBOL {{ keywords[SYMBOL] = True }} # trigger  
+    rule subject: SYMBOL {{ return SYMBOL }}
+    rule additional<<d>>: GT SYMBOL {{ d[GT] = SYMBOL; return d }}
+    rule value: SYMBOL {{ return SYMBOL }}
+
+    # no quoted strings atm. sorry.
+
+%%
+
+def parse_line(text):
+    return parse("goal", text)
+
+from jambalah.jam import Jam
+
+jam = Jam.create_all()
+jam.run()

jambalah/__init__.py

Empty file added.
+from cmd import Cmd
+from traceback import print_exc
+from textwrap import dedent
+
+from .command import parse_line
+
+class CLISyntaxError(Exception):
+    pass
+
+class CLIError(Exception):
+    pass
+
+class CLI(Cmd):
+    prompt = 'jambalah> '
+
+    def __init__(self, jam):
+        Cmd.__init__(self)
+        self.jam = jam
+
+    def emptyline(self):
+        """
+            empty line? do nothing.
+        """
+        return
+
+    def onecmd(self, line):
+        """
+            replacement for the original `onecmd` that uses the
+            yapps2-generated parser in :mod:`jambalah.command`.
+        """
+        if not line:
+            return self.emptyline()        
+        cmd, positional, keywords, additional = parse_line(line)
+        self.lastcmd = line
+        try:
+            func = self.jam.bindings[cmd] 
+        except KeyError:
+            try:
+                func = getattr(self, 'do_%s' % cmd)
+            except AttributeError:
+                return self.default(line)
+        return func(positional, keywords, additional)
+
+    def get_names(self):
+        return self.jam.bindinges.iterkeys()
+
+    def immortal_loop(self):
+        """
+            a loop that catches all exceptions and prints them.
+            (except KeyboardInterrupt and SystemExit)
+        """
+        while True:
+            try:
+                self.cmdloop()
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except:
+                print_exc()
+
+    def completenames(self, text, *ignored):
+        return [name for name in self.jam.bindings if name.startswith(text)]
+
+    def do_help(self, positional, keywords, additional):
+        arg = ' '.join(positional)
+        if arg:
+            # XXX check arg syntax
+            try:
+                func = self.jam.help_bindings[positional[0]] 
+            except KeyError:
+                try:
+                    try:
+                        doc = self.jam.bindings[positional[0]].__doc__
+                    except KeyError:
+                        doc = getattr(self, 'do_%s' % arg).__doc__
+                    if doc:
+                        self.stdout.write("%s\n" % dedent(str(doc)))
+                        return
+                except AttributeError:
+                    pass
+                self.stdout.write("%s\n"%str(self.nohelp % (arg,)))
+                return
+            func()
+        else:
+            names = self.get_names()
+            cmds_doc = []
+            cmds_undoc = []
+            help = {}
+            for name in names:
+                if name[:5] == 'help_':
+                    help[name[5:]]=1
+            names.sort()
+            # There can be duplicates if routines overridden
+            prevname = ''
+            for name in names:
+                if name[:3] == 'do_':
+                    if name == prevname:
+                        continue
+                    prevname = name
+                    cmd=name[3:]
+                    if cmd in help:
+                        cmds_doc.append(cmd)
+                        del help[cmd]
+                    elif getattr(self, name).__doc__:
+                        cmds_doc.append(cmd)
+                    else:
+                        cmds_undoc.append(cmd)
+            self.stdout.write("%s\n"%str(self.doc_leader))
+            self.print_topics(self.doc_header,   cmds_doc,   15,80)
+            self.print_topics(self.misc_header,  help.keys(),15,80)
+            self.print_topics(self.undoc_header, cmds_undoc, 15,80)
+
+

jambalah/command.py

+
+
+from string import *
+import re
+from yappsrt import *
+
+class CommandScanner(Scanner):
+    patterns = [
+        ('"="', re.compile('=')),
+        ('\\s+', re.compile('\\s+')),
+        ('END', re.compile('$')),
+        ('SYMBOL', re.compile('[a-zA-Z0-9_.:#/][a-zA-Z0-9_.:#/-]*')),
+        ('HYPHEN', re.compile('-')),
+        ('DHYPHEN', re.compile('--')),
+        ('GT', re.compile('>')),
+    ]
+    def __init__(self, str):
+        Scanner.__init__(self,None,['\\s+'],str)
+
+class Command(Parser):
+    def goal(self):
+        subject = self.subject()
+        if self._peek('END', 'GT', 'SYMBOL', 'DHYPHEN', 'HYPHEN') not in ['END', 'GT']:
+            parameters = self.parameters([], {})
+        if self._peek('END', 'GT') == 'GT':
+            additional = self.additional({})
+        END = self._scan('END')
+        try: parameters
+        except NameError: parameters = [], {} # WAH! That is UGLY!!
+        try: additional
+        except NameError: additional = {} # That, too.
+        return (subject, parameters[0], parameters[1], additional)
+
+    def parameters(self, positional, keywords):
+        while 1:
+            parameter = self.parameter(positional, keywords)
+            if self._peek('SYMBOL', 'DHYPHEN', 'HYPHEN', 'END', 'GT') not in ['SYMBOL', 'DHYPHEN', 'HYPHEN']: break
+        return positional, keywords
+
+    def parameter(self, positional, keywords):
+        _token_ = self._peek('SYMBOL', 'DHYPHEN', 'HYPHEN')
+        if _token_ == 'SYMBOL':
+            SYMBOL = self._scan('SYMBOL')
+            positional.append(SYMBOL)
+        elif _token_ == 'DHYPHEN':
+            DHYPHEN = self._scan('DHYPHEN')
+            SYMBOL = self._scan('SYMBOL')
+            if self._peek('"="', 'SYMBOL') == '"="':
+                self._scan('"="')
+            value = self.value()
+            keywords[SYMBOL] = value
+        else:# == 'HYPHEN'
+            HYPHEN = self._scan('HYPHEN')
+            SYMBOL = self._scan('SYMBOL')
+            keywords[SYMBOL] = True
+
+    def subject(self):
+        SYMBOL = self._scan('SYMBOL')
+        return SYMBOL
+
+    def additional(self, d):
+        GT = self._scan('GT')
+        SYMBOL = self._scan('SYMBOL')
+        d[GT] = SYMBOL; return d
+
+    def value(self):
+        SYMBOL = self._scan('SYMBOL')
+        return SYMBOL
+
+
+def parse(rule, text):
+    P = Command(CommandScanner(text))
+    return wrap_error_reporter(P, rule)
+
+
+
+
+def parse_line(text):
+    return parse("goal", text)
+
+import os
+import sys
+from thread import start_new_thread
+
+from .liblet import AtomicLiblet
+from .meta import MetaLiblet
+from .player import Player
+from .cli import CLI, CLISyntaxError, CLIError
+
+class Jam(object):
+    supported_extensions = ('.mp3', '.ogg', '.flac', '.oga')
+
+    def __init__(self):
+        # the root liblet. HAS to be set.
+        self.root = None
+        # current liblet. ALSO has to be set.
+        self.liblet = None
+        # cli object. GUESS what.
+        self.cli = None
+        # the playlists {name : [url1, url2, ...] }
+        self.playlists = {'jam':[]}
+        # the current playlist name
+        self.current_playlist = 'jam'
+        # the player
+        self.player = Player()
+        # bindings of command -> function
+        # the function is required to have the following signature:
+        #   def func(positional: list, keywords: dict, additional: dict)
+        # its docstring is used as documentation if there is no
+        # help function given.
+        self.bindings = {}
+        # bindings of command -> help function
+        #   def help_func(line)
+        self.help_bindings = {}
+        # add some bindings.
+        self.add_default_bindings()
+
+    @classmethod
+    def create_all(cls):
+        jam = cls()
+        jam.root = jam.liblet = MetaLiblet(jam)
+        jam.cli = CLI(jam)
+        return jam
+
+    def run(self):
+        start_new_thread(self.player.run, ())
+        self.cli.immortal_loop()
+
+    def quit(self):
+        self.player.quit()
+        sys.exit(0)
+
+    def parse_path(self, path):
+        """
+            parse the unix-like path *path* and return the matching
+            liblet (or None)
+        """
+        # split the path into components and throw away empty strings
+        # (that's done using `bool`, because 
+        # bool('') -> False, bool('a') -> True)
+        splitted = filter(bool, path.split(os.path.sep))
+        # get the root node we'll use (root for absolute paths)
+        if os.path.isabs(path):
+            node = self.root
+        else:
+            node = self.liblet
+        # search them.
+        while (splitted and node is not None):
+            next = splitted.pop(0)
+            # .. is the parent directory.
+            if next == '..':
+                node = node.parent
+            # . is the current directory (yep, makes no sense)
+            elif next == '.':
+                continue
+            # is a real component. get the liblet's child.
+            else:
+                node = node.get_child(next)
+        if node is None:
+            raise CLIError('You are going beyond the top-level node.') # TODO: just ignore this?
+        return node
+
+    def do_move(self, positional, keywords, additional):
+        """
+            `move FILENAME|TRACK FILENAME|TRACK ... [>PLAYLIST]`
+
+            If PLAYLIST is not specified, the 'jam' playlist is used.
+            If the playlist does not exist yet, it is created.
+            At the moment, only top-level atomic liblets are moved
+            to the playlist. No folder moving.
+
+            :todo: change that.
+        """
+        playlist = additional.get('>', 'jam')
+        if not positional:
+            raise CLISyntaxError(
+                    'You have to specify at least one filename or track')
+        # list of atomic liblets to add
+        liblets = []  
+        for path in positional:
+            liblet = self.parse_path(path)
+            # only move atomic (top-level) liblets for now.
+            if isinstance(liblet, AtomicLiblet):
+                liblets.append(liblet)
+        # add their urls to the playlist1
+        self.add_to_playlist(playlist, (liblet.url for liblet in liblets))
+
+    def do_quit(self, positional, keywords, additional):
+        """
+            `quit`
+
+            quits the application immediately.
+        """
+        self.quit()
+
+    def do_cd(self, positional, keywords, additional):
+        """
+            `cd LIBLET`
+        """
+        dest = positional[0]
+        self.liblet = self.parse_path(dest)
+        self.update_prompt()
+
+    def do_ls(self, positional, keywords, additional):
+        """
+            `ls`
+
+            :todo: support `ls foo/`
+        """
+        if len(positional) > 0:
+            dest = positional[0]
+            liblet = self.parse_path(dest)
+        else:
+            liblet = self.liblet
+        self.cli.columnize(list(liblet.get_children_names()))
+
+    def do_play(self, positional, keywords, additional):
+        """
+            `play [PLAYLIST]`
+
+            if PLAYLIST is not specified, play the 'jam' playlist
+        """
+        playlist = positional[0] if len(positional) > 0 else 'jam'
+        self.play_playlist(playlist)
+
+    def do_next(self, positional, keywords, additional):
+        """
+            `next`
+
+            plays the next item in the playlist (if any)
+        """
+        self.player.next()
+
+    def do_stop(self, positional, keywords, additional):
+        """
+            `stop`
+
+            stop the current playlist
+        """
+        self.player.stop()
+
+    def do_playlists(self, positional, keywords, additional):
+        """
+            `playlists`
+
+            print all playlists, mark the current playlist with an asterisk.
+        """
+        for name, playlist in self.playlists.iteritems():
+            if name == self.current_playlist:
+                print '*',
+            else:
+                print '-',
+            print name
+            for item in playlist:
+                print '  - %s' % repr(item)
+
+    def update_prompt(self):
+        """
+            sets the cli's prompt to the current liblet's full name
+        """
+        self.cli.prompt = 'jambalah %s> ' % self.liblet.full_name
+
+    def add_default_bindings(self):
+        """
+            add all default bindings
+
+            Currently:
+            
+             * quit
+             * cd
+             * ls
+             * move
+             * play
+             * next
+             * stop
+             * playlists
+
+        """
+        bindings = [('quit', self.do_quit),
+                    ('cd', self.do_cd),
+                    ('ls', self.do_ls),
+                    ('move', self.do_move),
+                    ('play', self.do_play),
+                    ('next', self.do_next),
+                    ('stop', self.do_stop),
+                    ('playlists', self.do_playlists),
+                    ]
+        for command, func in bindings:
+            self.add_binding(command, func)
+
+    def add_binding(self, command, func, help_func=None):
+        """
+            bind the command *cmd* to the function *func*.
+            :todo: doc
+        """
+        self.bindings[command] = func
+        if help_func is not None:
+            self.help_bindings[command] = help_func
+
+    def add_to_playlist(self, playlist, urls):
+        self.playlists.setdefault(playlist, []).extend(urls)
+    
+    def play_playlist(self, playlist):
+        assert playlist in self.playlists
+        self.current_playlist = playlist
+        self.player.play_iterator(iter(self.playlists[playlist]))
+

jambalah/liblet.py

+import os
+
+class UnknownLiblet(Exception):
+    pass
+
+class Liblet(object):
+    def __init__(self, jam, parent=None):
+        self.jam = jam
+        self.parent = parent
+
+    @property
+    def name(self):
+        raise NotImplementedError()
+
+    @property
+    def full_name(self):
+        name = ''
+        liblet = self
+        while liblet is not None:
+            name = os.path.join(liblet.name, name)
+            liblet = liblet.parent
+        return name
+
+    def get_children_names(self):
+        """
+            return a list of children liblet names.
+        """
+        raise NotImplementedError()
+
+    def get_child_by_name(self, name):
+        """
+            get a child liblet by its name, raises `UnknownLiblet`
+            if there is no matching liblet.
+        """
+        raise NotImplementedError()
+
+    def get_child_by_no(self, no):
+        """
+            get a child by its number, raises `UnknownLiblet`
+            if there is no matching liblet.
+            `no` starts at 1. It will most likely only return
+            atomic liblets.
+        """
+        raise NotImplementedError()
+
+    def get_child(self, stuff):
+        """
+            get a child. If `stuff` starts with a '#', it is
+            interpreted as a number, and :meth:`get_child_by_no`
+            is invoked. Otherwise, :meth:`get_child_by_name`
+            is used.
+        """
+        if stuff.startswith('#'):
+            return self.get_child_by_no(int(stuff[1:]))
+        else:
+            return self.get_child_by_name(stuff)
+
+class AtomicLiblet(Liblet):
+    def __init__(self, jam, parent, url):
+        Liblet.__init__(self, jam, parent)
+        self.url = url
+
+    @property
+    def name(self):
+        return self.url # TODO
+
+    def get_children_names(self):
+        return []
+
+    def get_child_by_name(self, name):
+        raise UnknownLiblet(name)
+
+    def get_child_by_no(self, no):
+        raise UnknownLiblet(no)
+
+def make_named_liblet_cls(basecls, clsname, name):
+    class _Liblet(basecls):
+        @property
+        def name(self):
+            return name
+    _Liblet.__name__ = clsname
+    return _Liblet
+
+def make_named_liblet(basecls, name, *args, **kwargs):
+    return make_named_liblet_cls(basecls, '%sLiblet' % name, name)(*args, **kwargs)

jambalah/local.py

+import os
+from .liblet import Liblet, AtomicLiblet
+
+def filter_extensions(contents, extensions):
+    return (item for item in contents
+            if os.path.splitext(item)[1] in extensions)
+
+class LocalLiblet(Liblet):
+    def __init__(self, jam, parent, path):
+        Liblet.__init__(self, jam, parent)
+        self.path = path
+
+    @property
+    def name(self):
+        return os.path.split(self.path)[-1]
+
+    def get_children_names(self):
+        return (item for item in os.listdir(self.path)
+                if os.path.splitext(item)[1] in self.jam.supported_extensions
+                or os.path.isdir(os.path.join(self.path, item))
+                )
+
+    def get_child_by_name(self, name):
+        child_path = os.path.join(self.path, name)
+        return self._get_child_by_path(child_path)
+
+    def get_child_by_no(self, no):
+        path = self._get_path_by_no(no)
+        # only files (atomic liblets)
+        assert os.path.isfile(path)
+        return AtomicLiblet(self.jam, self, path)
+
+    def _get_child_by_path(self, child_path):
+        assert os.path.exists(child_path)
+        # if it's a directory, return another LocalLiblet.
+        if os.path.isdir(child_path):
+            return LocalLiblet(self.jam, self, child_path)
+        else: # if it's a file, return an AtomicLiblet.
+            return AtomicLiblet(self.jam, self, child_path)
+
+    def _get_path_by_no(self, no):
+        files = filter_extensions(os.listdir(self.path),
+                self.jam.supported_extensions)
+        return os.path.join(self.path, sorted(files)[no - 1])
+
+from .liblet import Liblet, UnknownLiblet, make_named_liblet
+from .local import LocalLiblet
+
+class MetaLiblet(Liblet):
+    def __init__(self, jam):
+        Liblet.__init__(self, jam, None)
+        self.liblets = {'local': 
+                make_named_liblet(LocalLiblet, 'local', self.jam, self, '/')
+                }
+
+    @property
+    def name(self):
+        return '/'
+
+    def get_children_names(self):
+        return self.liblets.iterkeys()
+
+    def get_child_by_name(self, name):
+        try:
+            return self.liblets[name]
+        except KeyError:
+            raise UnknownLiblet(name)
+
+    def get_child_by_no(self, no):
+        """
+            getting by number not supported (yet?)
+        """
+        raise UnknownLiblet(no)

jambalah/player.py

+from urlparse import urlparse
+
+import gst
+import gobject
+
+def translate_url(url):
+    """
+        translate *url* so that gstreamer can read it.
+        Convert absolute, local paths to a file:// uri,
+        keep other urls.
+    """
+    parsed = urlparse(url)
+    # no scheme provided -> most likely a local path.
+    if not parsed.scheme:
+        return 'file://%s' % url
+    # there is a scheme. don't change anything.
+    else:
+        return url
+
+class Player(object):
+    def __init__(self):
+        # create a playbin and connect its signals
+        self.playbin = gst.element_factory_make("playbin", "jam-playbin")
+        bus = self.playbin.get_bus()
+        bus.add_signal_watch()
+        bus.connect('message', self.on_message)
+        # and now some variables:
+        # an iterator that yields url strings, representing the current queue.
+        self.iterator = None
+        self.running = True
+
+    def run(self):
+        loop = gobject.MainLoop()
+        gobject.threads_init()
+        context = loop.get_context()
+        while self.running:
+            context.iteration(True)
+
+    def quit(self):
+        self.running = False
+     
+    def on_message(self, bus, message):
+        """
+            message handler. will play the next file at the end of the
+            stream
+        """
+        type_ = message.type
+        if type_ == gst.MESSAGE_EOS:
+            self.next()
+        elif type_ == gst.MESSAGE_ERROR:
+            self.stop()
+            err, debug = message.parse_error()
+            print 'Error while playing: %s' % err, debug
+            # just continue on error?
+            self.next()
+
+    def next(self):
+        """
+            fetches the next url from `self.iterator` and plays it.
+
+            If there are no more urls, just be quiet and set `self.iterator`
+            to None.
+        """
+        if self.iterator is None:
+            print 'Please specify a playlist.' # TODO: raise exception
+            return
+        try:
+            url = self.iterator.next()
+        except StopIteration:
+            self.stop()
+            self.iterator = None
+        else:
+            self.play_now(url)
+
+    def stop(self):
+        """
+            play nothing, but keep the playlist.
+        """
+        self.playbin.set_state(gst.STATE_NULL)
+
+    def play(self):
+        """
+            play the playlist.
+        """
+        self.playbin.set_state(gst.STATE_PLAYING)
+
+    def play_iterator(self, iterator):
+        """
+            *iterator* is yielding urls. play all these.
+        """
+        self.iterator = iterator
+        self.next()
+
+    def play_now(self, url):
+        """
+            Play the `url`. NOW.
+
+            :note: Don't use this if not needed. Use `play_iterator`.
+        """
+        self.stop()
+        self.playbin.set_property("uri", translate_url(url))
+        self.play()

jambalah/yappsrt.py

+# Yapps 2.0 Runtime
+#
+# This module is needed to run generated parsers.
+
+from string import join, count, find, rfind
+import re
+
+class SyntaxError(Exception):
+    """When we run into an unexpected token, this is the exception to use"""
+    def __init__(self, pos=-1, msg="Bad Token"):
+        Exception.__init__(self)
+	self.pos = pos
+	self.msg = msg
+    def __repr__(self):
+	if self.pos < 0: return "#<syntax-error>"
+	else: return "SyntaxError[@ char %s: %s]" % (repr(self.pos), self.msg)
+
+class NoMoreTokens(Exception):
+    """Another exception object, for when we run out of tokens"""
+    pass
+
+class Scanner:
+    def __init__(self, patterns, ignore, input):
+	"""Patterns is [(terminal,regex)...]
+        Ignore is [terminal,...];
+	Input is a string"""
+	self.tokens = []
+	self.restrictions = []
+	self.input = input
+	self.pos = 0
+	self.ignore = ignore
+	# The stored patterns are a pair (compiled regex,source
+	# regex).  If the patterns variable passed in to the
+	# constructor is None, we assume that the class already has a
+	# proper .patterns list constructed
+        if patterns is not None:
+            self.patterns = []
+            for k, r in patterns:
+                self.patterns.append( (k, re.compile(r)) )
+
+    def token(self, i, restrict=0):
+	"""Get the i'th token, and if i is one past the end, then scan
+	for another token; restrict is a list of tokens that
+	are allowed, or 0 for any token."""
+	if i == len(self.tokens): self.scan(restrict)
+	if i < len(self.tokens):
+	    # Make sure the restriction is more restricted
+	    if restrict and self.restrictions[i]:
+		for r in restrict:
+		    if r not in self.restrictions[i]:
+			raise NotImplementedError("Unimplemented: restriction set changed")
+	    return self.tokens[i]
+	raise NoMoreTokens()
+
+    def __repr__(self):
+	"""Print the last 10 tokens that have been scanned in"""
+	output = ''
+	for t in self.tokens[-10:]:
+	    output = '%s\n  (@%s)  %s  =  %s' % (output,t[0],t[2],repr(t[3]))
+	return output
+
+    def scan(self, restrict):
+	"""Should scan another token and add it to the list, self.tokens,
+	and add the restriction to self.restrictions"""
+	# Keep looking for a token, ignoring any in self.ignore
+	while 1:
+	    # Search the patterns for the longest match, with earlier
+	    # tokens in the list having preference
+	    best_match = -1
+	    best_pat = '(error)'
+	    for p, regexp in self.patterns:
+		# First check to see if we're ignoring this token
+		if restrict and p not in restrict and p not in self.ignore:
+		    continue
+		m = regexp.match(self.input, self.pos)
+		if m and len(m.group(0)) > best_match:
+		    # We got a match that's better than the previous one
+		    best_pat = p
+		    best_match = len(m.group(0))
+
+	    # If we didn't find anything, raise an error
+	    if best_pat == '(error)' and best_match < 0:
+		msg = "Bad Token"
+		if restrict:
+		    msg = "Trying to find one of "+join(restrict,", ")
+		raise SyntaxError(self.pos, msg)
+
+	    # If we found something that isn't to be ignored, return it
+	    if best_pat not in self.ignore:
+		# Create a token with this data
+		token = (self.pos, self.pos+best_match, best_pat,
+			 self.input[self.pos:self.pos+best_match])
+		self.pos = self.pos + best_match
+		# Only add this token if it's not in the list
+		# (to prevent looping)
+		if not self.tokens or token != self.tokens[-1]:
+		    self.tokens.append(token)
+		    self.restrictions.append(restrict)
+		return
+	    else:
+		# This token should be ignored ..
+		self.pos = self.pos + best_match
+
+class Parser:
+    def __init__(self, scanner):
+        self._scanner = scanner
+        self._pos = 0
+
+    def _peek(self, *types):
+        """Returns the token type for lookahead; if there are any args
+        then the list of args is the set of token types to allow"""
+        tok = self._scanner.token(self._pos, types)
+        return tok[2]
+
+    def _scan(self, type):
+        """Returns the matched text, and moves to the next token"""
+        tok = self._scanner.token(self._pos, [type])
+        if tok[2] != type:
+            raise SyntaxError(tok[0], 'Trying to find '+type)
+        self._pos = 1+self._pos
+        return tok[3]
+
+
+
+def print_error(input, err, scanner):
+    """This is a really dumb long function to print error messages nicely."""
+    p = err.pos
+    # Figure out the line number
+    line = count(input[:p], '\n')
+    print err.msg+" on line "+repr(line+1)+":"
+    # Now try printing part of the line
+    text = input[max(p-80, 0):p+80]
+    p = p - max(p-80, 0)
+
+    # Strip to the left
+    i = rfind(text[:p], '\n')
+    j = rfind(text[:p], '\r')
+    if i < 0 or (0 <= j < i): i = j
+    if 0 <= i < p:
+	p = p - i - 1
+	text = text[i+1:]
+
+    # Strip to the right
+    i = find(text,'\n', p)
+    j = find(text,'\r', p)
+    if i < 0 or (0 <= j < i): i = j
+    if i >= 0:
+	text = text[:i]
+
+    # Now shorten the text
+    while len(text) > 70 and p > 60:
+	# Cut off 10 chars
+	text = "..." + text[10:]
+	p = p - 7
+
+    # Now print the string, along with an indicator
+    print '> ',text
+    print '> ',' '*p + '^'
+    print 'List of nearby tokens:', scanner
+
+def wrap_error_reporter(parser, rule):
+    try:
+        return_value = getattr(parser, rule)()
+    except SyntaxError, s:
+        input = parser._scanner.input
+        try:
+            print_error(input, s, parser._scanner)
+        except ImportError:
+            print 'Syntax Error',s.msg,'on line',1+count(input[:s.pos], '\n')
+        raise
+    except NoMoreTokens:
+        print 'Could not complete parsing; stopped around here:'
+        print parser._scanner
+        raise
+    return return_value