Auto-completion

I’ve recently started a little project called ‘completion’. It’s purpose is to allow auto-completion of user input, according to a grammar. It’s readily understandable, if you already use Pyparsing. I’d like to merge this functionality into Pyparsing at some point.

Here’s the alpha code, with explanatory doctests:

#!/usr/bin/python
"""
Completion is a module for performing autocompletion of text according to a 
programatically expressed grammar.
 
If you don't get any of that ;-) have a look at the doctests below...
 
>>> foo = Literal('foo')
>>> print foo.parse('f')
ParseResult(False, input=None, completions=['oo'], message=None)
 
>>> foo = Literal('foo')
>>> print foo.parse('ba')
ParseResult(False, input=None, completions=[], message=None)
 
>>> foobar = Literal('foobar')
>>> food = Literal('food')
>>> foobar_or_food = Or(foobar, food)
>>> print foobar_or_food.parse('f')
ParseResult(False, input=None, completions=['oobar', 'ood'], message=None)
>>> print foobar_or_food.parse('g')
ParseResult(False, input=None, completions=[], message=None)
>>> print foobar_or_food.parse('food')
ParseResult(True, input=, completions=[], message=None)
>>> print foobar_or_food.parse('foo')
ParseResult(False, input=None, completions=['bar', 'd'], message=None)
 
>>> foobar_then_food = Then(foobar, WhiteSpace(), food)
>>> print foobar_then_food.parse('fo')
ParseResult(False, input=None, completions=['obar'], message=None)
>>> print foobar_then_food.parse('')
ParseResult(False, input=None, completions=['foobar'], message=None)
>>> print foobar_then_food.parse('foobar f')
ParseResult(False, input=None, completions=['ood'], message=None)
>>> print foobar_then_food.parse('foobar')
ParseResult(False, input=, completions=[' ', '\\t'], message=None)
 
>>> optional_foo = Occurances(foo, min=0, max=1)
>>> optional_foo.parse("")
ParseResult(True, input=None, completions=[], message=None)
>>> optional_foo.parse("fo")
ParseResult(True, input=None, completions=[], message=None)
 
>>> three_or_four_foos = Occurances(foo, min=3, max=4)
>>> three_or_four_foos.parse("")
ParseResult(False, input=None, completions=['foo'], message=None)
>>> three_or_four_foos.parse("foof")
ParseResult(False, input=None, completions=['oo'], message=None)
 
>>> name = Alphas()
>>> name.parse("")
ParseResult(False, input=, completions=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'], message=None)
>>> name.parse("abc")
ParseResult(True, input=, completions=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'], message=None)
 
>>> bar = Literal('bar')
>>> complex = Then(name, WhiteSpace(), bar)
>>> complex.parse('f')
ParseResult(False, input=, completions=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '\\t'], message=None)
>>> complex = Or(Or(foo, food, foobar), Then(name, WhiteSpace(), bar))
>>> complex.parse('f')
ParseResult(False, input=None, completions=['oo', 'ood', 'oobar', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '\\t'], message=None)
 
"""
 
import sys
 
class ParseResult:
    def __init__(self, success, input=None, completions=[], message=None):
        self.success = success
        self.input = input
        self.completions = completions
        self.message = message
 
    def __repr__(self):
        return "ParseResult(%s, input=%s, completions=%s, message=%s)" % (
            str(self.success),
            str(self.input),
            str(self.completions),
            str(self.message),
        )
 
class Parser:
    def parse(self, input):
        pass
 
class Literal(Parser):
    def __init__(self, text):
        self.text = text
 
    def parse(self, input):
        if input.startswith(self.text):
            return ParseResult(True, input[len(self.text):])
        # if the input isn't at the beginning of self.text, then this 
        # cannot be autocompleted
        if not self.text.startswith(input):
            return ParseResult(False)
        return ParseResult(False, completions=[self.text[len(input):]])
 
class Occurances(Parser):
    def __init__(self, parser, min=0, max=1):
        self.parser = parser
        if min < 0:
            raise Exception("Min must not be negative.")
        if max < min:
            raise Exception("Max must not be less than min.")
        if max < 1:
            raise Exception("Max must be more than zero.")
        self.min = min
        self.max = max
 
    def parse(self, input):
        # see how many times parser can be applied, up to max
        for i in range(self.max):
            parse_result = self.parser.parse(input)
            input = parse_result.input
            if not parse_result.success:
                break
        #print i, self.min
        if i < self.min and self.min > 0:
            return parse_result
        return ParseResult(True, input)
 
 
class Chars(Parser):
    def __init__(self, chars):
        self.chars = chars
 
    def parse(self, input):
        if len(input) == 0:
            return ParseResult(False, input, list(self.chars))
        if input[0] not in self.chars:
            return ParseResult(False, input)
        while len(input) > 0 and input[0] in self.chars:
            input = input[1:]
        if len(input) == 0:
            return ParseResult(True, input, list(self.chars))
        return ParseResult(True, input)
 
class WhiteSpace(Chars):
    def __init__(self):
        self.chars = ' \t'
 
class Alphas(Chars):
    def __init__(self):
        self.chars = 'abcdefghijklmnopqrstuvwxyz'
 
class Alphanums(Chars):
    def __init__(self):
        self.chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
 
class Then(Parser):
    def __init__(self, *parsers):
        self.parsers = parsers
 
    def parse(self, input):
        # try all the parsers in order, until one fails
        completions = []
        for parser in self.parsers:
            parse_result = parser.parse(input)
            completions += parse_result.completions
            if not parse_result.success:
                # add the completions for successful parsings which can take more
                parse_result.completions = completions
                return parse_result
            input = parse_result.input
        return ParseResult(True, input)
 
class Or(Parser):
    def __init__(self, *parsers):
        self.parsers = parsers
 
    def parse(self, input):
        # try all the parsers and see whether any work
        parse_results = map(lambda x: x.parse(input), self.parsers)
 
        # were any successful?
        successful_result = None
        completions = []
        for parse_result in parse_results:
            if parse_result.success and not successful_result:
                # first matched
                successful_result = parse_result
            completions += parse_result.completions
 
        if successful_result:
            return successful_result
 
        # otherwise return all the completions
        return ParseResult(False, None, completions) 
 
 
 
def _test():
    """Test with doctest."""
    import doctest
    doctest.testmod()
 
 
if __name__ == "__main__":
    _test()