""" Helper class for handling formula autocomplete. It's intended to use with rlcompleter.Completer. It allows finding global names using lowercase searches, and adds function usage information to some results. """ import __builtin__ import inspect from collections import namedtuple # funcname is the function name, e.g. "MAX" # argspec is the signature, e.g. "(arg, *more_args)" # isgrist is a boolean for whether this function should be in Grist documentation. Completion = namedtuple('Completion', ['funcname', 'argspec', 'isgrist']) def is_grist_func(func): try: return inspect.getmodule(func).__name__.startswith('functions.') except Exception, e: return e class AutocompleteContext(object): def __init__(self, usercode_context): # rlcompleter is case-sensitive. This is hard to work around while maintaining attribute # lookups. As a middle ground, we only introduce lowercase versions of all global names. self._context = { key: value for key, value in usercode_context.iteritems() # Don't propose unimplemented functions in autocomplete if not (value and callable(value) and getattr(value, 'unimplemented', None)) } # Prepare detailed Completion objects for functions where we can supply more info. # TODO It would be nice to include builtin functions too, but getargspec doesn't work there. self._functions = {} for key, value in self._context.iteritems(): if value and callable(value): argspec = inspect.formatargspec(*inspect.getargspec(value)) self._functions[key] = Completion(key, argspec, is_grist_func(value)) # Add in the important UserTable methods, with custom friendlier descriptions. self._functions['.lookupOne'] = Completion('.lookupOne', '(colName=, ...)', True) self._functions['.lookupRecords'] = Completion('.lookupRecords', '(colName=, ...)', True) # Remember the original name for each lowercase one. self._lowercase = {} for key in self._context: lower = key.lower() if lower == key: continue if lower not in self._context and lower not in __builtin__.__dict__: self._lowercase[lower] = key else: # This is still good enough to find a match for, and translate back to the original. # It allows rlcompleter to match e.g. 'max' against 'max', 'Max', and 'MAX' (using keys # 'max', 'max*', and 'max**', respectively). lower += '*' if lower in self._lowercase: lower += '*' self._lowercase[lower] = key # Add the lowercase names to the context, and to the detailed completions in _functions. for lower, key in self._lowercase.iteritems(): self._context[lower] = self._context[key] if key in self._functions: self._functions[lower] = self._functions[key] def get_context(self): return self._context def process_result(self, result): # Callables are returned by rlcompleter with a trailing "(". if result.endswith('('): funcname = result[0:-1] dot = funcname.rfind(".") key = funcname[dot:] if dot >= 0 else funcname completion = self._functions.get(key) # Return the detailed completion if we have it, or the result string otherwise. if completion: # For methods (eg ".lookupOne"), use the original result as funcname (eg "Foo.lookupOne"). if dot >= 0: varname = funcname[:dot] funcname = self._lowercase.get(varname, varname) + key completion = completion._replace(funcname=funcname) return tuple(completion) return result # Return translation from lowercase if there is one, or the result string otherwise. return self._lowercase.get(result, result)