Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/terminal/ptutils.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

112 statements  

1"""prompt-toolkit utilities 

2 

3Everything in this module is a private API, 

4not to be used outside IPython. 

5""" 

6from __future__ import annotations 

7 

8# Copyright (c) IPython Development Team. 

9# Distributed under the terms of the Modified BSD License. 

10 

11import unicodedata 

12from wcwidth import wcwidth 

13 

14from IPython.core.completer import ( 

15 provisionalcompleter, cursor_to_position, 

16 _deduplicate_completions) 

17from prompt_toolkit.completion import Completer, Completion 

18from prompt_toolkit.lexers import Lexer 

19from prompt_toolkit.lexers import PygmentsLexer 

20from prompt_toolkit.patch_stdout import patch_stdout 

21 

22 

23import pygments.lexers as pygments_lexers 

24import os 

25import sys 

26import traceback 

27 

28_completion_sentinel = object() 

29 

30 

31def _elide_point(string: str, *, min_elide) -> str: 

32 """ 

33 If a string is long enough, and has at least 3 dots, 

34 replace the middle part with ellipses. 

35 

36 If a string naming a file is long enough, and has at least 3 slashes, 

37 replace the middle part with ellipses. 

38 

39 If three consecutive dots, or two consecutive dots are encountered these are 

40 replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode 

41 equivalents 

42 """ 

43 if min_elide <= 0: 

44 return string 

45 string = string.replace('...','\N{HORIZONTAL ELLIPSIS}') 

46 string = string.replace('..','\N{TWO DOT LEADER}') 

47 if len(string) < min_elide: 

48 return string 

49 

50 object_parts = string.split('.') 

51 file_parts = string.split(os.sep) 

52 if file_parts[-1] == '': 

53 file_parts.pop() 

54 

55 if len(object_parts) > 3: 

56 return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format( 

57 object_parts[0], 

58 object_parts[1][:1], 

59 object_parts[-2][-1:], 

60 object_parts[-1], 

61 ) 

62 

63 elif len(file_parts) > 3: 

64 return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format( 

65 file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1] 

66 ) 

67 

68 return string 

69 

70 

71def _elide_typed(string: str, typed: str, *, min_elide: int) -> str: 

72 """ 

73 Elide the middle of a long string if the beginning has already been typed. 

74 """ 

75 

76 if min_elide <= 0: 

77 return string 

78 if len(string) < min_elide: 

79 return string 

80 cut_how_much = len(typed)-3 

81 if cut_how_much < 7: 

82 return string 

83 if string.startswith(typed) and len(string)> len(typed): 

84 return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}" 

85 return string 

86 

87 

88def _elide(string: str, typed: str, min_elide) -> str: 

89 return _elide_typed( 

90 _elide_point(string, min_elide=min_elide), 

91 typed, min_elide=min_elide) 

92 

93 

94 

95def _adjust_completion_text_based_on_context(text, body, offset): 

96 if text.endswith('=') and len(body) > offset and body[offset] == '=': 

97 return text[:-1] 

98 else: 

99 return text 

100 

101 

102class IPythonPTCompleter(Completer): 

103 """Adaptor to provide IPython completions to prompt_toolkit""" 

104 def __init__(self, ipy_completer=None, shell=None): 

105 if shell is None and ipy_completer is None: 

106 raise TypeError("Please pass shell=an InteractiveShell instance.") 

107 self._ipy_completer = ipy_completer 

108 self.shell = shell 

109 

110 @property 

111 def ipy_completer(self): 

112 if self._ipy_completer: 

113 return self._ipy_completer 

114 else: 

115 return self.shell.Completer 

116 

117 def get_completions(self, document, complete_event): 

118 if not document.current_line.strip(): 

119 return 

120 # Some bits of our completion system may print stuff (e.g. if a module 

121 # is imported). This context manager ensures that doesn't interfere with 

122 # the prompt. 

123 

124 with patch_stdout(), provisionalcompleter(): 

125 body = document.text 

126 cursor_row = document.cursor_position_row 

127 cursor_col = document.cursor_position_col 

128 cursor_position = document.cursor_position 

129 offset = cursor_to_position(body, cursor_row, cursor_col) 

130 try: 

131 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer) 

132 except Exception as e: 

133 try: 

134 exc_type, exc_value, exc_tb = sys.exc_info() 

135 traceback.print_exception(exc_type, exc_value, exc_tb) 

136 except AttributeError: 

137 print('Unrecoverable Error in completions') 

138 

139 def _get_completions(self, body, offset, cursor_position, ipyc): 

140 """ 

141 Private equivalent of get_completions() use only for unit_testing. 

142 """ 

143 debug = getattr(ipyc, 'debug', False) 

144 completions = _deduplicate_completions( 

145 body, ipyc.completions(body, offset)) 

146 for c in completions: 

147 if not c.text: 

148 # Guard against completion machinery giving us an empty string. 

149 continue 

150 text = unicodedata.normalize('NFC', c.text) 

151 # When the first character of the completion has a zero length, 

152 # then it's probably a decomposed unicode character. E.g. caused by 

153 # the "\dot" completion. Try to compose again with the previous 

154 # character. 

155 if wcwidth(text[0]) == 0: 

156 if cursor_position + c.start > 0: 

157 char_before = body[c.start - 1] 

158 fixed_text = unicodedata.normalize( 

159 'NFC', char_before + text) 

160 

161 # Yield the modified completion instead, if this worked. 

162 if wcwidth(text[0:1]) == 1: 

163 yield Completion(fixed_text, start_position=c.start - offset - 1) 

164 continue 

165 

166 # TODO: Use Jedi to determine meta_text 

167 # (Jedi currently has a bug that results in incorrect information.) 

168 # meta_text = '' 

169 # yield Completion(m, start_position=start_pos, 

170 # display_meta=meta_text) 

171 display_text = c.text 

172 

173 adjusted_text = _adjust_completion_text_based_on_context( 

174 c.text, body, offset 

175 ) 

176 min_elide = 30 if self.shell is None else self.shell.min_elide 

177 if c.type == "function": 

178 yield Completion( 

179 adjusted_text, 

180 start_position=c.start - offset, 

181 display=_elide( 

182 display_text + "()", 

183 body[c.start : c.end], 

184 min_elide=min_elide, 

185 ), 

186 display_meta=c.type + c.signature, 

187 ) 

188 else: 

189 yield Completion( 

190 adjusted_text, 

191 start_position=c.start - offset, 

192 display=_elide( 

193 display_text, 

194 body[c.start : c.end], 

195 min_elide=min_elide, 

196 ), 

197 display_meta=c.type, 

198 ) 

199 

200 

201class IPythonPTLexer(Lexer): 

202 """ 

203 Wrapper around PythonLexer and BashLexer. 

204 """ 

205 def __init__(self): 

206 l = pygments_lexers 

207 self.python_lexer = PygmentsLexer(l.Python3Lexer) 

208 self.shell_lexer = PygmentsLexer(l.BashLexer) 

209 

210 self.magic_lexers = { 

211 'HTML': PygmentsLexer(l.HtmlLexer), 

212 'html': PygmentsLexer(l.HtmlLexer), 

213 'javascript': PygmentsLexer(l.JavascriptLexer), 

214 'js': PygmentsLexer(l.JavascriptLexer), 

215 'perl': PygmentsLexer(l.PerlLexer), 

216 'ruby': PygmentsLexer(l.RubyLexer), 

217 'latex': PygmentsLexer(l.TexLexer), 

218 } 

219 

220 def lex_document(self, document): 

221 text = document.text.lstrip() 

222 

223 lexer = self.python_lexer 

224 

225 if text.startswith('!') or text.startswith('%%bash'): 

226 lexer = self.shell_lexer 

227 

228 elif text.startswith('%%'): 

229 for magic, l in self.magic_lexers.items(): 

230 if text.startswith('%%' + magic): 

231 lexer = l 

232 break 

233 

234 return lexer.lex_document(document)