Coverage for calltip.py: 75%

108 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1"""Pop up a reminder of how to call a function. 

2 

3Call Tips are floating windows which display function, class, and method 

4parameter and docstring information when you type an opening parenthesis, and 

5which disappear when you type a closing parenthesis. 

6""" 

7import __main__ 

8import inspect 

9import re 

10import sys 

11import textwrap 

12import types 

13 

14from idlelib import calltip_w 

15from idlelib.hyperparser import HyperParser 

16 

17 

18class Calltip: 

19 

20 def __init__(self, editwin=None): 

21 if editwin is None: # subprocess and test 

22 self.editwin = None 

23 else: 

24 self.editwin = editwin 

25 self.text = editwin.text 

26 self.active_calltip = None 

27 self._calltip_window = self._make_tk_calltip_window 

28 

29 def close(self): 

30 self._calltip_window = None 

31 

32 def _make_tk_calltip_window(self): 

33 # See __init__ for usage 

34 return calltip_w.CalltipWindow(self.text) 

35 

36 def remove_calltip_window(self, event=None): 

37 if self.active_calltip: 

38 self.active_calltip.hidetip() 

39 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" 

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 won't be made. 

50 """ 

51 self.open_calltip(False) 

52 

53 def refresh_calltip_event(self, event): 

54 if self.active_calltip and self.active_calltip.tipwindow: 

55 self.open_calltip(False) 

56 

57 def open_calltip(self, evalfuncs): 

58 """Maybe close an existing calltip and maybe open a new calltip. 

59 

60 Called from (force_open|try_open|refresh)_calltip_event functions. 

61 """ 

62 hp = HyperParser(self.editwin, "insert") 1ghei

63 sur_paren = hp.get_surrounding_brackets('(') 1ghei

64 

65 # If not inside parentheses, no calltip. 

66 if not sur_paren: 1ghei

67 self.remove_calltip_window() 1ghei

68 return 1ghei

69 

70 # If a calltip is shown for the current parentheses, do 

71 # nothing. 

72 if self.active_calltip: 1ghei

73 opener_line, opener_col = map(int, sur_paren[0].split('.')) 1e

74 if ( 74 ↛ 80line 74 didn't jump to line 80

75 (opener_line, opener_col) == 

76 (self.active_calltip.parenline, self.active_calltip.parencol) 

77 ): 

78 return 1e

79 

80 hp.set_index(sur_paren[0]) 1ghei

81 try: 1ghei

82 expression = hp.get_expression() 1ghei

83 except ValueError: 

84 expression = None 

85 if not expression: 85 ↛ 89line 85 didn't jump to line 89, because the condition on line 85 was never true1ghei

86 # No expression before the opening parenthesis, e.g. 

87 # because it's in a string or the opener for a tuple: 

88 # Do nothing. 

89 return 

90 

91 # At this point, the current index is after an opening 

92 # parenthesis, in a section of code, preceded by a valid 

93 # expression. If there is a calltip shown, it's not for the 

94 # same index and should be closed. 

95 self.remove_calltip_window() 1ghei

96 

97 # Simple, fast heuristic: If the preceding expression includes 

98 # an opening parenthesis, it likely includes a function call. 

99 if not evalfuncs and (expression.find('(') != -1): 99 ↛ 100line 99 didn't jump to line 100, because the condition on line 99 was never true1ghei

100 return 

101 

102 argspec = self.fetch_tip(expression) 1ghei

103 if not argspec: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true1ghei

104 return 

105 self.active_calltip = self._calltip_window() 1ghei

106 self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) 1ghei

107 

108 def fetch_tip(self, expression): 

109 """Return the argument list and docstring of a function or class. 

110 

111 If there is a Python subprocess, get the calltip there. Otherwise, 

112 either this fetch_tip() is running in the subprocess or it was 

113 called in an IDLE running without the subprocess. 

114 

115 The subprocess environment is that of the most recently run script. If 

116 two unrelated modules are being edited some calltips in the current 

117 module may be inoperative if the module was not the last to run. 

118 

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

120 

121 """ 

122 try: 

123 rpcclt = self.editwin.flist.pyshell.interp.rpcclt 

124 except AttributeError: 

125 rpcclt = None 

126 if rpcclt: 

127 return rpcclt.remotecall("exec", "get_the_calltip", 

128 (expression,), {}) 

129 else: 

130 return get_argspec(get_entity(expression)) 

131 

132 

133def get_entity(expression): 

134 """Return the object corresponding to expression evaluated 

135 in a namespace spanning sys.modules and __main.dict__. 

136 """ 

137 if expression: 137 ↛ exitline 137 didn't return from function 'get_entity', because the condition on line 137 was never false1st

138 namespace = {**sys.modules, **__main__.__dict__} 1st

139 try: 1st

140 return eval(expression, namespace) # Only protect user code. 1st

141 except BaseException: 1s

142 # An uncaught exception closes idle, and eval can raise any 

143 # exception, especially if user classes are involved. 

144 return None 1s

145 

146# The following are used in get_argspec and some in tests 

147_MAX_COLS = 85 

148_MAX_LINES = 5 # enough for bytes 

149_INDENT = ' '*4 # for wrapped signatures 

150_first_param = re.compile(r'(?<=\()\w*\,?\s*') 

151_default_callable_argspec = "See source or doc" 

152_invalid_method = "invalid method signature" 

153 

154def get_argspec(ob): 

155 '''Return a string describing the signature of a callable object, or ''. 

156 

157 For Python-coded functions and methods, the first line is introspected. 

158 Delete 'self' parameter for classes (.__init__) and bound methods. 

159 The next lines are the first lines of the doc string up to the first 

160 empty line or _MAX_LINES. For builtins, this typically includes 

161 the arguments in addition to the return value. 

162 ''' 

163 # Determine function object fob to inspect. 

164 try: 1jocfkrbldpumnq

165 ob_call = ob.__call__ 1jocfkrbldpumnq

166 except BaseException: # Buggy user object could raise anything. 1obu

167 return '' # No popup for non-callables. 1obu

168 # For Get_argspecTest.test_buggy_getattr_class, CallA() & CallB(). 

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

170 

171 # Initialize argspec and wrap it to get lines. 

172 try: 1jocfkrbldpmnq

173 argspec = str(inspect.signature(fob)) 1jocfkrbldpmnq

174 except Exception as err: 1crbd

175 msg = str(err) 1crbd

176 if msg.startswith(_invalid_method): 1crbd

177 return _invalid_method 1r

178 else: 

179 argspec = '' 1cbd

180 

181 if isinstance(fob, type) and argspec == '()': 1jocfkbldpmnq

182 # If fob has no argument, use default callable argspec. 

183 argspec = _default_callable_argspec 1o

184 

185 lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) 1jocfkbldpmnq

186 if len(argspec) > _MAX_COLS else [argspec] if argspec else []) 

187 

188 # Augment lines from docstring, if any, and join to get argspec. 

189 doc = inspect.getdoc(ob) 1jocfkbldpmnq

190 if doc: 1jocfkbldpmnq

191 for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: 1jcfkbldmn

192 line = line.strip() 1jcfkbldmn

193 if not line: 1jcfkbldmn

194 break 1d

195 if len(line) > _MAX_COLS: 1jcfkbldmn

196 line = line[: _MAX_COLS - 3] + '...' 1cf

197 lines.append(line) 1jcfkbldmn

198 argspec = '\n'.join(lines) 1jocfkbldpmnq

199 

200 return argspec or _default_callable_argspec 1jocfkbldpmnq

201 

202 

203if __name__ == '__main__': 203 ↛ 204line 203 didn't jump to line 204, because the condition on line 203 was never true

204 from unittest import main 

205 main('idlelib.idle_test.test_calltip', verbosity=2)