interactiveshell.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127
  1. """IPython terminal interface using prompt_toolkit"""
  2. import os
  3. import sys
  4. import inspect
  5. from warnings import warn
  6. from typing import Union as UnionType, Optional
  7. from IPython.core.async_helpers import get_asyncio_loop
  8. from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
  9. from IPython.utils.py3compat import input
  10. from IPython.utils.PyColorize import theme_table
  11. from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
  12. from IPython.utils.process import abbrev_cwd
  13. from traitlets import (
  14. Any,
  15. Bool,
  16. Dict,
  17. Enum,
  18. Float,
  19. Instance,
  20. Integer,
  21. List,
  22. Type,
  23. Unicode,
  24. Union,
  25. default,
  26. observe,
  27. validate,
  28. DottedObjectName,
  29. )
  30. from traitlets.utils.importstring import import_item
  31. from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
  32. from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
  33. from prompt_toolkit.filters import HasFocus, Condition, IsDone
  34. from prompt_toolkit.formatted_text import PygmentsTokens
  35. from prompt_toolkit.history import History
  36. from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
  37. from prompt_toolkit.output import ColorDepth
  38. from prompt_toolkit.patch_stdout import patch_stdout
  39. from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
  40. from prompt_toolkit.styles import DynamicStyle, merge_styles
  41. from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
  42. from pygments.styles import get_style_by_name
  43. from pygments.style import Style
  44. from .debugger import TerminalPdb, Pdb
  45. from .magics import TerminalMagics
  46. from .pt_inputhooks import get_inputhook_name_and_func
  47. from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
  48. from .ptutils import IPythonPTCompleter, IPythonPTLexer
  49. from .shortcuts import (
  50. KEY_BINDINGS,
  51. UNASSIGNED_ALLOWED_COMMANDS,
  52. create_ipython_shortcuts,
  53. create_identifier,
  54. RuntimeBinding,
  55. add_binding,
  56. )
  57. from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
  58. from .shortcuts.auto_suggest import (
  59. NavigableAutoSuggestFromHistory,
  60. AppendAutoSuggestionInAnyLine,
  61. )
  62. class _NoStyle(Style):
  63. pass
  64. def _backward_compat_continuation_prompt_tokens(
  65. method, width: int, *, lineno: int, wrap_count: int
  66. ):
  67. """
  68. Sagemath use custom prompt and we broke them in 8.19.
  69. make sure to pass only width if method only support width
  70. """
  71. sig = inspect.signature(method)
  72. extra = {}
  73. params = inspect.signature(method).parameters
  74. if "lineno" in inspect.signature(method).parameters or any(
  75. [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
  76. ):
  77. extra["lineno"] = lineno
  78. if "line_number" in inspect.signature(method).parameters or any(
  79. [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
  80. ):
  81. extra["line_number"] = lineno
  82. if "wrap_count" in inspect.signature(method).parameters or any(
  83. [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
  84. ):
  85. extra["wrap_count"] = wrap_count
  86. return method(width, **extra)
  87. def get_default_editor():
  88. try:
  89. return os.environ['EDITOR']
  90. except KeyError:
  91. pass
  92. except UnicodeError:
  93. warn("$EDITOR environment variable is not pure ASCII. Using platform "
  94. "default editor.")
  95. if os.name == 'posix':
  96. return 'vi' # the only one guaranteed to be there!
  97. else:
  98. return "notepad" # same in Windows!
  99. # conservatively check for tty
  100. # overridden streams can result in things like:
  101. # - sys.stdin = None
  102. # - no isatty method
  103. for _name in ('stdin', 'stdout', 'stderr'):
  104. _stream = getattr(sys, _name)
  105. try:
  106. if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
  107. _is_tty = False
  108. break
  109. except ValueError:
  110. # stream is closed
  111. _is_tty = False
  112. break
  113. else:
  114. _is_tty = True
  115. _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
  116. def black_reformat_handler(text_before_cursor):
  117. """
  118. We do not need to protect against error,
  119. this is taken care at a higher level where any reformat error is ignored.
  120. Indeed we may call reformatting on incomplete code.
  121. """
  122. import black
  123. formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
  124. if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
  125. formatted_text = formatted_text[:-1]
  126. return formatted_text
  127. def yapf_reformat_handler(text_before_cursor):
  128. from yapf.yapflib import file_resources
  129. from yapf.yapflib import yapf_api
  130. style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
  131. formatted_text, was_formatted = yapf_api.FormatCode(
  132. text_before_cursor, style_config=style_config
  133. )
  134. if was_formatted:
  135. if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
  136. formatted_text = formatted_text[:-1]
  137. return formatted_text
  138. else:
  139. return text_before_cursor
  140. class PtkHistoryAdapter(History):
  141. """
  142. Prompt toolkit has it's own way of handling history, Where it assumes it can
  143. Push/pull from history.
  144. """
  145. def __init__(self, shell):
  146. super().__init__()
  147. self.shell = shell
  148. self._refresh()
  149. def append_string(self, string):
  150. # we rely on sql for that.
  151. self._loaded = False
  152. self._refresh()
  153. def _refresh(self):
  154. if not self._loaded:
  155. self._loaded_strings = list(self.load_history_strings())
  156. def load_history_strings(self):
  157. last_cell = ""
  158. res = []
  159. for __, ___, cell in self.shell.history_manager.get_tail(
  160. self.shell.history_load_length, include_latest=True
  161. ):
  162. # Ignore blank lines and consecutive duplicates
  163. cell = cell.rstrip()
  164. if cell and (cell != last_cell):
  165. res.append(cell)
  166. last_cell = cell
  167. yield from res[::-1]
  168. def store_string(self, string: str) -> None:
  169. pass
  170. class TerminalInteractiveShell(InteractiveShell):
  171. mime_renderers = Dict().tag(config=True)
  172. min_elide = Integer(
  173. 30, help="minimum characters for filling with ellipsis in file completions"
  174. ).tag(config=True)
  175. space_for_menu = Integer(
  176. 6,
  177. help="Number of line at the bottom of the screen "
  178. "to reserve for the tab completion menu, "
  179. "search history, ...etc, the height of "
  180. "these menus will at most this value. "
  181. "Increase it is you prefer long and skinny "
  182. "menus, decrease for short and wide.",
  183. ).tag(config=True)
  184. pt_app: UnionType[PromptSession, None] = None
  185. auto_suggest: UnionType[
  186. AutoSuggestFromHistory,
  187. NavigableAutoSuggestFromHistory,
  188. None,
  189. ] = None
  190. debugger_history = None
  191. debugger_history_file = Unicode(
  192. "~/.pdbhistory", help="File in which to store and read history"
  193. ).tag(config=True)
  194. simple_prompt = Bool(_use_simple_prompt,
  195. help="""Use `raw_input` for the REPL, without completion and prompt colors.
  196. Useful when controlling IPython as a subprocess, and piping
  197. STDIN/OUT/ERR. Known usage are: IPython's own testing machinery,
  198. and emacs' inferior-python subprocess (assuming you have set
  199. `python-shell-interpreter` to "ipython") available through the
  200. built-in `M-x run-python` and third party packages such as elpy.
  201. This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
  202. environment variable is set, or the current terminal is not a tty.
  203. Thus the Default value reported in --help-all, or config will often
  204. be incorrectly reported.
  205. """,
  206. ).tag(config=True)
  207. @property
  208. def debugger_cls(self):
  209. return Pdb if self.simple_prompt else TerminalPdb
  210. confirm_exit = Bool(True,
  211. help="""
  212. Set to confirm when you try to exit IPython with an EOF (Control-D
  213. in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
  214. you can force a direct exit without any confirmation.""",
  215. ).tag(config=True)
  216. editing_mode = Unicode('emacs',
  217. help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
  218. ).tag(config=True)
  219. emacs_bindings_in_vi_insert_mode = Bool(
  220. True,
  221. help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
  222. ).tag(config=True)
  223. modal_cursor = Bool(
  224. True,
  225. help="""
  226. Cursor shape changes depending on vi mode: beam in vi insert mode,
  227. block in nav mode, underscore in replace mode.""",
  228. ).tag(config=True)
  229. ttimeoutlen = Float(
  230. 0.01,
  231. help="""The time in milliseconds that is waited for a key code
  232. to complete.""",
  233. ).tag(config=True)
  234. timeoutlen = Float(
  235. 0.5,
  236. help="""The time in milliseconds that is waited for a mapped key
  237. sequence to complete.""",
  238. ).tag(config=True)
  239. autoformatter = Unicode(
  240. None,
  241. help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
  242. allow_none=True
  243. ).tag(config=True)
  244. auto_match = Bool(
  245. False,
  246. help="""
  247. Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
  248. Brackets: (), [], {}
  249. Quotes: '', \"\"
  250. """,
  251. ).tag(config=True)
  252. mouse_support = Bool(False,
  253. help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
  254. ).tag(config=True)
  255. # We don't load the list of styles for the help string, because loading
  256. # Pygments plugins takes time and can cause unexpected errors.
  257. highlighting_style = Union(
  258. [Unicode("legacy"), Type(klass=Style)],
  259. help="""Deprecated, and has not effect, use IPython themes
  260. The name or class of a Pygments style to use for syntax
  261. highlighting. To see available styles, run `pygmentize -L styles`.""",
  262. ).tag(config=True)
  263. @validate('editing_mode')
  264. def _validate_editing_mode(self, proposal):
  265. if proposal['value'].lower() == 'vim':
  266. proposal['value']= 'vi'
  267. elif proposal['value'].lower() == 'default':
  268. proposal['value']= 'emacs'
  269. if hasattr(EditingMode, proposal['value'].upper()):
  270. return proposal['value'].lower()
  271. return self.editing_mode
  272. @observe('editing_mode')
  273. def _editing_mode(self, change):
  274. if self.pt_app:
  275. self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
  276. def _set_formatter(self, formatter):
  277. if formatter is None:
  278. self.reformat_handler = lambda x:x
  279. elif formatter == 'black':
  280. self.reformat_handler = black_reformat_handler
  281. elif formatter == "yapf":
  282. self.reformat_handler = yapf_reformat_handler
  283. else:
  284. raise ValueError
  285. @observe("autoformatter")
  286. def _autoformatter_changed(self, change):
  287. formatter = change.new
  288. self._set_formatter(formatter)
  289. @observe('highlighting_style')
  290. @observe('colors')
  291. def _highlighting_style_changed(self, change):
  292. assert change.new == change.new.lower()
  293. if change.new != "legacy":
  294. warn(
  295. "highlighting_style is deprecated since 9.0 and have no effect, use themeing."
  296. )
  297. return
  298. def refresh_style(self):
  299. self._style = self._make_style_from_name_or_cls("legacy")
  300. # TODO: deprecate this
  301. highlighting_style_overrides = Dict(
  302. help="Override highlighting format for specific tokens"
  303. ).tag(config=True)
  304. true_color = Bool(False,
  305. help="""Use 24bit colors instead of 256 colors in prompt highlighting.
  306. If your terminal supports true color, the following command should
  307. print ``TRUECOLOR`` in orange::
  308. printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
  309. """,
  310. ).tag(config=True)
  311. editor = Unicode(get_default_editor(),
  312. help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
  313. ).tag(config=True)
  314. prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
  315. prompts = Instance(Prompts)
  316. @default('prompts')
  317. def _prompts_default(self):
  318. return self.prompts_class(self)
  319. # @observe('prompts')
  320. # def _(self, change):
  321. # self._update_layout()
  322. @default('displayhook_class')
  323. def _displayhook_class_default(self):
  324. return RichPromptDisplayHook
  325. term_title = Bool(True,
  326. help="Automatically set the terminal title"
  327. ).tag(config=True)
  328. term_title_format = Unicode("IPython: {cwd}",
  329. help="Customize the terminal title format. This is a python format string. " +
  330. "Available substitutions are: {cwd}."
  331. ).tag(config=True)
  332. display_completions = Enum(('column', 'multicolumn','readlinelike'),
  333. help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
  334. "'readlinelike'. These options are for `prompt_toolkit`, see "
  335. "`prompt_toolkit` documentation for more information."
  336. ),
  337. default_value='multicolumn').tag(config=True)
  338. highlight_matching_brackets = Bool(True,
  339. help="Highlight matching brackets.",
  340. ).tag(config=True)
  341. extra_open_editor_shortcuts = Bool(False,
  342. help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
  343. "This is in addition to the F2 binding, which is always enabled."
  344. ).tag(config=True)
  345. handle_return = Any(None,
  346. help="Provide an alternative handler to be called when the user presses "
  347. "Return. This is an advanced option intended for debugging, which "
  348. "may be changed or removed in later releases."
  349. ).tag(config=True)
  350. enable_history_search = Bool(True,
  351. help="Allows to enable/disable the prompt toolkit history search"
  352. ).tag(config=True)
  353. autosuggestions_provider = Unicode(
  354. "NavigableAutoSuggestFromHistory",
  355. help="Specifies from which source automatic suggestions are provided. "
  356. "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
  357. ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
  358. " or ``None`` to disable automatic suggestions. "
  359. "Default is `'NavigableAutoSuggestFromHistory`'.",
  360. allow_none=True,
  361. ).tag(config=True)
  362. _autosuggestions_provider: Any
  363. llm_constructor_kwargs = Dict(
  364. {},
  365. help="""
  366. Extra arguments to pass to `llm_provider_class` constructor.
  367. This is used to – for example – set the `model_id`""",
  368. ).tag(config=True)
  369. llm_prefix_from_history = DottedObjectName(
  370. "input_history",
  371. help="""\
  372. Fully Qualifed name of a function that takes an IPython history manager and
  373. return a prefix to pass the llm provider in addition to the current buffer
  374. text.
  375. You can use:
  376. - no_prefix
  377. - input_history
  378. As default value. `input_history` (default), will use all the input history
  379. of current IPython session
  380. """,
  381. ).tag(config=True)
  382. _llm_prefix_from_history: Any
  383. @observe("llm_prefix_from_history")
  384. def _llm_prefix_from_history_changed(self, change):
  385. name = change.new
  386. self._llm_prefix_from_history = name
  387. self._set_autosuggestions()
  388. llm_provider_class = DottedObjectName(
  389. None,
  390. allow_none=True,
  391. help="""\
  392. Provisional:
  393. This is a provisional API in IPython 8.32, before stabilisation
  394. in 9.0, it may change without warnings.
  395. class to use for the `NavigableAutoSuggestFromHistory` to request
  396. completions from a LLM, this should inherit from
  397. `jupyter_ai_magics:BaseProvider` and implement
  398. `stream_inline_completions`
  399. """,
  400. ).tag(config=True)
  401. _llm_provider_class: Any = None
  402. @observe("llm_provider_class")
  403. def _llm_provider_class_changed(self, change):
  404. provider_class = change.new
  405. self._llm_provider_class = provider_class
  406. self._set_autosuggestions()
  407. def _set_autosuggestions(self, provider=None):
  408. if provider is None:
  409. provider = self.autosuggestions_provider
  410. # disconnect old handler
  411. if self.auto_suggest and isinstance(
  412. self.auto_suggest, NavigableAutoSuggestFromHistory
  413. ):
  414. self.auto_suggest.disconnect()
  415. if provider is None:
  416. self.auto_suggest = None
  417. elif provider == "AutoSuggestFromHistory":
  418. self.auto_suggest = AutoSuggestFromHistory()
  419. elif provider == "NavigableAutoSuggestFromHistory":
  420. # LLM stuff are all Provisional in 8.32
  421. if self._llm_provider_class:
  422. def init_llm_provider():
  423. llm_provider_constructor = import_item(self._llm_provider_class)
  424. return llm_provider_constructor(**self.llm_constructor_kwargs)
  425. else:
  426. init_llm_provider = None
  427. self.auto_suggest = NavigableAutoSuggestFromHistory()
  428. # Provisinal in 8.32
  429. self.auto_suggest._init_llm_provider = init_llm_provider
  430. name = self.llm_prefix_from_history
  431. if name == "no_prefix":
  432. def no_prefix(history_manager):
  433. return ""
  434. fun = no_prefix
  435. elif name == "input_history":
  436. def input_history(history_manager):
  437. return "\n".join([s[2] for s in history_manager.get_range()]) + "\n"
  438. fun = input_history
  439. else:
  440. fun = import_item(name)
  441. self.auto_suggest._llm_prefixer = fun
  442. else:
  443. raise ValueError("No valid provider.")
  444. if self.pt_app:
  445. self.pt_app.auto_suggest = self.auto_suggest
  446. @observe("autosuggestions_provider")
  447. def _autosuggestions_provider_changed(self, change):
  448. provider = change.new
  449. self._set_autosuggestions(provider)
  450. shortcuts = List(
  451. trait=Dict(
  452. key_trait=Enum(
  453. [
  454. "command",
  455. "match_keys",
  456. "match_filter",
  457. "new_keys",
  458. "new_filter",
  459. "create",
  460. ]
  461. ),
  462. per_key_traits={
  463. "command": Unicode(),
  464. "match_keys": List(Unicode()),
  465. "match_filter": Unicode(),
  466. "new_keys": List(Unicode()),
  467. "new_filter": Unicode(),
  468. "create": Bool(False),
  469. },
  470. ),
  471. help="""
  472. Add, disable or modifying shortcuts.
  473. Each entry on the list should be a dictionary with ``command`` key
  474. identifying the target function executed by the shortcut and at least
  475. one of the following:
  476. - ``match_keys``: list of keys used to match an existing shortcut,
  477. - ``match_filter``: shortcut filter used to match an existing shortcut,
  478. - ``new_keys``: list of keys to set,
  479. - ``new_filter``: a new shortcut filter to set
  480. The filters have to be composed of pre-defined verbs and joined by one
  481. of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
  482. The pre-defined verbs are:
  483. {filters}
  484. To disable a shortcut set ``new_keys`` to an empty list.
  485. To add a shortcut add key ``create`` with value ``True``.
  486. When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
  487. be omitted if the provided specification uniquely identifies a shortcut
  488. to be modified/disabled. When modifying a shortcut ``new_filter`` or
  489. ``new_keys`` can be omitted which will result in reuse of the existing
  490. filter/keys.
  491. Only shortcuts defined in IPython (and not default prompt-toolkit
  492. shortcuts) can be modified or disabled. The full list of shortcuts,
  493. command identifiers and filters is available under
  494. :ref:`terminal-shortcuts-list`.
  495. Here is an example:
  496. .. code::
  497. c.TerminalInteractiveShell.shortcuts = [
  498. {{
  499. "new_keys": ["c-q"],
  500. "command": "prompt_toolkit:named_commands.capitalize_word",
  501. "create": True,
  502. }},
  503. {{
  504. "new_keys": ["c-j"],
  505. "command": "prompt_toolkit:named_commands.beginning_of_line",
  506. "create": True,
  507. }},
  508. ]
  509. """.format(
  510. filters="\n ".join([f" - ``{k}``" for k in KEYBINDING_FILTERS])
  511. ),
  512. ).tag(config=True)
  513. @observe("shortcuts")
  514. def _shortcuts_changed(self, change):
  515. if self.pt_app:
  516. self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
  517. def _merge_shortcuts(self, user_shortcuts):
  518. # rebuild the bindings list from scratch
  519. key_bindings = create_ipython_shortcuts(self)
  520. # for now we only allow adding shortcuts for a specific set of
  521. # commands; this is a security precution.
  522. allowed_commands = {
  523. create_identifier(binding.command): binding.command
  524. for binding in KEY_BINDINGS
  525. }
  526. allowed_commands.update(
  527. {
  528. create_identifier(command): command
  529. for command in UNASSIGNED_ALLOWED_COMMANDS
  530. }
  531. )
  532. shortcuts_to_skip = []
  533. shortcuts_to_add = []
  534. for shortcut in user_shortcuts:
  535. command_id = shortcut["command"]
  536. if command_id not in allowed_commands:
  537. allowed_commands = "\n - ".join(allowed_commands)
  538. raise ValueError(
  539. f"{command_id} is not a known shortcut command."
  540. f" Allowed commands are: \n - {allowed_commands}"
  541. )
  542. old_keys = shortcut.get("match_keys", None)
  543. old_filter = (
  544. filter_from_string(shortcut["match_filter"])
  545. if "match_filter" in shortcut
  546. else None
  547. )
  548. matching = [
  549. binding
  550. for binding in KEY_BINDINGS
  551. if (
  552. (old_filter is None or binding.filter == old_filter)
  553. and (old_keys is None or [k for k in binding.keys] == old_keys)
  554. and create_identifier(binding.command) == command_id
  555. )
  556. ]
  557. new_keys = shortcut.get("new_keys", None)
  558. new_filter = shortcut.get("new_filter", None)
  559. command = allowed_commands[command_id]
  560. creating_new = shortcut.get("create", False)
  561. modifying_existing = not creating_new and (
  562. new_keys is not None or new_filter
  563. )
  564. if creating_new and new_keys == []:
  565. raise ValueError("Cannot add a shortcut without keys")
  566. if modifying_existing:
  567. specification = {
  568. key: shortcut[key]
  569. for key in ["command", "filter"]
  570. if key in shortcut
  571. }
  572. if len(matching) == 0:
  573. raise ValueError(
  574. f"No shortcuts matching {specification} found in {KEY_BINDINGS}"
  575. )
  576. elif len(matching) > 1:
  577. raise ValueError(
  578. f"Multiple shortcuts matching {specification} found,"
  579. f" please add keys/filter to select one of: {matching}"
  580. )
  581. matched = matching[0]
  582. old_filter = matched.filter
  583. old_keys = list(matched.keys)
  584. shortcuts_to_skip.append(
  585. RuntimeBinding(
  586. command,
  587. keys=old_keys,
  588. filter=old_filter,
  589. )
  590. )
  591. if new_keys != []:
  592. shortcuts_to_add.append(
  593. RuntimeBinding(
  594. command,
  595. keys=new_keys or old_keys,
  596. filter=(
  597. filter_from_string(new_filter)
  598. if new_filter is not None
  599. else (
  600. old_filter
  601. if old_filter is not None
  602. else filter_from_string("always")
  603. )
  604. ),
  605. )
  606. )
  607. # rebuild the bindings list from scratch
  608. key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
  609. for binding in shortcuts_to_add:
  610. add_binding(key_bindings, binding)
  611. return key_bindings
  612. prompt_includes_vi_mode = Bool(True,
  613. help="Display the current vi mode (when using vi editing mode)."
  614. ).tag(config=True)
  615. prompt_line_number_format = Unicode(
  616. "",
  617. help="The format for line numbering, will be passed `line` (int, 1 based)"
  618. " the current line number and `rel_line` the relative line number."
  619. " for example to display both you can use the following template string :"
  620. " c.TerminalInteractiveShell.prompt_line_number_format='{line: 4d}/{rel_line:+03d} | '"
  621. " This will display the current line number, with leading space and a width of at least 4"
  622. " character, as well as the relative line number 0 padded and always with a + or - sign."
  623. " Note that when using Emacs mode the prompt of the first line may not update.",
  624. ).tag(config=True)
  625. @observe('term_title')
  626. def init_term_title(self, change=None):
  627. # Enable or disable the terminal title.
  628. if self.term_title and _is_tty:
  629. toggle_set_term_title(True)
  630. set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
  631. else:
  632. toggle_set_term_title(False)
  633. def restore_term_title(self):
  634. if self.term_title and _is_tty:
  635. restore_term_title()
  636. def init_display_formatter(self):
  637. super(TerminalInteractiveShell, self).init_display_formatter()
  638. # terminal only supports plain text if not explicitly configured
  639. config = self.display_formatter._trait_values["config"]
  640. if not (
  641. "DisplayFormatter" in config
  642. and "active_types" in config["DisplayFormatter"]
  643. ):
  644. self.display_formatter.active_types = ["text/plain"]
  645. def init_prompt_toolkit_cli(self):
  646. if self.simple_prompt:
  647. # Fall back to plain non-interactive output for tests.
  648. # This is very limited.
  649. def prompt():
  650. prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
  651. lines = [input(prompt_text)]
  652. prompt_continuation = "".join(
  653. x[1] for x in self.prompts.continuation_prompt_tokens()
  654. )
  655. while self.check_complete("\n".join(lines))[0] == "incomplete":
  656. lines.append(input(prompt_continuation))
  657. return "\n".join(lines)
  658. self.prompt_for_code = prompt
  659. return
  660. # Set up keyboard shortcuts
  661. key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
  662. # Pre-populate history from IPython's history database
  663. history = PtkHistoryAdapter(self)
  664. self.refresh_style()
  665. ptk_s = DynamicStyle(lambda: self._style)
  666. editing_mode = getattr(EditingMode, self.editing_mode.upper())
  667. self._use_asyncio_inputhook = False
  668. self.pt_app = PromptSession(
  669. auto_suggest=self.auto_suggest,
  670. editing_mode=editing_mode,
  671. key_bindings=key_bindings,
  672. history=history,
  673. completer=IPythonPTCompleter(shell=self),
  674. enable_history_search=self.enable_history_search,
  675. style=ptk_s,
  676. include_default_pygments_style=False,
  677. mouse_support=self.mouse_support,
  678. enable_open_in_editor=self.extra_open_editor_shortcuts,
  679. color_depth=self.color_depth,
  680. tempfile_suffix=".py",
  681. **self._extra_prompt_options(),
  682. )
  683. if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
  684. self.auto_suggest.connect(self.pt_app)
  685. def _make_style_from_name_or_cls(self, name_or_cls):
  686. """
  687. Small wrapper that make an IPython compatible style from a style name
  688. We need that to add style for prompt ... etc.
  689. """
  690. assert name_or_cls == "legacy"
  691. legacy = self.colors.lower()
  692. theme = theme_table.get(legacy, None)
  693. assert theme is not None, legacy
  694. if legacy == "nocolor":
  695. style_overrides = {}
  696. style_cls = _NoStyle
  697. else:
  698. style_overrides = {**theme.extra_style, **self.highlighting_style_overrides}
  699. if theme.base is not None:
  700. style_cls = get_style_by_name(theme.base)
  701. else:
  702. style_cls = _NoStyle
  703. style = merge_styles(
  704. [
  705. style_from_pygments_cls(style_cls),
  706. style_from_pygments_dict(style_overrides),
  707. ]
  708. )
  709. return style
  710. @property
  711. def pt_complete_style(self):
  712. return {
  713. 'multicolumn': CompleteStyle.MULTI_COLUMN,
  714. 'column': CompleteStyle.COLUMN,
  715. 'readlinelike': CompleteStyle.READLINE_LIKE,
  716. }[self.display_completions]
  717. @property
  718. def color_depth(self):
  719. return (ColorDepth.TRUE_COLOR if self.true_color else None)
  720. def _ptk_prompt_cont(self, width: int, line_number: int, wrap_count: int):
  721. return PygmentsTokens(
  722. _backward_compat_continuation_prompt_tokens(
  723. self.prompts.continuation_prompt_tokens,
  724. width,
  725. lineno=line_number,
  726. wrap_count=wrap_count,
  727. )
  728. )
  729. def _extra_prompt_options(self):
  730. """
  731. Return the current layout option for the current Terminal InteractiveShell
  732. """
  733. def get_message():
  734. return PygmentsTokens(self.prompts.in_prompt_tokens())
  735. if self.editing_mode == "emacs" and self.prompt_line_number_format == "":
  736. # with emacs mode the prompt is (usually) static, so we call only
  737. # the function once. With VI mode it can toggle between [ins] and
  738. # [nor] so we can't precompute.
  739. # here I'm going to favor the default keybinding which almost
  740. # everybody uses to decrease CPU usage.
  741. # if we have issues with users with custom Prompts we can see how to
  742. # work around this.
  743. get_message = get_message()
  744. options = {
  745. "complete_in_thread": False,
  746. "lexer": IPythonPTLexer(),
  747. "reserve_space_for_menu": self.space_for_menu,
  748. "message": get_message,
  749. "prompt_continuation": self._ptk_prompt_cont,
  750. "multiline": True,
  751. "complete_style": self.pt_complete_style,
  752. "input_processors": [
  753. # Highlight matching brackets, but only when this setting is
  754. # enabled, and only when the DEFAULT_BUFFER has the focus.
  755. ConditionalProcessor(
  756. processor=HighlightMatchingBracketProcessor(chars="[](){}"),
  757. filter=HasFocus(DEFAULT_BUFFER)
  758. & ~IsDone()
  759. & Condition(lambda: self.highlight_matching_brackets),
  760. ),
  761. # Show auto-suggestion in lines other than the last line.
  762. ConditionalProcessor(
  763. processor=AppendAutoSuggestionInAnyLine(),
  764. filter=HasFocus(DEFAULT_BUFFER)
  765. & ~IsDone()
  766. & Condition(
  767. lambda: isinstance(
  768. self.auto_suggest,
  769. NavigableAutoSuggestFromHistory,
  770. )
  771. ),
  772. ),
  773. ],
  774. }
  775. return options
  776. def prompt_for_code(self):
  777. if self.rl_next_input:
  778. default = self.rl_next_input
  779. self.rl_next_input = None
  780. else:
  781. default = ''
  782. # In order to make sure that asyncio code written in the
  783. # interactive shell doesn't interfere with the prompt, we run the
  784. # prompt in a different event loop.
  785. # If we don't do this, people could spawn coroutine with a
  786. # while/true inside which will freeze the prompt.
  787. with patch_stdout(raw=True):
  788. if self._use_asyncio_inputhook:
  789. # When we integrate the asyncio event loop, run the UI in the
  790. # same event loop as the rest of the code. don't use an actual
  791. # input hook. (Asyncio is not made for nesting event loops.)
  792. asyncio_loop = get_asyncio_loop()
  793. text = asyncio_loop.run_until_complete(
  794. self.pt_app.prompt_async(
  795. default=default, **self._extra_prompt_options()
  796. )
  797. )
  798. else:
  799. text = self.pt_app.prompt(
  800. default=default,
  801. inputhook=self._inputhook,
  802. **self._extra_prompt_options(),
  803. )
  804. return text
  805. def init_io(self):
  806. if sys.platform not in {'win32', 'cli'}:
  807. return
  808. import colorama
  809. colorama.init()
  810. def init_magics(self):
  811. super(TerminalInteractiveShell, self).init_magics()
  812. self.register_magics(TerminalMagics)
  813. def init_alias(self):
  814. # The parent class defines aliases that can be safely used with any
  815. # frontend.
  816. super(TerminalInteractiveShell, self).init_alias()
  817. # Now define aliases that only make sense on the terminal, because they
  818. # need direct access to the console in a way that we can't emulate in
  819. # GUI or web frontend
  820. if os.name == 'posix':
  821. for cmd in ('clear', 'more', 'less', 'man'):
  822. self.alias_manager.soft_define_alias(cmd, cmd)
  823. def __init__(self, *args, **kwargs) -> None:
  824. super().__init__(*args, **kwargs)
  825. self._set_autosuggestions(self.autosuggestions_provider)
  826. self.init_prompt_toolkit_cli()
  827. self.init_term_title()
  828. self.keep_running = True
  829. self._set_formatter(self.autoformatter)
  830. def ask_exit(self):
  831. self.keep_running = False
  832. rl_next_input = None
  833. def interact(self):
  834. self.keep_running = True
  835. while self.keep_running:
  836. print(self.separate_in, end='')
  837. try:
  838. code = self.prompt_for_code()
  839. except EOFError:
  840. if (not self.confirm_exit) \
  841. or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
  842. self.ask_exit()
  843. else:
  844. if code:
  845. self.run_cell(code, store_history=True)
  846. def mainloop(self):
  847. # An extra layer of protection in case someone mashing Ctrl-C breaks
  848. # out of our internal code.
  849. while True:
  850. try:
  851. self.interact()
  852. break
  853. except KeyboardInterrupt as e:
  854. print("\n%s escaped interact()\n" % type(e).__name__)
  855. finally:
  856. # An interrupt during the eventloop will mess up the
  857. # internal state of the prompt_toolkit library.
  858. # Stopping the eventloop fixes this, see
  859. # https://github.com/ipython/ipython/pull/9867
  860. if hasattr(self, '_eventloop'):
  861. self._eventloop.stop()
  862. self.restore_term_title()
  863. # try to call some at-exit operation optimistically as some things can't
  864. # be done during interpreter shutdown. this is technically inaccurate as
  865. # this make mainlool not re-callable, but that should be a rare if not
  866. # in existent use case.
  867. self._atexit_once()
  868. _inputhook = None
  869. def inputhook(self, context):
  870. warn(
  871. "inputkook seem unused, and marked for deprecation/Removal as of IPython 9.0. "
  872. "Please open an issue if you are using it.",
  873. category=DeprecationWarning,
  874. stacklevel=2,
  875. )
  876. if self._inputhook is not None:
  877. self._inputhook(context)
  878. active_eventloop: Optional[str] = None
  879. def enable_gui(self, gui: Optional[str] = None) -> None:
  880. if gui:
  881. from ..core.pylabtools import _convert_gui_from_matplotlib
  882. gui = _convert_gui_from_matplotlib(gui)
  883. if self.simple_prompt is True and gui is not None:
  884. print(
  885. f'Cannot install event loop hook for "{gui}" when running with `--simple-prompt`.'
  886. )
  887. print(
  888. "NOTE: Tk is supported natively; use Tk apps and Tk backends with `--simple-prompt`."
  889. )
  890. return
  891. if self._inputhook is None and gui is None:
  892. print("No event loop hook running.")
  893. return
  894. if self._inputhook is not None and gui is not None:
  895. newev, newinhook = get_inputhook_name_and_func(gui)
  896. if self._inputhook == newinhook:
  897. # same inputhook, do nothing
  898. self.log.info(
  899. f"Shell is already running the {self.active_eventloop} eventloop. Doing nothing"
  900. )
  901. return
  902. self.log.warning(
  903. f"Shell is already running a different gui event loop for {self.active_eventloop}. "
  904. "Call with no arguments to disable the current loop."
  905. )
  906. return
  907. if self._inputhook is not None and gui is None:
  908. self.active_eventloop = self._inputhook = None
  909. if gui and (gui not in {None, "webagg"}):
  910. # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
  911. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
  912. else:
  913. self.active_eventloop = self._inputhook = None
  914. self._use_asyncio_inputhook = gui == "asyncio"
  915. # Run !system commands directly, not through pipes, so terminal programs
  916. # work correctly.
  917. system = InteractiveShell.system_raw
  918. def auto_rewrite_input(self, cmd):
  919. """Overridden from the parent class to use fancy rewriting prompt"""
  920. if not self.show_rewritten_input:
  921. return
  922. tokens = self.prompts.rewrite_prompt_tokens()
  923. if self.pt_app:
  924. print_formatted_text(PygmentsTokens(tokens), end='',
  925. style=self.pt_app.app.style)
  926. print(cmd)
  927. else:
  928. prompt = ''.join(s for t, s in tokens)
  929. print(prompt, cmd, sep='')
  930. _prompts_before = None
  931. def switch_doctest_mode(self, mode):
  932. """Switch prompts to classic for %doctest_mode"""
  933. if mode:
  934. self._prompts_before = self.prompts
  935. self.prompts = ClassicPrompts(self)
  936. elif self._prompts_before:
  937. self.prompts = self._prompts_before
  938. self._prompts_before = None
  939. # self._update_layout()
  940. InteractiveShellABC.register(TerminalInteractiveShell)
  941. if __name__ == '__main__':
  942. TerminalInteractiveShell.instance().interact()