diff -r c6f47e635845 -r c0239d8ae742 toolsutils.py --- a/toolsutils.py Tue May 06 15:59:39 2014 +0200 +++ b/toolsutils.py Mon May 12 12:37:19 2014 +0200 @@ -25,7 +25,12 @@ import subprocess from os import listdir, makedirs, environ, chmod, walk, remove from os.path import exists, join, abspath, normpath - +import re +from rlcompleter import Completer +try: + import readline +except ImportError: # readline not available, no completion + pass try: from os import symlink except ImportError: @@ -264,3 +269,155 @@ password = getpass('password: ') return connect(login=user, password=password, host=optconfig.host, database=appid) + +## cwshell helpers ############################################################# + +class AbstractMatcher(object): + """Abstract class for CWShellCompleter's matchers. + + A matcher should implement a ``possible_matches`` method. This + method has to return the list of possible completions for user's input. + Because of the python / readline interaction, each completion should + be a superset of the user's input. + + NOTE: readline tokenizes user's input and only passes last token to + completers. + """ + + def possible_matches(self, text): + """return possible completions for user's input. + + Parameters: + text: the user's input + + Return: + a list of completions. Each completion includes the original input. + """ + raise NotImplementedError() + + +class RQLExecuteMatcher(AbstractMatcher): + """Custom matcher for rql queries. + + If user's input starts with ``rql(`` or ``session.execute(`` and + the corresponding rql query is incomplete, suggest some valid completions. + """ + query_match_rgx = re.compile( + r'(?P\s*(?:rql)' # match rql, possibly indented + r'|' # or + r'\s*(?:\w+\.execute))' # match .execute, possibly indented + # end of + r'\(' # followed by a parenthesis + r'(?P["\'])' # a quote or double quote + r'(?P.*)') # and some content + + def __init__(self, local_ctx, req): + self.local_ctx = local_ctx + self.req = req + self.schema = req.vreg.schema + self.rsb = req.vreg['components'].select('rql.suggestions', req) + + @staticmethod + def match(text): + """check if ``text`` looks like a call to ``rql`` or ``session.execute`` + + Parameters: + text: the user's input + + Returns: + None if it doesn't match, the query structure otherwise. + """ + query_match = RQLExecuteMatcher.query_match_rgx.match(text) + if query_match is None: + return None + parameters_text = query_match.group('parameters') + quote_delim = query_match.group('quote_delim') + # first parameter is fully specified, no completion needed + if re.match(r"(.*?)%s" % quote_delim, parameters_text) is not None: + return None + func_prefix = query_match.group('func_prefix') + return { + # user's input + 'text': text, + # rql( or session.execute( + 'func_prefix': func_prefix, + # offset of rql query + 'rql_offset': len(func_prefix) + 2, + # incomplete rql query + 'rql_query': parameters_text, + } + + def possible_matches(self, text): + """call ``rql.suggestions`` component to complete user's input. + """ + # readline will only send last token, but we need the entire user's input + user_input = readline.get_line_buffer() + query_struct = self.match(user_input) + if query_struct is None: + return [] + else: + # we must only send completions of the last token => compute where it + # starts relatively to the rql query itself. + completion_offset = readline.get_begidx() - query_struct['rql_offset'] + rql_query = query_struct['rql_query'] + return [suggestion[completion_offset:] + for suggestion in self.rsb.build_suggestions(rql_query)] + + +class DefaultMatcher(AbstractMatcher): + """Default matcher: delegate to standard's `rlcompleter.Completer`` class + """ + def __init__(self, local_ctx): + self.completer = Completer(local_ctx) + + def possible_matches(self, text): + if "." in text: + return self.completer.attr_matches(text) + else: + return self.completer.global_matches(text) + + +class CWShellCompleter(object): + """Custom auto-completion helper for cubicweb-ctl shell. + + ``CWShellCompleter`` provides a ``complete`` method suitable for + ``readline.set_completer``. + + Attributes: + matchers: the list of ``AbstractMatcher`` instances that will suggest + possible completions + + The completion process is the following: + + - readline calls the ``complete`` method with user's input, + - the ``complete`` method asks for each known matchers if + it can suggest completions for user's input. + """ + + def __init__(self, local_ctx): + # list of matchers to ask for possible matches on completion + self.matchers = [DefaultMatcher(local_ctx)] + self.matchers.insert(0, RQLExecuteMatcher(local_ctx, local_ctx['session'])) + + def complete(self, text, state): + """readline's completer method + + cf http://docs.python.org/2/library/readline.html#readline.set_completer + for more details. + + Implementation inspired by `rlcompleter.Completer` + """ + if state == 0: + # reset self.matches + self.matches = [] + for matcher in self.matchers: + matches = matcher.possible_matches(text) + if matches: + self.matches = matches + break + else: + return None # no matcher able to handle `text` + try: + return self.matches[state] + except IndexError: + return None