##// END OF EJS Templates
Simplify code with a context manager
Thomas Kluyver -
Show More
@@ -1,390 +1,387 b''
1 1 """IPython terminal interface using prompt_toolkit in place of readline"""
2 2 from __future__ import print_function
3 3
4 4 import os
5 5 import sys
6 6 import signal
7 7 from warnings import warn
8 8
9 9 from IPython.core.error import TryNext
10 10 from IPython.core.interactiveshell import InteractiveShell
11 11 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
12 12 from IPython.utils.terminal import toggle_set_term_title, set_term_title
13 13 from IPython.utils.process import abbrev_cwd
14 14 from traitlets import Bool, CBool, Unicode, Dict
15 15
16 16 from prompt_toolkit.completion import Completer, Completion
17 17 from prompt_toolkit.enums import DEFAULT_BUFFER
18 18 from prompt_toolkit.filters import HasFocus, HasSelection, Condition
19 19 from prompt_toolkit.history import InMemoryHistory
20 20 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
21 21 from prompt_toolkit.interface import CommandLineInterface
22 22 from prompt_toolkit.key_binding.manager import KeyBindingManager
23 23 from prompt_toolkit.key_binding.vi_state import InputMode
24 24 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
25 25 from prompt_toolkit.keys import Keys
26 26 from prompt_toolkit.layout.lexers import Lexer
27 27 from prompt_toolkit.layout.lexers import PygmentsLexer
28 28 from prompt_toolkit.styles import PygmentsStyle
29 29
30 30 from pygments.styles import get_style_by_name
31 31 from pygments.lexers import Python3Lexer, BashLexer, PythonLexer
32 32 from pygments.token import Token
33 33
34 34 from .pt_inputhooks import get_inputhook_func
35 35 from .interactiveshell import get_default_editor, TerminalMagics
36 36
37 37
38 38
39 39 class IPythonPTCompleter(Completer):
40 40 """Adaptor to provide IPython completions to prompt_toolkit"""
41 41 def __init__(self, ipy_completer):
42 42 self.ipy_completer = ipy_completer
43 43
44 44 def get_completions(self, document, complete_event):
45 45 if not document.current_line.strip():
46 46 return
47 47
48 48 used, matches = self.ipy_completer.complete(
49 49 line_buffer=document.current_line,
50 50 cursor_pos=document.cursor_position_col
51 51 )
52 52 start_pos = -len(used)
53 53 for m in matches:
54 54 yield Completion(m, start_position=start_pos)
55 55
56 56
57 57 class IPythonPTLexer(Lexer):
58 58 """
59 59 Wrapper around PythonLexer and BashLexer.
60 60 """
61 61 def __init__(self):
62 62 self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer)
63 63 self.shell_lexer = PygmentsLexer(BashLexer)
64 64
65 65 def lex_document(self, cli, document):
66 66 if document.text.startswith('!'):
67 67 return self.shell_lexer.lex_document(cli, document)
68 68 else:
69 69 return self.python_lexer.lex_document(cli, document)
70 70
71 71
72 72 class TerminalInteractiveShell(InteractiveShell):
73 73 colors_force = True
74 74
75 75 pt_cli = None
76 76
77 77 autoedit_syntax = CBool(False, config=True,
78 78 help="auto editing of files with syntax errors.")
79 79
80 80 confirm_exit = CBool(True, config=True,
81 81 help="""
82 82 Set to confirm when you try to exit IPython with an EOF (Control-D
83 83 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
84 84 you can force a direct exit without any confirmation.""",
85 85 )
86 86 vi_mode = Bool(False, config=True,
87 87 help="Use vi style keybindings at the prompt",
88 88 )
89 89
90 90 mouse_support = Bool(False, config=True,
91 91 help="Enable mouse support in the prompt"
92 92 )
93 93
94 94 highlighting_style = Unicode('', config=True,
95 95 help="The name of a Pygments style to use for syntax highlighting"
96 96 )
97 97
98 98 highlighting_style_overrides = Dict(config=True,
99 99 help="Override highlighting format for specific tokens"
100 100 )
101 101
102 102 editor = Unicode(get_default_editor(), config=True,
103 103 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
104 104 )
105 105
106 106 term_title = Bool(True, config=True,
107 107 help="Automatically set the terminal title"
108 108 )
109 109 def _term_title_changed(self, name, new_value):
110 110 self.init_term_title()
111 111
112 112 def init_term_title(self):
113 113 # Enable or disable the terminal title.
114 114 if self.term_title:
115 115 toggle_set_term_title(True)
116 116 set_term_title('IPython: ' + abbrev_cwd())
117 117 else:
118 118 toggle_set_term_title(False)
119 119
120 120 def get_prompt_tokens(self, cli):
121 121 return [
122 122 (Token.Prompt, 'In ['),
123 123 (Token.PromptNum, str(self.execution_count)),
124 124 (Token.Prompt, ']: '),
125 125 ]
126 126
127 127 def get_continuation_tokens(self, cli, width):
128 128 return [
129 129 (Token.Prompt, (' ' * (width - 5)) + '...: '),
130 130 ]
131 131
132 132 def init_prompt_toolkit_cli(self):
133 133 if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty():
134 134 # Fall back to plain non-interactive output for tests.
135 135 # This is very limited, and only accepts a single line.
136 136 def prompt():
137 137 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
138 138 self.prompt_for_code = prompt
139 139 return
140 140
141 141 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
142 142 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
143 143 # Ctrl+J == Enter, seemingly
144 144 @kbmanager.registry.add_binding(Keys.ControlJ,
145 145 filter=(HasFocus(DEFAULT_BUFFER)
146 146 & ~HasSelection()
147 147 & insert_mode
148 148 ))
149 149 def _(event):
150 150 b = event.current_buffer
151 151 d = b.document
152 152 if not (d.on_last_line or d.cursor_position_row >= d.line_count
153 153 - d.empty_line_count_at_the_end()):
154 154 b.newline()
155 155 return
156 156
157 157 status, indent = self.input_splitter.check_complete(d.text)
158 158
159 159 if (status != 'incomplete') and b.accept_action.is_returnable:
160 160 b.accept_action.validate_and_handle(event.cli, b)
161 161 else:
162 162 b.insert_text('\n' + (' ' * (indent or 0)))
163 163
164 164 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER))
165 165 def _(event):
166 166 event.current_buffer.reset()
167 167
168 168 supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP'))
169 169
170 170 @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend)
171 171 def _(event):
172 172 event.cli.suspend_to_background()
173 173
174 174 @Condition
175 175 def cursor_in_leading_ws(cli):
176 176 before = cli.application.buffer.document.current_line_before_cursor
177 177 return (not before) or before.isspace()
178 178
179 179 # Ctrl+I == Tab
180 180 @kbmanager.registry.add_binding(Keys.ControlI,
181 181 filter=(HasFocus(DEFAULT_BUFFER)
182 182 & ~HasSelection()
183 183 & insert_mode
184 184 & cursor_in_leading_ws
185 185 ))
186 186 def _(event):
187 187 event.current_buffer.insert_text(' ' * 4)
188 188
189 189 # Pre-populate history from IPython's history database
190 190 history = InMemoryHistory()
191 191 last_cell = u""
192 192 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
193 193 include_latest=True):
194 194 # Ignore blank lines and consecutive duplicates
195 195 cell = cell.rstrip()
196 196 if cell and (cell != last_cell):
197 197 history.append(cell)
198 198
199 199 style_overrides = {
200 200 Token.Prompt: '#009900',
201 201 Token.PromptNum: '#00ff00 bold',
202 202 }
203 203 if self.highlighting_style:
204 204 style_cls = get_style_by_name(self.highlighting_style)
205 205 else:
206 206 style_cls = get_style_by_name('default')
207 207 # The default theme needs to be visible on both a dark background
208 208 # and a light background, because we can't tell what the terminal
209 209 # looks like. These tweaks to the default theme help with that.
210 210 style_overrides.update({
211 211 Token.Number: '#007700',
212 212 Token.Operator: 'noinherit',
213 213 Token.String: '#BB6622',
214 214 Token.Name.Function: '#2080D0',
215 215 Token.Name.Class: 'bold #2080D0',
216 216 Token.Name.Namespace: 'bold #2080D0',
217 217 })
218 218 style_overrides.update(self.highlighting_style_overrides)
219 219 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
220 220 style_dict=style_overrides)
221 221
222 222 app = create_prompt_application(multiline=True,
223 223 lexer=IPythonPTLexer(),
224 224 get_prompt_tokens=self.get_prompt_tokens,
225 225 get_continuation_tokens=self.get_continuation_tokens,
226 226 key_bindings_registry=kbmanager.registry,
227 227 history=history,
228 228 completer=IPythonPTCompleter(self.Completer),
229 229 enable_history_search=True,
230 230 style=style,
231 231 mouse_support=self.mouse_support,
232 232 reserve_space_for_menu=6,
233 233 )
234 234
235 235 self.pt_cli = CommandLineInterface(app,
236 236 eventloop=create_eventloop(self.inputhook))
237 237
238 238 def prompt_for_code(self):
239 239 document = self.pt_cli.run(pre_run=self.pre_prompt)
240 240 return document.text
241 241
242 242 def init_io(self):
243 243 if sys.platform not in {'win32', 'cli'}:
244 244 return
245 245
246 246 import colorama
247 247 colorama.init()
248 248
249 249 # For some reason we make these wrappers around stdout/stderr.
250 250 # For now, we need to reset them so all output gets coloured.
251 251 # https://github.com/ipython/ipython/issues/8669
252 252 from IPython.utils import io
253 253 io.stdout = io.IOStream(sys.stdout)
254 254 io.stderr = io.IOStream(sys.stderr)
255 255
256 256 def init_magics(self):
257 257 super(TerminalInteractiveShell, self).init_magics()
258 258 self.register_magics(TerminalMagics)
259 259
260 260 def init_alias(self):
261 261 # The parent class defines aliases that can be safely used with any
262 262 # frontend.
263 263 super(TerminalInteractiveShell, self).init_alias()
264 264
265 265 # Now define aliases that only make sense on the terminal, because they
266 266 # need direct access to the console in a way that we can't emulate in
267 267 # GUI or web frontend
268 268 if os.name == 'posix':
269 269 for cmd in ['clear', 'more', 'less', 'man']:
270 270 self.alias_manager.soft_define_alias(cmd, cmd)
271 271
272 272
273 273 def __init__(self, *args, **kwargs):
274 274 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
275 275 self.init_prompt_toolkit_cli()
276 276 self.init_term_title()
277 277 self.keep_running = True
278 278
279 279 def ask_exit(self):
280 280 self.keep_running = False
281 281
282 282 rl_next_input = None
283 283
284 284 def pre_prompt(self):
285 285 if self.rl_next_input:
286 286 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
287 287 self.rl_next_input = None
288 288
289 289 def interact(self):
290 290 while self.keep_running:
291 291 print(self.separate_in, end='')
292 292
293 293 try:
294 294 code = self.prompt_for_code()
295 295 except EOFError:
296 296 if (not self.confirm_exit) \
297 297 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
298 298 self.ask_exit()
299 299
300 300 else:
301 301 if code:
302 302 self.run_cell(code, store_history=True)
303 303 if self.autoedit_syntax and self.SyntaxTB.last_syntax_error:
304 304 self.edit_syntax_error()
305 305
306 306 def mainloop(self):
307 307 # An extra layer of protection in case someone mashing Ctrl-C breaks
308 308 # out of our internal code.
309 309 while True:
310 310 try:
311 311 self.interact()
312 312 break
313 313 except KeyboardInterrupt:
314 314 print("\nKeyboardInterrupt escaped interact()\n")
315 315
316 316 _inputhook = None
317 317 def inputhook(self, context):
318 318 if self._inputhook is not None:
319 319 self._inputhook(context)
320 320
321 321 def enable_gui(self, gui=None):
322 322 if gui:
323 323 self._inputhook = get_inputhook_func(gui)
324 324 else:
325 325 self._inputhook = None
326 326
327 327 # Methods to support auto-editing of SyntaxErrors:
328 328
329 329 def edit_syntax_error(self):
330 330 """The bottom half of the syntax error handler called in the main loop.
331 331
332 332 Loop until syntax error is fixed or user cancels.
333 333 """
334 334
335 335 while self.SyntaxTB.last_syntax_error:
336 336 # copy and clear last_syntax_error
337 337 err = self.SyntaxTB.clear_err_state()
338 338 if not self._should_recompile(err):
339 339 return
340 340 try:
341 341 # may set last_syntax_error again if a SyntaxError is raised
342 342 self.safe_execfile(err.filename, self.user_ns)
343 343 except:
344 344 self.showtraceback()
345 345 else:
346 346 try:
347 f = open(err.filename)
348 try:
347 with open(err.filename) as f:
349 348 # This should be inside a display_trap block and I
350 349 # think it is.
351 350 sys.displayhook(f.read())
352 finally:
353 f.close()
354 351 except:
355 352 self.showtraceback()
356 353
357 354 def _should_recompile(self, e):
358 355 """Utility routine for edit_syntax_error"""
359 356
360 357 if e.filename in ('<ipython console>', '<input>', '<string>',
361 358 '<console>', '<BackgroundJob compilation>',
362 359 None):
363 360 return False
364 361 try:
365 362 if (self.autoedit_syntax and
366 363 not self.ask_yes_no(
367 364 'Return to editor to correct syntax error? '
368 365 '[Y/n] ', 'y')):
369 366 return False
370 367 except EOFError:
371 368 return False
372 369
373 370 def int0(x):
374 371 try:
375 372 return int(x)
376 373 except TypeError:
377 374 return 0
378 375
379 376 # always pass integer line and offset values to editor hook
380 377 try:
381 378 self.hooks.fix_error_editor(e.filename,
382 379 int0(e.lineno), int0(e.offset),
383 380 e.msg)
384 381 except TryNext:
385 382 warn('Could not open editor')
386 383 return False
387 384 return True
388 385
389 386 if __name__ == '__main__':
390 387 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now