Skip to content
Snippets Groups Projects
completion.py 6.77 KiB
Newer Older
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
#     NScD Oak Ridge National Laboratory, European Spallation Source
#     & Institut Laue - Langevin
# SPDX - License - Identifier: GPL - 3.0 +
#  This file is part of the mantid workbench.

from __future__ import (absolute_import, unicode_literals)

import inspect
from collections import namedtuple
from six import PY2
    from inspect import getargspec as getfullargspec
    from inspect import getfullargspec

from mantidqt.widgets.codeeditor.editor import CodeEditor


def get_function_spec(func):
    """
    Get the python function signature for the given function object. First
    the args are inspected followed by varargs, which are set by some modules,
    e.g. mantid.simpleapi algorithm functions

    :param func: A Python function object
    :returns: A string containing the function specification
    """
    try:
        argspec = getfullargspec(func)
    except TypeError:
        try:
            ArgSpec = namedtuple("ArgSpec", "args varargs keywords defaults")
            args_obj = inspect.getargs(func.__code__)
            argspec = ArgSpec(args_obj.args, args_obj.varargs, args_obj.varkw, defaults=None)
        except (TypeError, AttributeError, ValueError):
            return ''
    # mantid algorithm functions have varargs set not args
    args = argspec[0]
    if args:
        # For methods strip the self argument
        if hasattr(func, 'im_func'):
            args = args[1:]
        defs = argspec[3]
    elif argspec[1] is not None:
        # Get from varargs/keywords
        arg_str = argspec[1].strip().lstrip('\b').replace(',', ', ')
        defs = []
        # Keyword args
        kwargs = argspec[2]
        if kwargs is not None:
            kwargs = kwargs.strip().lstrip('\b\b')
            if kwargs == 'kwargs':
                kwargs = '**' + kwargs + '=None'
            arg_str += ', %s' % kwargs
        # Any default argument appears in the string
        # on the rhs of an equal
        for arg in arg_str.split(', '):
            arg = arg.strip()
            if '=' in arg:
                arg_token = arg.split('=')
                args.append(arg_token[0])
                defs.append(arg_token[1])
            else:
                args.append(arg)
        if len(defs) == 0:
            defs = None
    else:
        return ''

    if defs is None:
        call_tip = ', '.join(args)
        call_tip = '(' + call_tip + ')'
    else:
        # The defaults list contains the default values for the last n arguments
        diff = len(args) - len(defs)
        call_tip = ''
        for index in range(len(args) - 1, -1, -1):
            def_index = index - diff
            if def_index >= 0:
                call_tip = '[' + args[index] + '], ' + call_tip
            else:
                call_tip = args[index] + ", " + call_tip
        call_tip = '(' + call_tip.rstrip(', ') + ')'
    return call_tip


def generate_call_tips(definitions, prepend_module_name=None):
    """
    Generate call tips for a dictionary of object definitions (eg. globals()).
    The call tips generated are of the form:
        Load(InputWorkspace, [OutputWorkspace], ...)
    where squared braces denote key-word arguments.

    :param dict definitions: Dictionary with names of python objects as keys and
        the objects themselves as values
    :param str prepend_module_name: Prepend the name of the module to the call tips
        default is None
    :returns list: A list of call tips
    """
    if not isinstance(definitions, dict):
        return []
    call_tips = []
    for name, py_object in definitions.items():
        if name.startswith('_'):
            continue
        if inspect.isfunction(py_object) or inspect.isbuiltin(py_object):
            if not prepend_module_name:
                call_tips.append(name + get_function_spec(py_object))
            else:
                call_tips.append(prepend_module_name + '.' + name + get_function_spec(py_object))
        # Ignore modules or we get duplicates of methods/classes that are imported
        # in outer scopes, e.g. numpy.array and numpy.core.array
        if inspect.ismodule(py_object):
            continue
        for attr in dir(py_object):
            try:
                f_attr = getattr(py_object, attr)
            except Exception:
                continue
            if attr.startswith('_'):
                continue
            if hasattr(f_attr, 'im_func') or inspect.isfunction(f_attr) or inspect.ismethod(f_attr):
                call_tip = name + '.' + attr + get_function_spec(f_attr)
                call_tip = name + '.' + attr
            if prepend_module_name:
                call_tips.append(prepend_module_name + '.' + call_tip)
class CodeCompleter:
    """
    This class generates autocompletions for Workbench's script editor.
    It generates autocompletions from environment globals. These completions
    are updated on every successful script execution.
    def __init__(self, editor, env_globals=None):
        self.editor = editor
        self.env_globals = env_globals

        # A dict gives O(1) lookups and ensures we have no duplicates
        self._completions_dict = dict()
        if "from mantid.simpleapi import *" in self.editor.text():
            self._add_to_completions(self._get_module_call_tips('mantid.simpleapi'))
        if re.search("import .*numpy( |,|$)", self.editor.text()):
            self._add_to_completions(self._get_module_call_tips('numpy'))
        if re.search("import .*pyplot( |,|$)", self.editor.text()):
            self._add_to_completions(self._get_module_call_tips('matplotlib.pyplot'))
        self.editor.enableAutoCompletion(CodeEditor.AcsAll)
        self.editor.updateCompletionAPI(self.completions)
    @property
    def completions(self):
        return list(self._completions_dict.keys())

    def _get_completions_from_globals(self):
        return generate_call_tips(self.env_globals)
    def _add_to_completions(self, completions):
        for completion in completions:
            self._completions_dict[completion] = True
    def update_completion_api(self):
        self._add_to_completions(self._get_completions_from_globals())
        self.editor.updateCompletionAPI(self.completions)
    def _get_module_call_tips(self, module):
        """
        Get the call tips for a given module. If the module cannot be
        found in sys.modules return an empty list
        :param str module: The name of the module
        :return list: A list of call tips for the module
        """
        try:
            module = sys.modules[module]
        except KeyError:
            return []
        return generate_call_tips(module.__dict__, module.__name__)