PageRenderTime 50ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/IPython/terminal/interactiveshell.py

https://github.com/minrk/ipython
Python | 646 lines | 618 code | 23 blank | 5 comment | 25 complexity | 4842a9d1a50e377512c87cbdee02676f MD5 | raw file
Possible License(s): BSD-3-Clause
  1. """IPython terminal interface using prompt_toolkit"""
  2. import asyncio
  3. import os
  4. import sys
  5. import warnings
  6. from warnings import warn
  7. from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
  8. from IPython.utils import io
  9. from IPython.utils.py3compat import input
  10. from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
  11. from IPython.utils.process import abbrev_cwd
  12. from traitlets import (
  13. Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union,
  14. Any, validate
  15. )
  16. from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
  17. from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
  18. from prompt_toolkit.formatted_text import PygmentsTokens
  19. from prompt_toolkit.history import InMemoryHistory
  20. from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
  21. from prompt_toolkit.output import ColorDepth
  22. from prompt_toolkit.patch_stdout import patch_stdout
  23. from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
  24. from prompt_toolkit.styles import DynamicStyle, merge_styles
  25. from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
  26. from prompt_toolkit import __version__ as ptk_version
  27. from pygments.styles import get_style_by_name
  28. from pygments.style import Style
  29. from pygments.token import Token
  30. from .debugger import TerminalPdb, Pdb
  31. from .magics import TerminalMagics
  32. from .pt_inputhooks import get_inputhook_name_and_func
  33. from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
  34. from .ptutils import IPythonPTCompleter, IPythonPTLexer
  35. from .shortcuts import create_ipython_shortcuts
  36. DISPLAY_BANNER_DEPRECATED = object()
  37. PTK3 = ptk_version.startswith('3.')
  38. class _NoStyle(Style): pass
  39. _style_overrides_light_bg = {
  40. Token.Prompt: '#ansibrightblue',
  41. Token.PromptNum: '#ansiblue bold',
  42. Token.OutPrompt: '#ansibrightred',
  43. Token.OutPromptNum: '#ansired bold',
  44. }
  45. _style_overrides_linux = {
  46. Token.Prompt: '#ansibrightgreen',
  47. Token.PromptNum: '#ansigreen bold',
  48. Token.OutPrompt: '#ansibrightred',
  49. Token.OutPromptNum: '#ansired bold',
  50. }
  51. def get_default_editor():
  52. try:
  53. return os.environ['EDITOR']
  54. except KeyError:
  55. pass
  56. except UnicodeError:
  57. warn("$EDITOR environment variable is not pure ASCII. Using platform "
  58. "default editor.")
  59. if os.name == 'posix':
  60. return 'vi' # the only one guaranteed to be there!
  61. else:
  62. return 'notepad' # same in Windows!
  63. # conservatively check for tty
  64. # overridden streams can result in things like:
  65. # - sys.stdin = None
  66. # - no isatty method
  67. for _name in ('stdin', 'stdout', 'stderr'):
  68. _stream = getattr(sys, _name)
  69. if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
  70. _is_tty = False
  71. break
  72. else:
  73. _is_tty = True
  74. _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
  75. def black_reformat_handler(text_before_cursor):
  76. import black
  77. formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
  78. if not text_before_cursor.endswith('\n') and formatted_text.endswith('\n'):
  79. formatted_text = formatted_text[:-1]
  80. return formatted_text
  81. class TerminalInteractiveShell(InteractiveShell):
  82. mime_renderers = Dict().tag(config=True)
  83. space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
  84. 'to reserve for the tab completion menu, '
  85. 'search history, ...etc, the height of '
  86. 'these menus will at most this value. '
  87. 'Increase it is you prefer long and skinny '
  88. 'menus, decrease for short and wide.'
  89. ).tag(config=True)
  90. pt_app = None
  91. debugger_history = None
  92. simple_prompt = Bool(_use_simple_prompt,
  93. help="""Use `raw_input` for the REPL, without completion and prompt colors.
  94. Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
  95. IPython own testing machinery, and emacs inferior-shell integration through elpy.
  96. This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
  97. environment variable is set, or the current terminal is not a tty."""
  98. ).tag(config=True)
  99. @property
  100. def debugger_cls(self):
  101. return Pdb if self.simple_prompt else TerminalPdb
  102. confirm_exit = Bool(True,
  103. help="""
  104. Set to confirm when you try to exit IPython with an EOF (Control-D
  105. in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
  106. you can force a direct exit without any confirmation.""",
  107. ).tag(config=True)
  108. editing_mode = Unicode('emacs',
  109. help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
  110. ).tag(config=True)
  111. autoformatter = Unicode(None,
  112. help="Autoformatter to reformat Terminal code. Can be `'black'` or `None`",
  113. allow_none=True
  114. ).tag(config=True)
  115. mouse_support = Bool(False,
  116. help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
  117. ).tag(config=True)
  118. # We don't load the list of styles for the help string, because loading
  119. # Pygments plugins takes time and can cause unexpected errors.
  120. highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
  121. help="""The name or class of a Pygments style to use for syntax
  122. highlighting. To see available styles, run `pygmentize -L styles`."""
  123. ).tag(config=True)
  124. @validate('editing_mode')
  125. def _validate_editing_mode(self, proposal):
  126. if proposal['value'].lower() == 'vim':
  127. proposal['value']= 'vi'
  128. elif proposal['value'].lower() == 'default':
  129. proposal['value']= 'emacs'
  130. if hasattr(EditingMode, proposal['value'].upper()):
  131. return proposal['value'].lower()
  132. return self.editing_mode
  133. @observe('editing_mode')
  134. def _editing_mode(self, change):
  135. u_mode = change.new.upper()
  136. if self.pt_app:
  137. self.pt_app.editing_mode = u_mode
  138. @observe('autoformatter')
  139. def _autoformatter_changed(self, change):
  140. formatter = change.new
  141. if formatter is None:
  142. self.reformat_handler = lambda x:x
  143. elif formatter == 'black':
  144. self.reformat_handler = black_reformat_handler
  145. else:
  146. raise ValueError
  147. @observe('highlighting_style')
  148. @observe('colors')
  149. def _highlighting_style_changed(self, change):
  150. self.refresh_style()
  151. def refresh_style(self):
  152. self._style = self._make_style_from_name_or_cls(self.highlighting_style)
  153. highlighting_style_overrides = Dict(
  154. help="Override highlighting format for specific tokens"
  155. ).tag(config=True)
  156. true_color = Bool(False,
  157. help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
  158. "If your terminal supports true color, the following command "
  159. "should print 'TRUECOLOR' in orange: "
  160. "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
  161. ).tag(config=True)
  162. editor = Unicode(get_default_editor(),
  163. help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
  164. ).tag(config=True)
  165. prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
  166. prompts = Instance(Prompts)
  167. @default('prompts')
  168. def _prompts_default(self):
  169. return self.prompts_class(self)
  170. # @observe('prompts')
  171. # def _(self, change):
  172. # self._update_layout()
  173. @default('displayhook_class')
  174. def _displayhook_class_default(self):
  175. return RichPromptDisplayHook
  176. term_title = Bool(True,
  177. help="Automatically set the terminal title"
  178. ).tag(config=True)
  179. term_title_format = Unicode("IPython: {cwd}",
  180. help="Customize the terminal title format. This is a python format string. " +
  181. "Available substitutions are: {cwd}."
  182. ).tag(config=True)
  183. display_completions = Enum(('column', 'multicolumn','readlinelike'),
  184. help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
  185. "'readlinelike'. These options are for `prompt_toolkit`, see "
  186. "`prompt_toolkit` documentation for more information."
  187. ),
  188. default_value='multicolumn').tag(config=True)
  189. highlight_matching_brackets = Bool(True,
  190. help="Highlight matching brackets.",
  191. ).tag(config=True)
  192. extra_open_editor_shortcuts = Bool(False,
  193. help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
  194. "This is in addition to the F2 binding, which is always enabled."
  195. ).tag(config=True)
  196. handle_return = Any(None,
  197. help="Provide an alternative handler to be called when the user presses "
  198. "Return. This is an advanced option intended for debugging, which "
  199. "may be changed or removed in later releases."
  200. ).tag(config=True)
  201. enable_history_search = Bool(True,
  202. help="Allows to enable/disable the prompt toolkit history search"
  203. ).tag(config=True)
  204. prompt_includes_vi_mode = Bool(True,
  205. help="Display the current vi mode (when using vi editing mode)."
  206. ).tag(config=True)
  207. @observe('term_title')
  208. def init_term_title(self, change=None):
  209. # Enable or disable the terminal title.
  210. if self.term_title:
  211. toggle_set_term_title(True)
  212. set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
  213. else:
  214. toggle_set_term_title(False)
  215. def restore_term_title(self):
  216. if self.term_title:
  217. restore_term_title()
  218. def init_display_formatter(self):
  219. super(TerminalInteractiveShell, self).init_display_formatter()
  220. # terminal only supports plain text
  221. self.display_formatter.active_types = ['text/plain']
  222. # disable `_ipython_display_`
  223. self.display_formatter.ipython_display_formatter.enabled = False
  224. def init_prompt_toolkit_cli(self):
  225. if self.simple_prompt:
  226. # Fall back to plain non-interactive output for tests.
  227. # This is very limited.
  228. def prompt():
  229. prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
  230. lines = [input(prompt_text)]
  231. prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
  232. while self.check_complete('\n'.join(lines))[0] == 'incomplete':
  233. lines.append( input(prompt_continuation) )
  234. return '\n'.join(lines)
  235. self.prompt_for_code = prompt
  236. return
  237. # Set up keyboard shortcuts
  238. key_bindings = create_ipython_shortcuts(self)
  239. # Pre-populate history from IPython's history database
  240. history = InMemoryHistory()
  241. last_cell = u""
  242. for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
  243. include_latest=True):
  244. # Ignore blank lines and consecutive duplicates
  245. cell = cell.rstrip()
  246. if cell and (cell != last_cell):
  247. history.append_string(cell)
  248. last_cell = cell
  249. self._style = self._make_style_from_name_or_cls(self.highlighting_style)
  250. self.style = DynamicStyle(lambda: self._style)
  251. editing_mode = getattr(EditingMode, self.editing_mode.upper())
  252. self.pt_loop = asyncio.new_event_loop()
  253. self.pt_app = PromptSession(
  254. editing_mode=editing_mode,
  255. key_bindings=key_bindings,
  256. history=history,
  257. completer=IPythonPTCompleter(shell=self),
  258. enable_history_search = self.enable_history_search,
  259. style=self.style,
  260. include_default_pygments_style=False,
  261. mouse_support=self.mouse_support,
  262. enable_open_in_editor=self.extra_open_editor_shortcuts,
  263. color_depth=self.color_depth,
  264. tempfile_suffix=".py",
  265. **self._extra_prompt_options())
  266. def _make_style_from_name_or_cls(self, name_or_cls):
  267. """
  268. Small wrapper that make an IPython compatible style from a style name
  269. We need that to add style for prompt ... etc.
  270. """
  271. style_overrides = {}
  272. if name_or_cls == 'legacy':
  273. legacy = self.colors.lower()
  274. if legacy == 'linux':
  275. style_cls = get_style_by_name('monokai')
  276. style_overrides = _style_overrides_linux
  277. elif legacy == 'lightbg':
  278. style_overrides = _style_overrides_light_bg
  279. style_cls = get_style_by_name('pastie')
  280. elif legacy == 'neutral':
  281. # The default theme needs to be visible on both a dark background
  282. # and a light background, because we can't tell what the terminal
  283. # looks like. These tweaks to the default theme help with that.
  284. style_cls = get_style_by_name('default')
  285. style_overrides.update({
  286. Token.Number: '#ansigreen',
  287. Token.Operator: 'noinherit',
  288. Token.String: '#ansiyellow',
  289. Token.Name.Function: '#ansiblue',
  290. Token.Name.Class: 'bold #ansiblue',
  291. Token.Name.Namespace: 'bold #ansiblue',
  292. Token.Name.Variable.Magic: '#ansiblue',
  293. Token.Prompt: '#ansigreen',
  294. Token.PromptNum: '#ansibrightgreen bold',
  295. Token.OutPrompt: '#ansired',
  296. Token.OutPromptNum: '#ansibrightred bold',
  297. })
  298. # Hack: Due to limited color support on the Windows console
  299. # the prompt colors will be wrong without this
  300. if os.name == 'nt':
  301. style_overrides.update({
  302. Token.Prompt: '#ansidarkgreen',
  303. Token.PromptNum: '#ansigreen bold',
  304. Token.OutPrompt: '#ansidarkred',
  305. Token.OutPromptNum: '#ansired bold',
  306. })
  307. elif legacy =='nocolor':
  308. style_cls=_NoStyle
  309. style_overrides = {}
  310. else :
  311. raise ValueError('Got unknown colors: ', legacy)
  312. else :
  313. if isinstance(name_or_cls, str):
  314. style_cls = get_style_by_name(name_or_cls)
  315. else:
  316. style_cls = name_or_cls
  317. style_overrides = {
  318. Token.Prompt: '#ansigreen',
  319. Token.PromptNum: '#ansibrightgreen bold',
  320. Token.OutPrompt: '#ansired',
  321. Token.OutPromptNum: '#ansibrightred bold',
  322. }
  323. style_overrides.update(self.highlighting_style_overrides)
  324. style = merge_styles([
  325. style_from_pygments_cls(style_cls),
  326. style_from_pygments_dict(style_overrides),
  327. ])
  328. return style
  329. @property
  330. def pt_complete_style(self):
  331. return {
  332. 'multicolumn': CompleteStyle.MULTI_COLUMN,
  333. 'column': CompleteStyle.COLUMN,
  334. 'readlinelike': CompleteStyle.READLINE_LIKE,
  335. }[self.display_completions]
  336. @property
  337. def color_depth(self):
  338. return (ColorDepth.TRUE_COLOR if self.true_color else None)
  339. def _extra_prompt_options(self):
  340. """
  341. Return the current layout option for the current Terminal InteractiveShell
  342. """
  343. def get_message():
  344. return PygmentsTokens(self.prompts.in_prompt_tokens())
  345. if self.editing_mode == 'emacs':
  346. # with emacs mode the prompt is (usually) static, so we call only
  347. # the function once. With VI mode it can toggle between [ins] and
  348. # [nor] so we can't precompute.
  349. # here I'm going to favor the default keybinding which almost
  350. # everybody uses to decrease CPU usage.
  351. # if we have issues with users with custom Prompts we can see how to
  352. # work around this.
  353. get_message = get_message()
  354. options = {
  355. 'complete_in_thread': False,
  356. 'lexer':IPythonPTLexer(),
  357. 'reserve_space_for_menu':self.space_for_menu,
  358. 'message': get_message,
  359. 'prompt_continuation': (
  360. lambda width, lineno, is_soft_wrap:
  361. PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
  362. 'multiline': True,
  363. 'complete_style': self.pt_complete_style,
  364. # Highlight matching brackets, but only when this setting is
  365. # enabled, and only when the DEFAULT_BUFFER has the focus.
  366. 'input_processors': [ConditionalProcessor(
  367. processor=HighlightMatchingBracketProcessor(chars='[](){}'),
  368. filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
  369. Condition(lambda: self.highlight_matching_brackets))],
  370. }
  371. if not PTK3:
  372. options['inputhook'] = self.inputhook
  373. return options
  374. def prompt_for_code(self):
  375. if self.rl_next_input:
  376. default = self.rl_next_input
  377. self.rl_next_input = None
  378. else:
  379. default = ''
  380. # In order to make sure that asyncio code written in the
  381. # interactive shell doesn't interfere with the prompt, we run the
  382. # prompt in a different event loop.
  383. # If we don't do this, people could spawn coroutine with a
  384. # while/true inside which will freeze the prompt.
  385. try:
  386. old_loop = asyncio.get_event_loop()
  387. except RuntimeError:
  388. # This happens when the user used `asyncio.run()`.
  389. old_loop = None
  390. asyncio.set_event_loop(self.pt_loop)
  391. try:
  392. with patch_stdout(raw=True):
  393. text = self.pt_app.prompt(
  394. default=default,
  395. **self._extra_prompt_options())
  396. finally:
  397. # Restore the original event loop.
  398. asyncio.set_event_loop(old_loop)
  399. return text
  400. def enable_win_unicode_console(self):
  401. # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
  402. # console by default, so WUC shouldn't be needed.
  403. from warnings import warn
  404. warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
  405. DeprecationWarning,
  406. stacklevel=2)
  407. def init_io(self):
  408. if sys.platform not in {'win32', 'cli'}:
  409. return
  410. import colorama
  411. colorama.init()
  412. # For some reason we make these wrappers around stdout/stderr.
  413. # For now, we need to reset them so all output gets coloured.
  414. # https://github.com/ipython/ipython/issues/8669
  415. # io.std* are deprecated, but don't show our own deprecation warnings
  416. # during initialization of the deprecated API.
  417. with warnings.catch_warnings():
  418. warnings.simplefilter('ignore', DeprecationWarning)
  419. io.stdout = io.IOStream(sys.stdout)
  420. io.stderr = io.IOStream(sys.stderr)
  421. def init_magics(self):
  422. super(TerminalInteractiveShell, self).init_magics()
  423. self.register_magics(TerminalMagics)
  424. def init_alias(self):
  425. # The parent class defines aliases that can be safely used with any
  426. # frontend.
  427. super(TerminalInteractiveShell, self).init_alias()
  428. # Now define aliases that only make sense on the terminal, because they
  429. # need direct access to the console in a way that we can't emulate in
  430. # GUI or web frontend
  431. if os.name == 'posix':
  432. for cmd in ('clear', 'more', 'less', 'man'):
  433. self.alias_manager.soft_define_alias(cmd, cmd)
  434. def __init__(self, *args, **kwargs):
  435. super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
  436. self.init_prompt_toolkit_cli()
  437. self.init_term_title()
  438. self.keep_running = True
  439. self.debugger_history = InMemoryHistory()
  440. def ask_exit(self):
  441. self.keep_running = False
  442. rl_next_input = None
  443. def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
  444. if display_banner is not DISPLAY_BANNER_DEPRECATED:
  445. warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
  446. self.keep_running = True
  447. while self.keep_running:
  448. print(self.separate_in, end='')
  449. try:
  450. code = self.prompt_for_code()
  451. except EOFError:
  452. if (not self.confirm_exit) \
  453. or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
  454. self.ask_exit()
  455. else:
  456. if code:
  457. self.run_cell(code, store_history=True)
  458. def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
  459. # An extra layer of protection in case someone mashing Ctrl-C breaks
  460. # out of our internal code.
  461. if display_banner is not DISPLAY_BANNER_DEPRECATED:
  462. warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
  463. while True:
  464. try:
  465. self.interact()
  466. break
  467. except KeyboardInterrupt as e:
  468. print("\n%s escaped interact()\n" % type(e).__name__)
  469. finally:
  470. # An interrupt during the eventloop will mess up the
  471. # internal state of the prompt_toolkit library.
  472. # Stopping the eventloop fixes this, see
  473. # https://github.com/ipython/ipython/pull/9867
  474. if hasattr(self, '_eventloop'):
  475. self._eventloop.stop()
  476. self.restore_term_title()
  477. _inputhook = None
  478. def inputhook(self, context):
  479. if self._inputhook is not None:
  480. self._inputhook(context)
  481. active_eventloop = None
  482. def enable_gui(self, gui=None):
  483. if gui and (gui != 'inline') :
  484. self.active_eventloop, self._inputhook =\
  485. get_inputhook_name_and_func(gui)
  486. else:
  487. self.active_eventloop = self._inputhook = None
  488. # For prompt_toolkit 3.0. We have to create an asyncio event loop with
  489. # this inputhook.
  490. if PTK3:
  491. import asyncio
  492. from prompt_toolkit.eventloop import new_eventloop_with_inputhook
  493. if gui == 'asyncio':
  494. # When we integrate the asyncio event loop, run the UI in the
  495. # same event loop as the rest of the code. don't use an actual
  496. # input hook. (Asyncio is not made for nesting event loops.)
  497. self.pt_loop = asyncio.get_event_loop()
  498. elif self._inputhook:
  499. # If an inputhook was set, create a new asyncio event loop with
  500. # this inputhook for the prompt.
  501. self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
  502. else:
  503. # When there's no inputhook, run the prompt in a separate
  504. # asyncio event loop.
  505. self.pt_loop = asyncio.new_event_loop()
  506. # Run !system commands directly, not through pipes, so terminal programs
  507. # work correctly.
  508. system = InteractiveShell.system_raw
  509. def auto_rewrite_input(self, cmd):
  510. """Overridden from the parent class to use fancy rewriting prompt"""
  511. if not self.show_rewritten_input:
  512. return
  513. tokens = self.prompts.rewrite_prompt_tokens()
  514. if self.pt_app:
  515. print_formatted_text(PygmentsTokens(tokens), end='',
  516. style=self.pt_app.app.style)
  517. print(cmd)
  518. else:
  519. prompt = ''.join(s for t, s in tokens)
  520. print(prompt, cmd, sep='')
  521. _prompts_before = None
  522. def switch_doctest_mode(self, mode):
  523. """Switch prompts to classic for %doctest_mode"""
  524. if mode:
  525. self._prompts_before = self.prompts
  526. self.prompts = ClassicPrompts(self)
  527. elif self._prompts_before:
  528. self.prompts = self._prompts_before
  529. self._prompts_before = None
  530. # self._update_layout()
  531. InteractiveShellABC.register(TerminalInteractiveShell)
  532. if __name__ == '__main__':
  533. TerminalInteractiveShell.instance().interact()