calltip.py 5.92 KB
Newer Older
1
"""Pop up a reminder of how to call a function.
David Scherer's avatar
David Scherer committed
2

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
3 4 5
Call Tips are floating windows which display function, class, and method
parameter and docstring information when you type an opening parenthesis, and
which disappear when you type a closing parenthesis.
6
"""
7
import inspect
8
import re
9
import sys
10
import textwrap
David Scherer's avatar
David Scherer committed
11 12
import types

13 14
from idlelib import calltip_w
from idlelib.hyperparser import HyperParser
15
import __main__
16

David Scherer's avatar
David Scherer committed
17

18
class Calltip:
David Scherer's avatar
David Scherer committed
19

20
    def __init__(self, editwin=None):
21
        if editwin is None:  # subprocess and test
22
            self.editwin = None
23 24 25 26 27
        else:
            self.editwin = editwin
            self.text = editwin.text
            self.active_calltip = None
            self._calltip_window = self._make_tk_calltip_window
David Scherer's avatar
David Scherer committed
28 29

    def close(self):
30
        self._calltip_window = None
David Scherer's avatar
David Scherer committed
31 32

    def _make_tk_calltip_window(self):
33
        # See __init__ for usage
34
        return calltip_w.CalltipWindow(self.text)
David Scherer's avatar
David Scherer committed
35

36
    def _remove_calltip_window(self, event=None):
37 38 39
        if self.active_calltip:
            self.active_calltip.hidetip()
            self.active_calltip = None
40

41
    def force_open_calltip_event(self, event):
42
        "The user selected the menu entry or hotkey, open the tip."
43
        self.open_calltip(True)
44
        return "break"
David Scherer's avatar
David Scherer committed
45

46
    def try_open_calltip_event(self, event):
47
        """Happens when it would be nice to open a calltip, but not really
48
        necessary, for example after an opening bracket, so function calls
49 50 51 52 53
        won't be made.
        """
        self.open_calltip(False)

    def refresh_calltip_event(self, event):
54
        if self.active_calltip and self.active_calltip.tipwindow:
55
            self.open_calltip(False)
David Scherer's avatar
David Scherer committed
56

57 58
    def open_calltip(self, evalfuncs):
        self._remove_calltip_window()
59

60 61 62 63 64
        hp = HyperParser(self.editwin, "insert")
        sur_paren = hp.get_surrounding_brackets('(')
        if not sur_paren:
            return
        hp.set_index(sur_paren[0])
65 66
        expression  = hp.get_expression()
        if not expression:
67
            return
68
        if not evalfuncs and (expression.find('(') != -1):
69
            return
70
        argspec = self.fetch_tip(expression)
71
        if not argspec:
72
            return
73 74
        self.active_calltip = self._calltip_window()
        self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1])
75

76
    def fetch_tip(self, expression):
77
        """Return the argument list and docstring of a function or class.
78

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
79
        If there is a Python subprocess, get the calltip there.  Otherwise,
80 81
        either this fetch_tip() is running in the subprocess or it was
        called in an IDLE running without the subprocess.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
82 83 84 85 86

        The subprocess environment is that of the most recently run script.  If
        two unrelated modules are being edited some calltips in the current
        module may be inoperative if the module was not the last to run.

87 88
        To find methods, fetch_tip must be fed a fully qualified name.

89
        """
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
90 91
        try:
            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
92
        except AttributeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
93
            rpcclt = None
94 95
        if rpcclt:
            return rpcclt.remotecall("exec", "get_the_calltip",
96
                                     (expression,), {})
97
        else:
98
            return get_argspec(get_entity(expression))
99

100

101 102 103 104 105 106 107 108 109 110 111 112 113 114
def get_entity(expression):
    """Return the object corresponding to expression evaluated
    in a namespace spanning sys.modules and __main.dict__.
    """
    if expression:
        namespace = sys.modules.copy()
        namespace.update(__main__.__dict__)
        try:
            return eval(expression, namespace)
        except BaseException:
            # An uncaught exception closes idle, and eval can raise any
            # exception, especially if user classes are involved.
            return None

115
# The following are used in get_argspec and some in tests
116
_MAX_COLS = 85
117
_MAX_LINES = 5  # enough for bytes
118
_INDENT = ' '*4  # for wrapped signatures
119
_first_param = re.compile(r'(?<=\()\w*\,?\s*')
120
_default_callable_argspec = "See source or doc"
121 122
_invalid_method = "invalid method signature"
_argument_positional = "\n['/' marks preceding arguments as positional-only]\n"
123 124

def get_argspec(ob):
125
    '''Return a string describing the signature of a callable object, or ''.
126 127 128

    For Python-coded functions and methods, the first line is introspected.
    Delete 'self' parameter for classes (.__init__) and bound methods.
129 130 131
    The next lines are the first lines of the doc string up to the first
    empty line or _MAX_LINES.    For builtins, this typically includes
    the arguments in addition to the return value.
132
    '''
133
    argspec = default = ""
134 135 136
    try:
        ob_call = ob.__call__
    except BaseException:
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
        return default

    fob = ob_call if isinstance(ob_call, types.MethodType) else ob

    try:
        argspec = str(inspect.signature(fob))
    except ValueError as err:
        msg = str(err)
        if msg.startswith(_invalid_method):
            return _invalid_method

    if '/' in argspec:
        """Using AC's positional argument should add the explain"""
        argspec += _argument_positional
    if isinstance(fob, type) and argspec == '()':
        """fob with no argument, use default callable argspec"""
        argspec = _default_callable_argspec
154

155
    lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
156
             if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
157

158 159 160 161 162
    if isinstance(ob_call, types.MethodType):
        doc = ob_call.__doc__
    else:
        doc = getattr(ob, "__doc__", "")
    if doc:
163
        for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]:
164 165 166 167 168 169 170
            line = line.strip()
            if not line:
                break
            if len(line) > _MAX_COLS:
                line = line[: _MAX_COLS - 3] + '...'
            lines.append(line)
        argspec = '\n'.join(lines)
171 172
    if not argspec:
        argspec = _default_callable_argspec
173
    return argspec
David Scherer's avatar
David Scherer committed
174

175

176
if __name__ == '__main__':
177
    from unittest import main
178
    main('idlelib.idle_test.test_calltip', verbosity=2)