jpayne@68: """Pop up a reminder of how to call a function. jpayne@68: jpayne@68: Call Tips are floating windows which display function, class, and method jpayne@68: parameter and docstring information when you type an opening parenthesis, and jpayne@68: which disappear when you type a closing parenthesis. jpayne@68: """ jpayne@68: import __main__ jpayne@68: import inspect jpayne@68: import re jpayne@68: import sys jpayne@68: import textwrap jpayne@68: import types jpayne@68: jpayne@68: from idlelib import calltip_w jpayne@68: from idlelib.hyperparser import HyperParser jpayne@68: jpayne@68: jpayne@68: class Calltip: jpayne@68: jpayne@68: def __init__(self, editwin=None): jpayne@68: if editwin is None: # subprocess and test jpayne@68: self.editwin = None jpayne@68: else: jpayne@68: self.editwin = editwin jpayne@68: self.text = editwin.text jpayne@68: self.active_calltip = None jpayne@68: self._calltip_window = self._make_tk_calltip_window jpayne@68: jpayne@68: def close(self): jpayne@68: self._calltip_window = None jpayne@68: jpayne@68: def _make_tk_calltip_window(self): jpayne@68: # See __init__ for usage jpayne@68: return calltip_w.CalltipWindow(self.text) jpayne@68: jpayne@68: def _remove_calltip_window(self, event=None): jpayne@68: if self.active_calltip: jpayne@68: self.active_calltip.hidetip() jpayne@68: self.active_calltip = None jpayne@68: jpayne@68: def force_open_calltip_event(self, event): jpayne@68: "The user selected the menu entry or hotkey, open the tip." jpayne@68: self.open_calltip(True) jpayne@68: return "break" jpayne@68: jpayne@68: def try_open_calltip_event(self, event): jpayne@68: """Happens when it would be nice to open a calltip, but not really jpayne@68: necessary, for example after an opening bracket, so function calls jpayne@68: won't be made. jpayne@68: """ jpayne@68: self.open_calltip(False) jpayne@68: jpayne@68: def refresh_calltip_event(self, event): jpayne@68: if self.active_calltip and self.active_calltip.tipwindow: jpayne@68: self.open_calltip(False) jpayne@68: jpayne@68: def open_calltip(self, evalfuncs): jpayne@68: self._remove_calltip_window() jpayne@68: jpayne@68: hp = HyperParser(self.editwin, "insert") jpayne@68: sur_paren = hp.get_surrounding_brackets('(') jpayne@68: if not sur_paren: jpayne@68: return jpayne@68: hp.set_index(sur_paren[0]) jpayne@68: expression = hp.get_expression() jpayne@68: if not expression: jpayne@68: return jpayne@68: if not evalfuncs and (expression.find('(') != -1): jpayne@68: return jpayne@68: argspec = self.fetch_tip(expression) jpayne@68: if not argspec: jpayne@68: return jpayne@68: self.active_calltip = self._calltip_window() jpayne@68: self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) jpayne@68: jpayne@68: def fetch_tip(self, expression): jpayne@68: """Return the argument list and docstring of a function or class. jpayne@68: jpayne@68: If there is a Python subprocess, get the calltip there. Otherwise, jpayne@68: either this fetch_tip() is running in the subprocess or it was jpayne@68: called in an IDLE running without the subprocess. jpayne@68: jpayne@68: The subprocess environment is that of the most recently run script. If jpayne@68: two unrelated modules are being edited some calltips in the current jpayne@68: module may be inoperative if the module was not the last to run. jpayne@68: jpayne@68: To find methods, fetch_tip must be fed a fully qualified name. jpayne@68: jpayne@68: """ jpayne@68: try: jpayne@68: rpcclt = self.editwin.flist.pyshell.interp.rpcclt jpayne@68: except AttributeError: jpayne@68: rpcclt = None jpayne@68: if rpcclt: jpayne@68: return rpcclt.remotecall("exec", "get_the_calltip", jpayne@68: (expression,), {}) jpayne@68: else: jpayne@68: return get_argspec(get_entity(expression)) jpayne@68: jpayne@68: jpayne@68: def get_entity(expression): jpayne@68: """Return the object corresponding to expression evaluated jpayne@68: in a namespace spanning sys.modules and __main.dict__. jpayne@68: """ jpayne@68: if expression: jpayne@68: namespace = {**sys.modules, **__main__.__dict__} jpayne@68: try: jpayne@68: return eval(expression, namespace) # Only protect user code. jpayne@68: except BaseException: jpayne@68: # An uncaught exception closes idle, and eval can raise any jpayne@68: # exception, especially if user classes are involved. jpayne@68: return None jpayne@68: jpayne@68: # The following are used in get_argspec and some in tests jpayne@68: _MAX_COLS = 85 jpayne@68: _MAX_LINES = 5 # enough for bytes jpayne@68: _INDENT = ' '*4 # for wrapped signatures jpayne@68: _first_param = re.compile(r'(?<=\()\w*\,?\s*') jpayne@68: _default_callable_argspec = "See source or doc" jpayne@68: _invalid_method = "invalid method signature" jpayne@68: _argument_positional = " # '/' marks preceding args as positional-only." jpayne@68: jpayne@68: def get_argspec(ob): jpayne@68: '''Return a string describing the signature of a callable object, or ''. jpayne@68: jpayne@68: For Python-coded functions and methods, the first line is introspected. jpayne@68: Delete 'self' parameter for classes (.__init__) and bound methods. jpayne@68: The next lines are the first lines of the doc string up to the first jpayne@68: empty line or _MAX_LINES. For builtins, this typically includes jpayne@68: the arguments in addition to the return value. jpayne@68: ''' jpayne@68: argspec = default = "" jpayne@68: try: jpayne@68: ob_call = ob.__call__ jpayne@68: except BaseException: jpayne@68: return default jpayne@68: jpayne@68: fob = ob_call if isinstance(ob_call, types.MethodType) else ob jpayne@68: jpayne@68: try: jpayne@68: argspec = str(inspect.signature(fob)) jpayne@68: except ValueError as err: jpayne@68: msg = str(err) jpayne@68: if msg.startswith(_invalid_method): jpayne@68: return _invalid_method jpayne@68: jpayne@68: if '/' in argspec and len(argspec) < _MAX_COLS - len(_argument_positional): jpayne@68: # Add explanation TODO remove after 3.7, before 3.9. jpayne@68: argspec += _argument_positional jpayne@68: if isinstance(fob, type) and argspec == '()': jpayne@68: # If fob has no argument, use default callable argspec. jpayne@68: argspec = _default_callable_argspec jpayne@68: jpayne@68: lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) jpayne@68: if len(argspec) > _MAX_COLS else [argspec] if argspec else []) jpayne@68: jpayne@68: if isinstance(ob_call, types.MethodType): jpayne@68: doc = ob_call.__doc__ jpayne@68: else: jpayne@68: doc = getattr(ob, "__doc__", "") jpayne@68: if doc: jpayne@68: for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: jpayne@68: line = line.strip() jpayne@68: if not line: jpayne@68: break jpayne@68: if len(line) > _MAX_COLS: jpayne@68: line = line[: _MAX_COLS - 3] + '...' jpayne@68: lines.append(line) jpayne@68: argspec = '\n'.join(lines) jpayne@68: if not argspec: jpayne@68: argspec = _default_callable_argspec jpayne@68: return argspec jpayne@68: jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_calltip', verbosity=2)