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

106 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

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 

20 

21import pygments.lexers as pygments_lexers 

22import os 

23import sys 

24import traceback 

25 

26_completion_sentinel = object() 

27 

28def _elide_point(string:str, *, min_elide=30)->str: 

29 """ 

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

31 replace the middle part with ellipses. 

32 

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

34 replace the middle part with ellipses. 

35 

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

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

38 equivalents 

39 """ 

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

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

42 if len(string) < min_elide: 

43 return string 

44 

45 object_parts = string.split('.') 

46 file_parts = string.split(os.sep) 

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

48 file_parts.pop() 

49 

50 if len(object_parts) > 3: 

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

52 object_parts[0], 

53 object_parts[1][:1], 

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

55 object_parts[-1], 

56 ) 

57 

58 elif len(file_parts) > 3: 

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

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

61 ) 

62 

63 return string 

64 

65def _elide_typed(string:str, typed:str, *, min_elide:int=30)->str: 

66 """ 

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

68 """ 

69 

70 if len(string) < min_elide: 

71 return string 

72 cut_how_much = len(typed)-3 

73 if cut_how_much < 7: 

74 return string 

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

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

77 return string 

78 

79def _elide(string:str, typed:str, min_elide=30)->str: 

80 return _elide_typed( 

81 _elide_point(string, min_elide=min_elide), 

82 typed, min_elide=min_elide) 

83 

84 

85 

86def _adjust_completion_text_based_on_context(text, body, offset): 

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

88 return text[:-1] 

89 else: 

90 return text 

91 

92 

93class IPythonPTCompleter(Completer): 

94 """Adaptor to provide IPython completions to prompt_toolkit""" 

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

96 if shell is None and ipy_completer is None: 

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

98 self._ipy_completer = ipy_completer 

99 self.shell = shell 

100 

101 @property 

102 def ipy_completer(self): 

103 if self._ipy_completer: 

104 return self._ipy_completer 

105 else: 

106 return self.shell.Completer 

107 

108 def get_completions(self, document, complete_event): 

109 if not document.current_line.strip(): 

110 return 

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

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

113 # the prompt. 

114 

115 with patch_stdout(), provisionalcompleter(): 

116 body = document.text 

117 cursor_row = document.cursor_position_row 

118 cursor_col = document.cursor_position_col 

119 cursor_position = document.cursor_position 

120 offset = cursor_to_position(body, cursor_row, cursor_col) 

121 try: 

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

123 except Exception as e: 

124 try: 

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

126 traceback.print_exception(exc_type, exc_value, exc_tb) 

127 except AttributeError: 

128 print('Unrecoverable Error in completions') 

129 

130 @staticmethod 

131 def _get_completions(body, offset, cursor_position, ipyc): 

132 """ 

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

134 """ 

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

136 completions = _deduplicate_completions( 

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

138 for c in completions: 

139 if not c.text: 

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

141 continue 

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

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

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

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

146 # character. 

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

148 if cursor_position + c.start > 0: 

149 char_before = body[c.start - 1] 

150 fixed_text = unicodedata.normalize( 

151 'NFC', char_before + text) 

152 

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

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

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

156 continue 

157 

158 # TODO: Use Jedi to determine meta_text 

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

160 # meta_text = '' 

161 # yield Completion(m, start_position=start_pos, 

162 # display_meta=meta_text) 

163 display_text = c.text 

164 

165 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset) 

166 if c.type == 'function': 

167 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()', body[c.start:c.end]), display_meta=c.type+c.signature) 

168 else: 

169 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text, body[c.start:c.end]), display_meta=c.type) 

170 

171class IPythonPTLexer(Lexer): 

172 """ 

173 Wrapper around PythonLexer and BashLexer. 

174 """ 

175 def __init__(self): 

176 l = pygments_lexers 

177 self.python_lexer = PygmentsLexer(l.Python3Lexer) 

178 self.shell_lexer = PygmentsLexer(l.BashLexer) 

179 

180 self.magic_lexers = { 

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

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

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

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

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

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

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

188 } 

189 

190 def lex_document(self, document): 

191 text = document.text.lstrip() 

192 

193 lexer = self.python_lexer 

194 

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

196 lexer = self.shell_lexer 

197 

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

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

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

201 lexer = l 

202 break 

203 

204 return lexer.lex_document(document)