Auto-completion
05 Oct 2008I’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()