2020-09-11 18:18:03 +00:00
|
|
|
"""
|
|
|
|
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 inspect
|
|
|
|
from collections import namedtuple
|
2021-11-04 19:02:23 +00:00
|
|
|
from six.moves import builtins
|
2021-06-22 15:12:25 +00:00
|
|
|
import six
|
|
|
|
|
2020-09-11 18:18:03 +00:00
|
|
|
# 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.')
|
2021-06-22 15:12:25 +00:00
|
|
|
except Exception as e:
|
2020-09-11 18:18:03 +00:00
|
|
|
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 = {
|
2021-06-22 15:12:25 +00:00
|
|
|
key: value for key, value in six.iteritems(usercode_context)
|
2020-09-11 18:18:03 +00:00
|
|
|
# Don't propose unimplemented functions in autocomplete
|
|
|
|
if not (value and callable(value) and getattr(value, 'unimplemented', None))
|
|
|
|
}
|
|
|
|
|
2021-11-04 19:02:23 +00:00
|
|
|
# Add some common non-lowercase builtins, so that we include them into the case-handling below.
|
|
|
|
self._context.update({
|
|
|
|
'True': True,
|
|
|
|
'False': False,
|
|
|
|
'None': None,
|
|
|
|
})
|
|
|
|
|
2020-09-11 18:18:03 +00:00
|
|
|
# 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 = {}
|
2021-06-22 15:12:25 +00:00
|
|
|
for key, value in six.iteritems(self._context):
|
2021-07-20 20:57:35 +00:00
|
|
|
if value and callable(value):
|
2020-09-11 18:18:03 +00:00
|
|
|
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=<value>, ...)', True)
|
|
|
|
self._functions['.lookupRecords'] = Completion('.lookupRecords', '(colName=<value>, ...)', True)
|
2021-07-16 18:15:04 +00:00
|
|
|
self._functions['.Record'] = Completion('.Record', '', True)
|
|
|
|
self._functions['.RecordSet'] = Completion('.RecordSet', '', True)
|
2020-09-11 18:18:03 +00:00
|
|
|
|
|
|
|
# Remember the original name for each lowercase one.
|
|
|
|
self._lowercase = {}
|
|
|
|
for key in self._context:
|
|
|
|
lower = key.lower()
|
|
|
|
if lower == key:
|
|
|
|
continue
|
2021-11-04 19:02:23 +00:00
|
|
|
if not any((lower in d) for d in (self._context, self._lowercase, builtins.__dict__)):
|
2020-09-11 18:18:03 +00:00
|
|
|
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
|
|
|
|
|
2021-07-07 16:03:01 +00:00
|
|
|
# Lowercase 'value' is used in trigger formulas, and is not the same as 'VALUE'.
|
|
|
|
self._lowercase.pop('value', None)
|
|
|
|
|
2020-09-11 18:18:03 +00:00
|
|
|
# Add the lowercase names to the context, and to the detailed completions in _functions.
|
2021-06-22 15:12:25 +00:00
|
|
|
for lower, key in six.iteritems(self._lowercase):
|
2020-09-11 18:18:03 +00:00
|
|
|
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):
|
2021-06-24 12:23:33 +00:00
|
|
|
# 'for' suggests the autocompletion 'for ' in python 3
|
|
|
|
result = result.rstrip()
|
|
|
|
|
2020-09-11 18:18:03 +00:00
|
|
|
# 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:
|
2020-09-13 05:41:43 +00:00
|
|
|
varname = funcname[:dot]
|
|
|
|
funcname = self._lowercase.get(varname, varname) + key
|
2020-09-11 18:18:03 +00:00
|
|
|
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)
|