interactiveshell.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. """IPython terminal interface using prompt_toolkit"""
  2. from __future__ import print_function
  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 PY3, cast_unicode_py2, input, string_types
  10. from IPython.utils.terminal import toggle_set_term_title, set_term_title
  11. from IPython.utils.process import abbrev_cwd
  12. from traitlets import Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union
  13. from prompt_toolkit.document import Document
  14. from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
  15. from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
  16. from prompt_toolkit.history import InMemoryHistory
  17. from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout, create_output
  18. from prompt_toolkit.interface import CommandLineInterface
  19. from prompt_toolkit.key_binding.manager import KeyBindingManager
  20. from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
  21. from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
  22. from pygments.styles import get_style_by_name
  23. from pygments.style import Style
  24. from pygments.token import Token
  25. from .debugger import TerminalPdb, Pdb
  26. from .magics import TerminalMagics
  27. from .pt_inputhooks import get_inputhook_name_and_func
  28. from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
  29. from .ptutils import IPythonPTCompleter, IPythonPTLexer
  30. from .shortcuts import register_ipython_shortcuts
  31. DISPLAY_BANNER_DEPRECATED = object()
  32. from pygments.style import Style
  33. class _NoStyle(Style): pass
  34. _style_overrides_light_bg = {
  35. Token.Prompt: '#0000ff',
  36. Token.PromptNum: '#0000ee bold',
  37. Token.OutPrompt: '#cc0000',
  38. Token.OutPromptNum: '#bb0000 bold',
  39. }
  40. _style_overrides_linux = {
  41. Token.Prompt: '#00cc00',
  42. Token.PromptNum: '#00bb00 bold',
  43. Token.OutPrompt: '#cc0000',
  44. Token.OutPromptNum: '#bb0000 bold',
  45. }
  46. def get_default_editor():
  47. try:
  48. ed = os.environ['EDITOR']
  49. if not PY3:
  50. ed = ed.decode()
  51. return ed
  52. except KeyError:
  53. pass
  54. except UnicodeError:
  55. warn("$EDITOR environment variable is not pure ASCII. Using platform "
  56. "default editor.")
  57. if os.name == 'posix':
  58. return 'vi' # the only one guaranteed to be there!
  59. else:
  60. return 'notepad' # same in Windows!
  61. # conservatively check for tty
  62. # overridden streams can result in things like:
  63. # - sys.stdin = None
  64. # - no isatty method
  65. for _name in ('stdin', 'stdout', 'stderr'):
  66. _stream = getattr(sys, _name)
  67. if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
  68. _is_tty = False
  69. break
  70. else:
  71. _is_tty = True
  72. _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
  73. class TerminalInteractiveShell(InteractiveShell):
  74. space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
  75. 'to reserve for the completion menu'
  76. ).tag(config=True)
  77. def _space_for_menu_changed(self, old, new):
  78. self._update_layout()
  79. pt_cli = None
  80. debugger_history = None
  81. _pt_app = None
  82. simple_prompt = Bool(_use_simple_prompt,
  83. help="""Use `raw_input` for the REPL, without completion and prompt colors.
  84. Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
  85. IPython own testing machinery, and emacs inferior-shell integration through elpy.
  86. This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
  87. environment variable is set, or the current terminal is not a tty.
  88. """
  89. ).tag(config=True)
  90. @property
  91. def debugger_cls(self):
  92. return Pdb if self.simple_prompt else TerminalPdb
  93. confirm_exit = Bool(True,
  94. help="""
  95. Set to confirm when you try to exit IPython with an EOF (Control-D
  96. in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
  97. you can force a direct exit without any confirmation.""",
  98. ).tag(config=True)
  99. editing_mode = Unicode('emacs',
  100. help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
  101. ).tag(config=True)
  102. mouse_support = Bool(False,
  103. help="Enable mouse support in the prompt"
  104. ).tag(config=True)
  105. # We don't load the list of styles for the help string, because loading
  106. # Pygments plugins takes time and can cause unexpected errors.
  107. highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
  108. help="""The name or class of a Pygments style to use for syntax
  109. highlighting. To see available styles, run `pygmentize -L styles`."""
  110. ).tag(config=True)
  111. @observe('highlighting_style')
  112. @observe('colors')
  113. def _highlighting_style_changed(self, change):
  114. self.refresh_style()
  115. def refresh_style(self):
  116. self._style = self._make_style_from_name_or_cls(self.highlighting_style)
  117. highlighting_style_overrides = Dict(
  118. help="Override highlighting format for specific tokens"
  119. ).tag(config=True)
  120. true_color = Bool(False,
  121. help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
  122. "If your terminal supports true color, the following command "
  123. "should print 'TRUECOLOR' in orange: "
  124. "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
  125. ).tag(config=True)
  126. editor = Unicode(get_default_editor(),
  127. help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
  128. ).tag(config=True)
  129. prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
  130. prompts = Instance(Prompts)
  131. @default('prompts')
  132. def _prompts_default(self):
  133. return self.prompts_class(self)
  134. @observe('prompts')
  135. def _(self, change):
  136. self._update_layout()
  137. @default('displayhook_class')
  138. def _displayhook_class_default(self):
  139. return RichPromptDisplayHook
  140. term_title = Bool(True,
  141. help="Automatically set the terminal title"
  142. ).tag(config=True)
  143. display_completions = Enum(('column', 'multicolumn','readlinelike'),
  144. help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
  145. "'readlinelike'. These options are for `prompt_toolkit`, see "
  146. "`prompt_toolkit` documentation for more information."
  147. ),
  148. default_value='multicolumn').tag(config=True)
  149. highlight_matching_brackets = Bool(True,
  150. help="Highlight matching brackets.",
  151. ).tag(config=True)
  152. extra_open_editor_shortcuts = Bool(False,
  153. help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
  154. "This is in addition to the F2 binding, which is always enabled."
  155. ).tag(config=True)
  156. @observe('term_title')
  157. def init_term_title(self, change=None):
  158. # Enable or disable the terminal title.
  159. if self.term_title:
  160. toggle_set_term_title(True)
  161. set_term_title('IPython: ' + abbrev_cwd())
  162. else:
  163. toggle_set_term_title(False)
  164. def init_display_formatter(self):
  165. super(TerminalInteractiveShell, self).init_display_formatter()
  166. # terminal only supports plain text
  167. self.display_formatter.active_types = ['text/plain']
  168. # disable `_ipython_display_`
  169. self.display_formatter.ipython_display_formatter.enabled = False
  170. def init_prompt_toolkit_cli(self):
  171. if self.simple_prompt:
  172. # Fall back to plain non-interactive output for tests.
  173. # This is very limited, and only accepts a single line.
  174. def prompt():
  175. isp = self.input_splitter
  176. prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
  177. prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
  178. while isp.push_accepts_more():
  179. line = cast_unicode_py2(input(prompt_text))
  180. isp.push(line)
  181. prompt_text = prompt_continuation
  182. return isp.source_reset()
  183. self.prompt_for_code = prompt
  184. return
  185. # Set up keyboard shortcuts
  186. kbmanager = KeyBindingManager.for_prompt(
  187. enable_open_in_editor=self.extra_open_editor_shortcuts,
  188. )
  189. register_ipython_shortcuts(kbmanager.registry, self)
  190. # Pre-populate history from IPython's history database
  191. history = InMemoryHistory()
  192. last_cell = u""
  193. for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
  194. include_latest=True):
  195. # Ignore blank lines and consecutive duplicates
  196. cell = cell.rstrip()
  197. if cell and (cell != last_cell):
  198. history.append(cell)
  199. last_cell = cell
  200. self._style = self._make_style_from_name_or_cls(self.highlighting_style)
  201. self.style = DynamicStyle(lambda: self._style)
  202. editing_mode = getattr(EditingMode, self.editing_mode.upper())
  203. def patch_stdout(**kwargs):
  204. return self.pt_cli.patch_stdout_context(**kwargs)
  205. self._pt_app = create_prompt_application(
  206. editing_mode=editing_mode,
  207. key_bindings_registry=kbmanager.registry,
  208. history=history,
  209. completer=IPythonPTCompleter(shell=self,
  210. patch_stdout=patch_stdout),
  211. enable_history_search=True,
  212. style=self.style,
  213. mouse_support=self.mouse_support,
  214. **self._layout_options()
  215. )
  216. self._eventloop = create_eventloop(self.inputhook)
  217. self.pt_cli = CommandLineInterface(
  218. self._pt_app, eventloop=self._eventloop,
  219. output=create_output(true_color=self.true_color))
  220. def _make_style_from_name_or_cls(self, name_or_cls):
  221. """
  222. Small wrapper that make an IPython compatible style from a style name
  223. We need that to add style for prompt ... etc.
  224. """
  225. style_overrides = {}
  226. if name_or_cls == 'legacy':
  227. legacy = self.colors.lower()
  228. if legacy == 'linux':
  229. style_cls = get_style_by_name('monokai')
  230. style_overrides = _style_overrides_linux
  231. elif legacy == 'lightbg':
  232. style_overrides = _style_overrides_light_bg
  233. style_cls = get_style_by_name('pastie')
  234. elif legacy == 'neutral':
  235. # The default theme needs to be visible on both a dark background
  236. # and a light background, because we can't tell what the terminal
  237. # looks like. These tweaks to the default theme help with that.
  238. style_cls = get_style_by_name('default')
  239. style_overrides.update({
  240. Token.Number: '#007700',
  241. Token.Operator: 'noinherit',
  242. Token.String: '#BB6622',
  243. Token.Name.Function: '#2080D0',
  244. Token.Name.Class: 'bold #2080D0',
  245. Token.Name.Namespace: 'bold #2080D0',
  246. Token.Prompt: '#009900',
  247. Token.PromptNum: '#00ff00 bold',
  248. Token.OutPrompt: '#990000',
  249. Token.OutPromptNum: '#ff0000 bold',
  250. })
  251. # Hack: Due to limited color support on the Windows console
  252. # the prompt colors will be wrong without this
  253. if os.name == 'nt':
  254. style_overrides.update({
  255. Token.Prompt: '#ansidarkgreen',
  256. Token.PromptNum: '#ansigreen bold',
  257. Token.OutPrompt: '#ansidarkred',
  258. Token.OutPromptNum: '#ansired bold',
  259. })
  260. elif legacy =='nocolor':
  261. style_cls=_NoStyle
  262. style_overrides = {}
  263. else :
  264. raise ValueError('Got unknown colors: ', legacy)
  265. else :
  266. if isinstance(name_or_cls, string_types):
  267. style_cls = get_style_by_name(name_or_cls)
  268. else:
  269. style_cls = name_or_cls
  270. style_overrides = {
  271. Token.Prompt: '#009900',
  272. Token.PromptNum: '#00ff00 bold',
  273. Token.OutPrompt: '#990000',
  274. Token.OutPromptNum: '#ff0000 bold',
  275. }
  276. style_overrides.update(self.highlighting_style_overrides)
  277. style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
  278. style_dict=style_overrides)
  279. return style
  280. def _layout_options(self):
  281. """
  282. Return the current layout option for the current Terminal InteractiveShell
  283. """
  284. return {
  285. 'lexer':IPythonPTLexer(),
  286. 'reserve_space_for_menu':self.space_for_menu,
  287. 'get_prompt_tokens':self.prompts.in_prompt_tokens,
  288. 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
  289. 'multiline':True,
  290. 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
  291. # Highlight matching brackets, but only when this setting is
  292. # enabled, and only when the DEFAULT_BUFFER has the focus.
  293. 'extra_input_processors': [ConditionalProcessor(
  294. processor=HighlightMatchingBracketProcessor(chars='[](){}'),
  295. filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
  296. Condition(lambda cli: self.highlight_matching_brackets))],
  297. }
  298. def _update_layout(self):
  299. """
  300. Ask for a re computation of the application layout, if for example ,
  301. some configuration options have changed.
  302. """
  303. if self._pt_app:
  304. self._pt_app.layout = create_prompt_layout(**self._layout_options())
  305. def prompt_for_code(self):
  306. document = self.pt_cli.run(
  307. pre_run=self.pre_prompt, reset_current_buffer=True)
  308. return document.text
  309. def enable_win_unicode_console(self):
  310. if sys.version_info >= (3, 6):
  311. # Since PEP 528, Python uses the unicode APIs for the Windows
  312. # console by default, so WUC shouldn't be needed.
  313. return
  314. import win_unicode_console
  315. if PY3:
  316. win_unicode_console.enable()
  317. else:
  318. # https://github.com/ipython/ipython/issues/9768
  319. from win_unicode_console.streams import (TextStreamWrapper,
  320. stdout_text_transcoded, stderr_text_transcoded)
  321. class LenientStrStreamWrapper(TextStreamWrapper):
  322. def write(self, s):
  323. if isinstance(s, bytes):
  324. s = s.decode(self.encoding, 'replace')
  325. self.base.write(s)
  326. stdout_text_str = LenientStrStreamWrapper(stdout_text_transcoded)
  327. stderr_text_str = LenientStrStreamWrapper(stderr_text_transcoded)
  328. win_unicode_console.enable(stdout=stdout_text_str,
  329. stderr=stderr_text_str)
  330. def init_io(self):
  331. if sys.platform not in {'win32', 'cli'}:
  332. return
  333. self.enable_win_unicode_console()
  334. import colorama
  335. colorama.init()
  336. # For some reason we make these wrappers around stdout/stderr.
  337. # For now, we need to reset them so all output gets coloured.
  338. # https://github.com/ipython/ipython/issues/8669
  339. # io.std* are deprecated, but don't show our own deprecation warnings
  340. # during initialization of the deprecated API.
  341. with warnings.catch_warnings():
  342. warnings.simplefilter('ignore', DeprecationWarning)
  343. io.stdout = io.IOStream(sys.stdout)
  344. io.stderr = io.IOStream(sys.stderr)
  345. def init_magics(self):
  346. super(TerminalInteractiveShell, self).init_magics()
  347. self.register_magics(TerminalMagics)
  348. def init_alias(self):
  349. # The parent class defines aliases that can be safely used with any
  350. # frontend.
  351. super(TerminalInteractiveShell, self).init_alias()
  352. # Now define aliases that only make sense on the terminal, because they
  353. # need direct access to the console in a way that we can't emulate in
  354. # GUI or web frontend
  355. if os.name == 'posix':
  356. for cmd in ['clear', 'more', 'less', 'man']:
  357. self.alias_manager.soft_define_alias(cmd, cmd)
  358. def __init__(self, *args, **kwargs):
  359. super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
  360. self.init_prompt_toolkit_cli()
  361. self.init_term_title()
  362. self.keep_running = True
  363. self.debugger_history = InMemoryHistory()
  364. def ask_exit(self):
  365. self.keep_running = False
  366. rl_next_input = None
  367. def pre_prompt(self):
  368. if self.rl_next_input:
  369. # We can't set the buffer here, because it will be reset just after
  370. # this. Adding a callable to pre_run_callables does what we need
  371. # after the buffer is reset.
  372. s = cast_unicode_py2(self.rl_next_input)
  373. def set_doc():
  374. self.pt_cli.application.buffer.document = Document(s)
  375. if hasattr(self.pt_cli, 'pre_run_callables'):
  376. self.pt_cli.pre_run_callables.append(set_doc)
  377. else:
  378. # Older version of prompt_toolkit; it's OK to set the document
  379. # directly here.
  380. set_doc()
  381. self.rl_next_input = None
  382. def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
  383. if display_banner is not DISPLAY_BANNER_DEPRECATED:
  384. warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
  385. self.keep_running = True
  386. while self.keep_running:
  387. print(self.separate_in, end='')
  388. try:
  389. code = self.prompt_for_code()
  390. except EOFError:
  391. if (not self.confirm_exit) \
  392. or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
  393. self.ask_exit()
  394. else:
  395. if code:
  396. self.run_cell(code, store_history=True)
  397. def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
  398. # An extra layer of protection in case someone mashing Ctrl-C breaks
  399. # out of our internal code.
  400. if display_banner is not DISPLAY_BANNER_DEPRECATED:
  401. warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
  402. while True:
  403. try:
  404. self.interact()
  405. break
  406. except KeyboardInterrupt as e:
  407. print("\n%s escaped interact()\n" % type(e).__name__)
  408. finally:
  409. # An interrupt during the eventloop will mess up the
  410. # internal state of the prompt_toolkit library.
  411. # Stopping the eventloop fixes this, see
  412. # https://github.com/ipython/ipython/pull/9867
  413. if hasattr(self, '_eventloop'):
  414. self._eventloop.stop()
  415. _inputhook = None
  416. def inputhook(self, context):
  417. if self._inputhook is not None:
  418. self._inputhook(context)
  419. active_eventloop = None
  420. def enable_gui(self, gui=None):
  421. if gui:
  422. self.active_eventloop, self._inputhook =\
  423. get_inputhook_name_and_func(gui)
  424. else:
  425. self.active_eventloop = self._inputhook = None
  426. # Run !system commands directly, not through pipes, so terminal programs
  427. # work correctly.
  428. system = InteractiveShell.system_raw
  429. def auto_rewrite_input(self, cmd):
  430. """Overridden from the parent class to use fancy rewriting prompt"""
  431. if not self.show_rewritten_input:
  432. return
  433. tokens = self.prompts.rewrite_prompt_tokens()
  434. if self.pt_cli:
  435. self.pt_cli.print_tokens(tokens)
  436. print(cmd)
  437. else:
  438. prompt = ''.join(s for t, s in tokens)
  439. print(prompt, cmd, sep='')
  440. _prompts_before = None
  441. def switch_doctest_mode(self, mode):
  442. """Switch prompts to classic for %doctest_mode"""
  443. if mode:
  444. self._prompts_before = self.prompts
  445. self.prompts = ClassicPrompts(self)
  446. elif self._prompts_before:
  447. self.prompts = self._prompts_before
  448. self._prompts_before = None
  449. self._update_layout()
  450. InteractiveShellABC.register(TerminalInteractiveShell)
  451. if __name__ == '__main__':
  452. TerminalInteractiveShell.instance().interact()