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

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

108 statements  

1"""prompt-toolkit utilities 

2 

3Everything in this module is a private API, 

4not to be used outside IPython. 

5""" 

6 

7# Copyright (c) IPython Development Team. 

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

9 

10import unicodedata 

11from wcwidth import wcwidth 

12 

13from IPython.core.completer import ( 

14 provisionalcompleter, cursor_to_position, 

15 _deduplicate_completions) 

16from prompt_toolkit.completion import Completer, Completion 

17from prompt_toolkit.lexers import Lexer 

18from prompt_toolkit.lexers import PygmentsLexer 

19from prompt_toolkit.patch_stdout import patch_stdout 

20from IPython.core.getipython import get_ipython 

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 string = string.replace('...','\N{HORIZONTAL ELLIPSIS}') 

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

45 if len(string) < min_elide: 

46 return string 

47 

48 object_parts = string.split('.') 

49 file_parts = string.split(os.sep) 

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

51 file_parts.pop() 

52 

53 if len(object_parts) > 3: 

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

55 object_parts[0], 

56 object_parts[1][:1], 

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

58 object_parts[-1], 

59 ) 

60 

61 elif len(file_parts) > 3: 

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

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

64 ) 

65 

66 return string 

67 

68 

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

70 """ 

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

72 """ 

73 

74 if len(string) < min_elide: 

75 return string 

76 cut_how_much = len(typed)-3 

77 if cut_how_much < 7: 

78 return string 

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

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

81 return string 

82 

83 

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

85 return _elide_typed( 

86 _elide_point(string, min_elide=min_elide), 

87 typed, min_elide=min_elide) 

88 

89 

90 

91def _adjust_completion_text_based_on_context(text, body, offset): 

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

93 return text[:-1] 

94 else: 

95 return text 

96 

97 

98class IPythonPTCompleter(Completer): 

99 """Adaptor to provide IPython completions to prompt_toolkit""" 

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

101 if shell is None and ipy_completer is None: 

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

103 self._ipy_completer = ipy_completer 

104 self.shell = shell 

105 

106 @property 

107 def ipy_completer(self): 

108 if self._ipy_completer: 

109 return self._ipy_completer 

110 else: 

111 return self.shell.Completer 

112 

113 def get_completions(self, document, complete_event): 

114 if not document.current_line.strip(): 

115 return 

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

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

118 # the prompt. 

119 

120 with patch_stdout(), provisionalcompleter(): 

121 body = document.text 

122 cursor_row = document.cursor_position_row 

123 cursor_col = document.cursor_position_col 

124 cursor_position = document.cursor_position 

125 offset = cursor_to_position(body, cursor_row, cursor_col) 

126 try: 

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

128 except Exception as e: 

129 try: 

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

131 traceback.print_exception(exc_type, exc_value, exc_tb) 

132 except AttributeError: 

133 print('Unrecoverable Error in completions') 

134 

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

136 """ 

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

138 """ 

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

140 completions = _deduplicate_completions( 

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

142 for c in completions: 

143 if not c.text: 

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

145 continue 

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

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

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

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

150 # character. 

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

152 if cursor_position + c.start > 0: 

153 char_before = body[c.start - 1] 

154 fixed_text = unicodedata.normalize( 

155 'NFC', char_before + text) 

156 

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

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

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

160 continue 

161 

162 # TODO: Use Jedi to determine meta_text 

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

164 # meta_text = '' 

165 # yield Completion(m, start_position=start_pos, 

166 # display_meta=meta_text) 

167 display_text = c.text 

168 

169 adjusted_text = _adjust_completion_text_based_on_context( 

170 c.text, body, offset 

171 ) 

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

173 if c.type == "function": 

174 yield Completion( 

175 adjusted_text, 

176 start_position=c.start - offset, 

177 display=_elide( 

178 display_text + "()", 

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

180 min_elide=min_elide, 

181 ), 

182 display_meta=c.type + c.signature, 

183 ) 

184 else: 

185 yield Completion( 

186 adjusted_text, 

187 start_position=c.start - offset, 

188 display=_elide( 

189 display_text, 

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

191 min_elide=min_elide, 

192 ), 

193 display_meta=c.type, 

194 ) 

195 

196 

197class IPythonPTLexer(Lexer): 

198 """ 

199 Wrapper around PythonLexer and BashLexer. 

200 """ 

201 def __init__(self): 

202 l = pygments_lexers 

203 self.python_lexer = PygmentsLexer(l.Python3Lexer) 

204 self.shell_lexer = PygmentsLexer(l.BashLexer) 

205 

206 self.magic_lexers = { 

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

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

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

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

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

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

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

214 } 

215 

216 def lex_document(self, document): 

217 text = document.text.lstrip() 

218 

219 lexer = self.python_lexer 

220 

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

222 lexer = self.shell_lexer 

223 

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

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

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

227 lexer = l 

228 break 

229 

230 return lexer.lex_document(document)