aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/ipython/py3/IPython/terminal/shortcuts
diff options
context:
space:
mode:
authornkozlovskiy <nmk@ydb.tech>2023-09-29 12:24:06 +0300
committernkozlovskiy <nmk@ydb.tech>2023-09-29 12:41:34 +0300
commite0e3e1717e3d33762ce61950504f9637a6e669ed (patch)
treebca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/ipython/py3/IPython/terminal/shortcuts
parent38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff)
downloadydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz
add ydb deps
Diffstat (limited to 'contrib/python/ipython/py3/IPython/terminal/shortcuts')
-rw-r--r--contrib/python/ipython/py3/IPython/terminal/shortcuts/__init__.py630
-rw-r--r--contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_match.py104
-rw-r--r--contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_suggest.py401
-rw-r--r--contrib/python/ipython/py3/IPython/terminal/shortcuts/filters.py322
4 files changed, 1457 insertions, 0 deletions
diff --git a/contrib/python/ipython/py3/IPython/terminal/shortcuts/__init__.py b/contrib/python/ipython/py3/IPython/terminal/shortcuts/__init__.py
new file mode 100644
index 0000000000..12890f4ab6
--- /dev/null
+++ b/contrib/python/ipython/py3/IPython/terminal/shortcuts/__init__.py
@@ -0,0 +1,630 @@
+"""
+Module to define and register Terminal IPython shortcuts with
+:mod:`prompt_toolkit`
+"""
+
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os
+import signal
+import sys
+import warnings
+from dataclasses import dataclass
+from typing import Callable, Any, Optional, List
+
+from prompt_toolkit.application.current import get_app
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.key_binding.key_processor import KeyPressEvent
+from prompt_toolkit.key_binding.bindings import named_commands as nc
+from prompt_toolkit.key_binding.bindings.completion import (
+ display_completions_like_readline,
+)
+from prompt_toolkit.key_binding.vi_state import InputMode, ViState
+from prompt_toolkit.filters import Condition
+
+from IPython.core.getipython import get_ipython
+from IPython.terminal.shortcuts import auto_match as match
+from IPython.terminal.shortcuts import auto_suggest
+from IPython.terminal.shortcuts.filters import filter_from_string
+from IPython.utils.decorators import undoc
+
+from prompt_toolkit.enums import DEFAULT_BUFFER
+
+__all__ = ["create_ipython_shortcuts"]
+
+
+@dataclass
+class BaseBinding:
+ command: Callable[[KeyPressEvent], Any]
+ keys: List[str]
+
+
+@dataclass
+class RuntimeBinding(BaseBinding):
+ filter: Condition
+
+
+@dataclass
+class Binding(BaseBinding):
+ # while filter could be created by referencing variables directly (rather
+ # than created from strings), by using strings we ensure that users will
+ # be able to create filters in configuration (e.g. JSON) files too, which
+ # also benefits the documentation by enforcing human-readable filter names.
+ condition: Optional[str] = None
+
+ def __post_init__(self):
+ if self.condition:
+ self.filter = filter_from_string(self.condition)
+ else:
+ self.filter = None
+
+
+def create_identifier(handler: Callable):
+ parts = handler.__module__.split(".")
+ name = handler.__name__
+ package = parts[0]
+ if len(parts) > 1:
+ final_module = parts[-1]
+ return f"{package}:{final_module}.{name}"
+ else:
+ return f"{package}:{name}"
+
+
+AUTO_MATCH_BINDINGS = [
+ *[
+ Binding(
+ cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
+ )
+ for key, cmd in match.auto_match_parens.items()
+ ],
+ *[
+ # raw string
+ Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
+ for key, cmd in match.auto_match_parens_raw_string.items()
+ ],
+ Binding(
+ match.double_quote,
+ ['"'],
+ "focused_insert"
+ " & auto_match"
+ " & not_inside_unclosed_string"
+ " & preceded_by_paired_double_quotes"
+ " & followed_by_closing_paren_or_end",
+ ),
+ Binding(
+ match.single_quote,
+ ["'"],
+ "focused_insert"
+ " & auto_match"
+ " & not_inside_unclosed_string"
+ " & preceded_by_paired_single_quotes"
+ " & followed_by_closing_paren_or_end",
+ ),
+ Binding(
+ match.docstring_double_quotes,
+ ['"'],
+ "focused_insert"
+ " & auto_match"
+ " & not_inside_unclosed_string"
+ " & preceded_by_two_double_quotes",
+ ),
+ Binding(
+ match.docstring_single_quotes,
+ ["'"],
+ "focused_insert"
+ " & auto_match"
+ " & not_inside_unclosed_string"
+ " & preceded_by_two_single_quotes",
+ ),
+ Binding(
+ match.skip_over,
+ [")"],
+ "focused_insert & auto_match & followed_by_closing_round_paren",
+ ),
+ Binding(
+ match.skip_over,
+ ["]"],
+ "focused_insert & auto_match & followed_by_closing_bracket",
+ ),
+ Binding(
+ match.skip_over,
+ ["}"],
+ "focused_insert & auto_match & followed_by_closing_brace",
+ ),
+ Binding(
+ match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
+ ),
+ Binding(
+ match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
+ ),
+ Binding(
+ match.delete_pair,
+ ["backspace"],
+ "focused_insert"
+ " & preceded_by_opening_round_paren"
+ " & auto_match"
+ " & followed_by_closing_round_paren",
+ ),
+ Binding(
+ match.delete_pair,
+ ["backspace"],
+ "focused_insert"
+ " & preceded_by_opening_bracket"
+ " & auto_match"
+ " & followed_by_closing_bracket",
+ ),
+ Binding(
+ match.delete_pair,
+ ["backspace"],
+ "focused_insert"
+ " & preceded_by_opening_brace"
+ " & auto_match"
+ " & followed_by_closing_brace",
+ ),
+ Binding(
+ match.delete_pair,
+ ["backspace"],
+ "focused_insert"
+ " & preceded_by_double_quote"
+ " & auto_match"
+ " & followed_by_double_quote",
+ ),
+ Binding(
+ match.delete_pair,
+ ["backspace"],
+ "focused_insert"
+ " & preceded_by_single_quote"
+ " & auto_match"
+ " & followed_by_single_quote",
+ ),
+]
+
+AUTO_SUGGEST_BINDINGS = [
+ # there are two reasons for re-defining bindings defined upstream:
+ # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
+ # 2) prompt-toolkit checks if we are at the end of text, not end of line
+ # hence it does not work in multi-line mode of navigable provider
+ Binding(
+ auto_suggest.accept_or_jump_to_end,
+ ["end"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept_or_jump_to_end,
+ ["c-e"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept,
+ ["c-f"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept,
+ ["right"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept_word,
+ ["escape", "f"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept_token,
+ ["c-right"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.discard,
+ ["escape"],
+ # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
+ # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
+ "has_suggestion & default_buffer_focused & emacs_insert_mode",
+ ),
+ Binding(
+ auto_suggest.discard,
+ ["delete"],
+ "has_suggestion & default_buffer_focused & emacs_insert_mode",
+ ),
+ Binding(
+ auto_suggest.swap_autosuggestion_up,
+ ["c-up"],
+ "navigable_suggestions"
+ " & ~has_line_above"
+ " & has_suggestion"
+ " & default_buffer_focused",
+ ),
+ Binding(
+ auto_suggest.swap_autosuggestion_down,
+ ["c-down"],
+ "navigable_suggestions"
+ " & ~has_line_below"
+ " & has_suggestion"
+ " & default_buffer_focused",
+ ),
+ Binding(
+ auto_suggest.up_and_update_hint,
+ ["c-up"],
+ "has_line_above & navigable_suggestions & default_buffer_focused",
+ ),
+ Binding(
+ auto_suggest.down_and_update_hint,
+ ["c-down"],
+ "has_line_below & navigable_suggestions & default_buffer_focused",
+ ),
+ Binding(
+ auto_suggest.accept_character,
+ ["escape", "right"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept_and_move_cursor_left,
+ ["c-left"],
+ "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.accept_and_keep_cursor,
+ ["escape", "down"],
+ "has_suggestion & default_buffer_focused & emacs_insert_mode",
+ ),
+ Binding(
+ auto_suggest.backspace_and_resume_hint,
+ ["backspace"],
+ # no `has_suggestion` here to allow resuming if no suggestion
+ "default_buffer_focused & emacs_like_insert_mode",
+ ),
+ Binding(
+ auto_suggest.resume_hinting,
+ ["right"],
+ "is_cursor_at_the_end_of_line"
+ " & default_buffer_focused"
+ " & emacs_like_insert_mode"
+ " & pass_through",
+ ),
+]
+
+
+SIMPLE_CONTROL_BINDINGS = [
+ Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
+ for key, cmd in {
+ "c-a": nc.beginning_of_line,
+ "c-b": nc.backward_char,
+ "c-k": nc.kill_line,
+ "c-w": nc.backward_kill_word,
+ "c-y": nc.yank,
+ "c-_": nc.undo,
+ }.items()
+]
+
+
+ALT_AND_COMOBO_CONTROL_BINDINGS = [
+ Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
+ for keys, cmd in {
+ # Control Combos
+ ("c-x", "c-e"): nc.edit_and_execute,
+ ("c-x", "e"): nc.edit_and_execute,
+ # Alt
+ ("escape", "b"): nc.backward_word,
+ ("escape", "c"): nc.capitalize_word,
+ ("escape", "d"): nc.kill_word,
+ ("escape", "h"): nc.backward_kill_word,
+ ("escape", "l"): nc.downcase_word,
+ ("escape", "u"): nc.uppercase_word,
+ ("escape", "y"): nc.yank_pop,
+ ("escape", "."): nc.yank_last_arg,
+ }.items()
+]
+
+
+def add_binding(bindings: KeyBindings, binding: Binding):
+ bindings.add(
+ *binding.keys,
+ **({"filter": binding.filter} if binding.filter is not None else {}),
+ )(binding.command)
+
+
+def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
+ """Set up the prompt_toolkit keyboard shortcuts for IPython.
+
+ Parameters
+ ----------
+ shell: InteractiveShell
+ The current IPython shell Instance
+ skip: List[Binding]
+ Bindings to skip.
+
+ Returns
+ -------
+ KeyBindings
+ the keybinding instance for prompt toolkit.
+
+ """
+ kb = KeyBindings()
+ skip = skip or []
+ for binding in KEY_BINDINGS:
+ skip_this_one = False
+ for to_skip in skip:
+ if (
+ to_skip.command == binding.command
+ and to_skip.filter == binding.filter
+ and to_skip.keys == binding.keys
+ ):
+ skip_this_one = True
+ break
+ if skip_this_one:
+ continue
+ add_binding(kb, binding)
+
+ def get_input_mode(self):
+ app = get_app()
+ app.ttimeoutlen = shell.ttimeoutlen
+ app.timeoutlen = shell.timeoutlen
+
+ return self._input_mode
+
+ def set_input_mode(self, mode):
+ shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
+ cursor = "\x1b[{} q".format(shape)
+
+ sys.stdout.write(cursor)
+ sys.stdout.flush()
+
+ self._input_mode = mode
+
+ if shell.editing_mode == "vi" and shell.modal_cursor:
+ ViState._input_mode = InputMode.INSERT # type: ignore
+ ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
+
+ return kb
+
+
+def reformat_and_execute(event):
+ """Reformat code and execute it"""
+ shell = get_ipython()
+ reformat_text_before_cursor(
+ event.current_buffer, event.current_buffer.document, shell
+ )
+ event.current_buffer.validate_and_handle()
+
+
+def reformat_text_before_cursor(buffer, document, shell):
+ text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
+ try:
+ formatted_text = shell.reformat_handler(text)
+ buffer.insert_text(formatted_text)
+ except Exception as e:
+ buffer.insert_text(text)
+
+
+def handle_return_or_newline_or_execute(event):
+ shell = get_ipython()
+ if getattr(shell, "handle_return", None):
+ return shell.handle_return(shell)(event)
+ else:
+ return newline_or_execute_outer(shell)(event)
+
+
+def newline_or_execute_outer(shell):
+ def newline_or_execute(event):
+ """When the user presses return, insert a newline or execute the code."""
+ b = event.current_buffer
+ d = b.document
+
+ if b.complete_state:
+ cc = b.complete_state.current_completion
+ if cc:
+ b.apply_completion(cc)
+ else:
+ b.cancel_completion()
+ return
+
+ # If there's only one line, treat it as if the cursor is at the end.
+ # See https://github.com/ipython/ipython/issues/10425
+ if d.line_count == 1:
+ check_text = d.text
+ else:
+ check_text = d.text[: d.cursor_position]
+ status, indent = shell.check_complete(check_text)
+
+ # if all we have after the cursor is whitespace: reformat current text
+ # before cursor
+ after_cursor = d.text[d.cursor_position :]
+ reformatted = False
+ if not after_cursor.strip():
+ reformat_text_before_cursor(b, d, shell)
+ reformatted = True
+ if not (
+ d.on_last_line
+ or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
+ ):
+ if shell.autoindent:
+ b.insert_text("\n" + indent)
+ else:
+ b.insert_text("\n")
+ return
+
+ if (status != "incomplete") and b.accept_handler:
+ if not reformatted:
+ reformat_text_before_cursor(b, d, shell)
+ b.validate_and_handle()
+ else:
+ if shell.autoindent:
+ b.insert_text("\n" + indent)
+ else:
+ b.insert_text("\n")
+
+ return newline_or_execute
+
+
+def previous_history_or_previous_completion(event):
+ """
+ Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
+
+ If completer is open this still select previous completion.
+ """
+ event.current_buffer.auto_up()
+
+
+def next_history_or_next_completion(event):
+ """
+ Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
+
+ If completer is open this still select next completion.
+ """
+ event.current_buffer.auto_down()
+
+
+def dismiss_completion(event):
+ """Dismiss completion"""
+ b = event.current_buffer
+ if b.complete_state:
+ b.cancel_completion()
+
+
+def reset_buffer(event):
+ """Reset buffer"""
+ b = event.current_buffer
+ if b.complete_state:
+ b.cancel_completion()
+ else:
+ b.reset()
+
+
+def reset_search_buffer(event):
+ """Reset search buffer"""
+ if event.current_buffer.document.text:
+ event.current_buffer.reset()
+ else:
+ event.app.layout.focus(DEFAULT_BUFFER)
+
+
+def suspend_to_bg(event):
+ """Suspend to background"""
+ event.app.suspend_to_background()
+
+
+def quit(event):
+ """
+ Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
+
+ On platforms that support SIGQUIT, send SIGQUIT to the current process.
+ On other platforms, just exit the process with a message.
+ """
+ sigquit = getattr(signal, "SIGQUIT", None)
+ if sigquit is not None:
+ os.kill(0, signal.SIGQUIT)
+ else:
+ sys.exit("Quit")
+
+
+def indent_buffer(event):
+ """Indent buffer"""
+ event.current_buffer.insert_text(" " * 4)
+
+
+def newline_autoindent(event):
+ """Insert a newline after the cursor indented appropriately.
+
+ Fancier version of former ``newline_with_copy_margin`` which should
+ compute the correct indentation of the inserted line. That is to say, indent
+ by 4 extra space after a function definition, class definition, context
+ manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
+ """
+ shell = get_ipython()
+ inputsplitter = shell.input_transformer_manager
+ b = event.current_buffer
+ d = b.document
+
+ if b.complete_state:
+ b.cancel_completion()
+ text = d.text[: d.cursor_position] + "\n"
+ _, indent = inputsplitter.check_complete(text)
+ b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
+
+
+def open_input_in_editor(event):
+ """Open code from input in external editor"""
+ event.app.current_buffer.open_in_editor()
+
+
+if sys.platform == "win32":
+ from IPython.core.error import TryNext
+ from IPython.lib.clipboard import (
+ ClipboardEmpty,
+ tkinter_clipboard_get,
+ win32_clipboard_get,
+ )
+
+ @undoc
+ def win_paste(event):
+ try:
+ text = win32_clipboard_get()
+ except TryNext:
+ try:
+ text = tkinter_clipboard_get()
+ except (TryNext, ClipboardEmpty):
+ return
+ except ClipboardEmpty:
+ return
+ event.current_buffer.insert_text(text.replace("\t", " " * 4))
+
+else:
+
+ @undoc
+ def win_paste(event):
+ """Stub used on other platforms"""
+ pass
+
+
+KEY_BINDINGS = [
+ Binding(
+ handle_return_or_newline_or_execute,
+ ["enter"],
+ "default_buffer_focused & ~has_selection & insert_mode",
+ ),
+ Binding(
+ reformat_and_execute,
+ ["escape", "enter"],
+ "default_buffer_focused & ~has_selection & insert_mode & ebivim",
+ ),
+ Binding(quit, ["c-\\"]),
+ Binding(
+ previous_history_or_previous_completion,
+ ["c-p"],
+ "vi_insert_mode & default_buffer_focused",
+ ),
+ Binding(
+ next_history_or_next_completion,
+ ["c-n"],
+ "vi_insert_mode & default_buffer_focused",
+ ),
+ Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
+ Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
+ Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
+ Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
+ Binding(
+ indent_buffer,
+ ["tab"], # Ctrl+I == Tab
+ "default_buffer_focused"
+ " & ~has_selection"
+ " & insert_mode"
+ " & cursor_in_leading_ws",
+ ),
+ Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
+ Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
+ *AUTO_MATCH_BINDINGS,
+ *AUTO_SUGGEST_BINDINGS,
+ Binding(
+ display_completions_like_readline,
+ ["c-i"],
+ "readline_like_completions"
+ " & default_buffer_focused"
+ " & ~has_selection"
+ " & insert_mode"
+ " & ~cursor_in_leading_ws",
+ ),
+ Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
+ *SIMPLE_CONTROL_BINDINGS,
+ *ALT_AND_COMOBO_CONTROL_BINDINGS,
+]
diff --git a/contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_match.py b/contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_match.py
new file mode 100644
index 0000000000..6c2b1ef70c
--- /dev/null
+++ b/contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_match.py
@@ -0,0 +1,104 @@
+"""
+Utilities function for keybinding with prompt toolkit.
+
+This will be bound to specific key press and filter modes,
+like whether we are in edit mode, and whether the completer is open.
+"""
+import re
+from prompt_toolkit.key_binding import KeyPressEvent
+
+
+def parenthesis(event: KeyPressEvent):
+ """Auto-close parenthesis"""
+ event.current_buffer.insert_text("()")
+ event.current_buffer.cursor_left()
+
+
+def brackets(event: KeyPressEvent):
+ """Auto-close brackets"""
+ event.current_buffer.insert_text("[]")
+ event.current_buffer.cursor_left()
+
+
+def braces(event: KeyPressEvent):
+ """Auto-close braces"""
+ event.current_buffer.insert_text("{}")
+ event.current_buffer.cursor_left()
+
+
+def double_quote(event: KeyPressEvent):
+ """Auto-close double quotes"""
+ event.current_buffer.insert_text('""')
+ event.current_buffer.cursor_left()
+
+
+def single_quote(event: KeyPressEvent):
+ """Auto-close single quotes"""
+ event.current_buffer.insert_text("''")
+ event.current_buffer.cursor_left()
+
+
+def docstring_double_quotes(event: KeyPressEvent):
+ """Auto-close docstring (double quotes)"""
+ event.current_buffer.insert_text('""""')
+ event.current_buffer.cursor_left(3)
+
+
+def docstring_single_quotes(event: KeyPressEvent):
+ """Auto-close docstring (single quotes)"""
+ event.current_buffer.insert_text("''''")
+ event.current_buffer.cursor_left(3)
+
+
+def raw_string_parenthesis(event: KeyPressEvent):
+ """Auto-close parenthesis in raw strings"""
+ matches = re.match(
+ r".*(r|R)[\"'](-*)",
+ event.current_buffer.document.current_line_before_cursor,
+ )
+ dashes = matches.group(2) if matches else ""
+ event.current_buffer.insert_text("()" + dashes)
+ event.current_buffer.cursor_left(len(dashes) + 1)
+
+
+def raw_string_bracket(event: KeyPressEvent):
+ """Auto-close bracker in raw strings"""
+ matches = re.match(
+ r".*(r|R)[\"'](-*)",
+ event.current_buffer.document.current_line_before_cursor,
+ )
+ dashes = matches.group(2) if matches else ""
+ event.current_buffer.insert_text("[]" + dashes)
+ event.current_buffer.cursor_left(len(dashes) + 1)
+
+
+def raw_string_braces(event: KeyPressEvent):
+ """Auto-close braces in raw strings"""
+ matches = re.match(
+ r".*(r|R)[\"'](-*)",
+ event.current_buffer.document.current_line_before_cursor,
+ )
+ dashes = matches.group(2) if matches else ""
+ event.current_buffer.insert_text("{}" + dashes)
+ event.current_buffer.cursor_left(len(dashes) + 1)
+
+
+def skip_over(event: KeyPressEvent):
+ """Skip over automatically added parenthesis/quote.
+
+ (rather than adding another parenthesis/quote)"""
+ event.current_buffer.cursor_right()
+
+
+def delete_pair(event: KeyPressEvent):
+ """Delete auto-closed parenthesis"""
+ event.current_buffer.delete()
+ event.current_buffer.delete_before_cursor()
+
+
+auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
+auto_match_parens_raw_string = {
+ "(": raw_string_parenthesis,
+ "[": raw_string_bracket,
+ "{": raw_string_braces,
+}
diff --git a/contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_suggest.py b/contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_suggest.py
new file mode 100644
index 0000000000..65f91577ce
--- /dev/null
+++ b/contrib/python/ipython/py3/IPython/terminal/shortcuts/auto_suggest.py
@@ -0,0 +1,401 @@
+import re
+import tokenize
+from io import StringIO
+from typing import Callable, List, Optional, Union, Generator, Tuple
+import warnings
+
+from prompt_toolkit.buffer import Buffer
+from prompt_toolkit.key_binding import KeyPressEvent
+from prompt_toolkit.key_binding.bindings import named_commands as nc
+from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
+from prompt_toolkit.document import Document
+from prompt_toolkit.history import History
+from prompt_toolkit.shortcuts import PromptSession
+from prompt_toolkit.layout.processors import (
+ Processor,
+ Transformation,
+ TransformationInput,
+)
+
+from IPython.core.getipython import get_ipython
+from IPython.utils.tokenutil import generate_tokens
+
+from .filters import pass_through
+
+
+def _get_query(document: Document):
+ return document.lines[document.cursor_position_row]
+
+
+class AppendAutoSuggestionInAnyLine(Processor):
+ """
+ Append the auto suggestion to lines other than the last (appending to the
+ last line is natively supported by the prompt toolkit).
+ """
+
+ def __init__(self, style: str = "class:auto-suggestion") -> None:
+ self.style = style
+
+ def apply_transformation(self, ti: TransformationInput) -> Transformation:
+ is_last_line = ti.lineno == ti.document.line_count - 1
+ is_active_line = ti.lineno == ti.document.cursor_position_row
+
+ if not is_last_line and is_active_line:
+ buffer = ti.buffer_control.buffer
+
+ if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
+ suggestion = buffer.suggestion.text
+ else:
+ suggestion = ""
+
+ return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
+ else:
+ return Transformation(fragments=ti.fragments)
+
+
+class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
+ """
+ A subclass of AutoSuggestFromHistory that allow navigation to next/previous
+ suggestion from history. To do so it remembers the current position, but it
+ state need to carefully be cleared on the right events.
+ """
+
+ def __init__(
+ self,
+ ):
+ self.skip_lines = 0
+ self._connected_apps = []
+
+ def reset_history_position(self, _: Buffer):
+ self.skip_lines = 0
+
+ def disconnect(self):
+ for pt_app in self._connected_apps:
+ text_insert_event = pt_app.default_buffer.on_text_insert
+ text_insert_event.remove_handler(self.reset_history_position)
+
+ def connect(self, pt_app: PromptSession):
+ self._connected_apps.append(pt_app)
+ # note: `on_text_changed` could be used for a bit different behaviour
+ # on character deletion (i.e. reseting history position on backspace)
+ pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
+ pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
+
+ def get_suggestion(
+ self, buffer: Buffer, document: Document
+ ) -> Optional[Suggestion]:
+ text = _get_query(document)
+
+ if text.strip():
+ for suggestion, _ in self._find_next_match(
+ text, self.skip_lines, buffer.history
+ ):
+ return Suggestion(suggestion)
+
+ return None
+
+ def _dismiss(self, buffer, *args, **kwargs):
+ buffer.suggestion = None
+
+ def _find_match(
+ self, text: str, skip_lines: float, history: History, previous: bool
+ ) -> Generator[Tuple[str, float], None, None]:
+ """
+ text : str
+ Text content to find a match for, the user cursor is most of the
+ time at the end of this text.
+ skip_lines : float
+ number of items to skip in the search, this is used to indicate how
+ far in the list the user has navigated by pressing up or down.
+ The float type is used as the base value is +inf
+ history : History
+ prompt_toolkit History instance to fetch previous entries from.
+ previous : bool
+ Direction of the search, whether we are looking previous match
+ (True), or next match (False).
+
+ Yields
+ ------
+ Tuple with:
+ str:
+ current suggestion.
+ float:
+ will actually yield only ints, which is passed back via skip_lines,
+ which may be a +inf (float)
+
+
+ """
+ line_number = -1
+ for string in reversed(list(history.get_strings())):
+ for line in reversed(string.splitlines()):
+ line_number += 1
+ if not previous and line_number < skip_lines:
+ continue
+ # do not return empty suggestions as these
+ # close the auto-suggestion overlay (and are useless)
+ if line.startswith(text) and len(line) > len(text):
+ yield line[len(text) :], line_number
+ if previous and line_number >= skip_lines:
+ return
+
+ def _find_next_match(
+ self, text: str, skip_lines: float, history: History
+ ) -> Generator[Tuple[str, float], None, None]:
+ return self._find_match(text, skip_lines, history, previous=False)
+
+ def _find_previous_match(self, text: str, skip_lines: float, history: History):
+ return reversed(
+ list(self._find_match(text, skip_lines, history, previous=True))
+ )
+
+ def up(self, query: str, other_than: str, history: History) -> None:
+ for suggestion, line_number in self._find_next_match(
+ query, self.skip_lines, history
+ ):
+ # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
+ # we want to switch from 'very.b' to 'very.a' because a) if the
+ # suggestion equals current text, prompt-toolkit aborts suggesting
+ # b) user likely would not be interested in 'very' anyways (they
+ # already typed it).
+ if query + suggestion != other_than:
+ self.skip_lines = line_number
+ break
+ else:
+ # no matches found, cycle back to beginning
+ self.skip_lines = 0
+
+ def down(self, query: str, other_than: str, history: History) -> None:
+ for suggestion, line_number in self._find_previous_match(
+ query, self.skip_lines, history
+ ):
+ if query + suggestion != other_than:
+ self.skip_lines = line_number
+ break
+ else:
+ # no matches found, cycle to end
+ for suggestion, line_number in self._find_previous_match(
+ query, float("Inf"), history
+ ):
+ if query + suggestion != other_than:
+ self.skip_lines = line_number
+ break
+
+
+def accept_or_jump_to_end(event: KeyPressEvent):
+ """Apply autosuggestion or jump to end of line."""
+ buffer = event.current_buffer
+ d = buffer.document
+ after_cursor = d.text[d.cursor_position :]
+ lines = after_cursor.split("\n")
+ end_of_current_line = lines[0].strip()
+ suggestion = buffer.suggestion
+ if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
+ buffer.insert_text(suggestion.text)
+ else:
+ nc.end_of_line(event)
+
+
+def _deprected_accept_in_vi_insert_mode(event: KeyPressEvent):
+ """Accept autosuggestion or jump to end of line.
+
+ .. deprecated:: 8.12
+ Use `accept_or_jump_to_end` instead.
+ """
+ return accept_or_jump_to_end(event)
+
+
+def accept(event: KeyPressEvent):
+ """Accept autosuggestion"""
+ buffer = event.current_buffer
+ suggestion = buffer.suggestion
+ if suggestion:
+ buffer.insert_text(suggestion.text)
+ else:
+ nc.forward_char(event)
+
+
+def discard(event: KeyPressEvent):
+ """Discard autosuggestion"""
+ buffer = event.current_buffer
+ buffer.suggestion = None
+
+
+def accept_word(event: KeyPressEvent):
+ """Fill partial autosuggestion by word"""
+ buffer = event.current_buffer
+ suggestion = buffer.suggestion
+ if suggestion:
+ t = re.split(r"(\S+\s+)", suggestion.text)
+ buffer.insert_text(next((x for x in t if x), ""))
+ else:
+ nc.forward_word(event)
+
+
+def accept_character(event: KeyPressEvent):
+ """Fill partial autosuggestion by character"""
+ b = event.current_buffer
+ suggestion = b.suggestion
+ if suggestion and suggestion.text:
+ b.insert_text(suggestion.text[0])
+
+
+def accept_and_keep_cursor(event: KeyPressEvent):
+ """Accept autosuggestion and keep cursor in place"""
+ buffer = event.current_buffer
+ old_position = buffer.cursor_position
+ suggestion = buffer.suggestion
+ if suggestion:
+ buffer.insert_text(suggestion.text)
+ buffer.cursor_position = old_position
+
+
+def accept_and_move_cursor_left(event: KeyPressEvent):
+ """Accept autosuggestion and move cursor left in place"""
+ accept_and_keep_cursor(event)
+ nc.backward_char(event)
+
+
+def _update_hint(buffer: Buffer):
+ if buffer.auto_suggest:
+ suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
+ buffer.suggestion = suggestion
+
+
+def backspace_and_resume_hint(event: KeyPressEvent):
+ """Resume autosuggestions after deleting last character"""
+ nc.backward_delete_char(event)
+ _update_hint(event.current_buffer)
+
+
+def resume_hinting(event: KeyPressEvent):
+ """Resume autosuggestions"""
+ pass_through.reply(event)
+ # Order matters: if update happened first and event reply second, the
+ # suggestion would be auto-accepted if both actions are bound to same key.
+ _update_hint(event.current_buffer)
+
+
+def up_and_update_hint(event: KeyPressEvent):
+ """Go up and update hint"""
+ current_buffer = event.current_buffer
+
+ current_buffer.auto_up(count=event.arg)
+ _update_hint(current_buffer)
+
+
+def down_and_update_hint(event: KeyPressEvent):
+ """Go down and update hint"""
+ current_buffer = event.current_buffer
+
+ current_buffer.auto_down(count=event.arg)
+ _update_hint(current_buffer)
+
+
+def accept_token(event: KeyPressEvent):
+ """Fill partial autosuggestion by token"""
+ b = event.current_buffer
+ suggestion = b.suggestion
+
+ if suggestion:
+ prefix = _get_query(b.document)
+ text = prefix + suggestion.text
+
+ tokens: List[Optional[str]] = [None, None, None]
+ substrings = [""]
+ i = 0
+
+ for token in generate_tokens(StringIO(text).readline):
+ if token.type == tokenize.NEWLINE:
+ index = len(text)
+ else:
+ index = text.index(token[1], len(substrings[-1]))
+ substrings.append(text[:index])
+ tokenized_so_far = substrings[-1]
+ if tokenized_so_far.startswith(prefix):
+ if i == 0 and len(tokenized_so_far) > len(prefix):
+ tokens[0] = tokenized_so_far[len(prefix) :]
+ substrings.append(tokenized_so_far)
+ i += 1
+ tokens[i] = token[1]
+ if i == 2:
+ break
+ i += 1
+
+ if tokens[0]:
+ to_insert: str
+ insert_text = substrings[-2]
+ if tokens[1] and len(tokens[1]) == 1:
+ insert_text = substrings[-1]
+ to_insert = insert_text[len(prefix) :]
+ b.insert_text(to_insert)
+ return
+
+ nc.forward_word(event)
+
+
+Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
+
+
+def _swap_autosuggestion(
+ buffer: Buffer,
+ provider: NavigableAutoSuggestFromHistory,
+ direction_method: Callable,
+):
+ """
+ We skip most recent history entry (in either direction) if it equals the
+ current autosuggestion because if user cycles when auto-suggestion is shown
+ they most likely want something else than what was suggested (otherwise
+ they would have accepted the suggestion).
+ """
+ suggestion = buffer.suggestion
+ if not suggestion:
+ return
+
+ query = _get_query(buffer.document)
+ current = query + suggestion.text
+
+ direction_method(query=query, other_than=current, history=buffer.history)
+
+ new_suggestion = provider.get_suggestion(buffer, buffer.document)
+ buffer.suggestion = new_suggestion
+
+
+def swap_autosuggestion_up(event: KeyPressEvent):
+ """Get next autosuggestion from history."""
+ shell = get_ipython()
+ provider = shell.auto_suggest
+
+ if not isinstance(provider, NavigableAutoSuggestFromHistory):
+ return
+
+ return _swap_autosuggestion(
+ buffer=event.current_buffer, provider=provider, direction_method=provider.up
+ )
+
+
+def swap_autosuggestion_down(event: KeyPressEvent):
+ """Get previous autosuggestion from history."""
+ shell = get_ipython()
+ provider = shell.auto_suggest
+
+ if not isinstance(provider, NavigableAutoSuggestFromHistory):
+ return
+
+ return _swap_autosuggestion(
+ buffer=event.current_buffer,
+ provider=provider,
+ direction_method=provider.down,
+ )
+
+
+def __getattr__(key):
+ if key == "accept_in_vi_insert_mode":
+ warnings.warn(
+ "`accept_in_vi_insert_mode` is deprecated since IPython 8.12 and "
+ "renamed to `accept_or_jump_to_end`. Please update your configuration "
+ "accordingly",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _deprected_accept_in_vi_insert_mode
+ raise AttributeError
diff --git a/contrib/python/ipython/py3/IPython/terminal/shortcuts/filters.py b/contrib/python/ipython/py3/IPython/terminal/shortcuts/filters.py
new file mode 100644
index 0000000000..7c9d6a9c41
--- /dev/null
+++ b/contrib/python/ipython/py3/IPython/terminal/shortcuts/filters.py
@@ -0,0 +1,322 @@
+"""
+Filters restricting scope of IPython Terminal shortcuts.
+"""
+
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import ast
+import re
+import signal
+import sys
+from typing import Callable, Dict, Union
+
+from prompt_toolkit.application.current import get_app
+from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
+from prompt_toolkit.key_binding import KeyPressEvent
+from prompt_toolkit.filters import Condition, Filter, emacs_insert_mode, has_completions
+from prompt_toolkit.filters import has_focus as has_focus_impl
+from prompt_toolkit.filters import (
+ Always,
+ Never,
+ has_selection,
+ has_suggestion,
+ vi_insert_mode,
+ vi_mode,
+)
+from prompt_toolkit.layout.layout import FocusableElement
+
+from IPython.core.getipython import get_ipython
+from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
+from IPython.terminal.shortcuts import auto_suggest
+from IPython.utils.decorators import undoc
+
+
+@undoc
+@Condition
+def cursor_in_leading_ws():
+ before = get_app().current_buffer.document.current_line_before_cursor
+ return (not before) or before.isspace()
+
+
+def has_focus(value: FocusableElement):
+ """Wrapper around has_focus adding a nice `__name__` to tester function"""
+ tester = has_focus_impl(value).func
+ tester.__name__ = f"is_focused({value})"
+ return Condition(tester)
+
+
+@undoc
+@Condition
+def has_line_below() -> bool:
+ document = get_app().current_buffer.document
+ return document.cursor_position_row < len(document.lines) - 1
+
+
+@undoc
+@Condition
+def is_cursor_at_the_end_of_line() -> bool:
+ document = get_app().current_buffer.document
+ return document.is_cursor_at_the_end_of_line
+
+
+@undoc
+@Condition
+def has_line_above() -> bool:
+ document = get_app().current_buffer.document
+ return document.cursor_position_row != 0
+
+
+@Condition
+def ebivim():
+ shell = get_ipython()
+ return shell.emacs_bindings_in_vi_insert_mode
+
+
+@Condition
+def supports_suspend():
+ return hasattr(signal, "SIGTSTP")
+
+
+@Condition
+def auto_match():
+ shell = get_ipython()
+ return shell.auto_match
+
+
+def all_quotes_paired(quote, buf):
+ paired = True
+ i = 0
+ while i < len(buf):
+ c = buf[i]
+ if c == quote:
+ paired = not paired
+ elif c == "\\":
+ i += 1
+ i += 1
+ return paired
+
+
+_preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
+_following_text_cache: Dict[Union[str, Callable], Condition] = {}
+
+
+def preceding_text(pattern: Union[str, Callable]):
+ if pattern in _preceding_text_cache:
+ return _preceding_text_cache[pattern]
+
+ if callable(pattern):
+
+ def _preceding_text():
+ app = get_app()
+ before_cursor = app.current_buffer.document.current_line_before_cursor
+ # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
+ return bool(pattern(before_cursor)) # type: ignore[operator]
+
+ else:
+ m = re.compile(pattern)
+
+ def _preceding_text():
+ app = get_app()
+ before_cursor = app.current_buffer.document.current_line_before_cursor
+ return bool(m.match(before_cursor))
+
+ _preceding_text.__name__ = f"preceding_text({pattern!r})"
+
+ condition = Condition(_preceding_text)
+ _preceding_text_cache[pattern] = condition
+ return condition
+
+
+def following_text(pattern):
+ try:
+ return _following_text_cache[pattern]
+ except KeyError:
+ pass
+ m = re.compile(pattern)
+
+ def _following_text():
+ app = get_app()
+ return bool(m.match(app.current_buffer.document.current_line_after_cursor))
+
+ _following_text.__name__ = f"following_text({pattern!r})"
+
+ condition = Condition(_following_text)
+ _following_text_cache[pattern] = condition
+ return condition
+
+
+@Condition
+def not_inside_unclosed_string():
+ app = get_app()
+ s = app.current_buffer.document.text_before_cursor
+ # remove escaped quotes
+ s = s.replace('\\"', "").replace("\\'", "")
+ # remove triple-quoted string literals
+ s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
+ # remove single-quoted string literals
+ s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
+ return not ('"' in s or "'" in s)
+
+
+@Condition
+def navigable_suggestions():
+ shell = get_ipython()
+ return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
+
+
+@Condition
+def readline_like_completions():
+ shell = get_ipython()
+ return shell.display_completions == "readlinelike"
+
+
+@Condition
+def is_windows_os():
+ return sys.platform == "win32"
+
+
+class PassThrough(Filter):
+ """A filter allowing to implement pass-through behaviour of keybindings.
+
+ Prompt toolkit key processor dispatches only one event per binding match,
+ which means that adding a new shortcut will suppress the old shortcut
+ if the keybindings are the same (unless one is filtered out).
+
+ To stop a shortcut binding from suppressing other shortcuts:
+ - add the `pass_through` filter to list of filter, and
+ - call `pass_through.reply(event)` in the shortcut handler.
+ """
+
+ def __init__(self):
+ self._is_replying = False
+
+ def reply(self, event: KeyPressEvent):
+ self._is_replying = True
+ try:
+ event.key_processor.reset()
+ event.key_processor.feed_multiple(event.key_sequence)
+ event.key_processor.process_keys()
+ finally:
+ self._is_replying = False
+
+ def __call__(self):
+ return not self._is_replying
+
+
+pass_through = PassThrough()
+
+# these one is callable and re-used multiple times hence needs to be
+# only defined once beforhand so that transforming back to human-readable
+# names works well in the documentation.
+default_buffer_focused = has_focus(DEFAULT_BUFFER)
+
+KEYBINDING_FILTERS = {
+ "always": Always(),
+ # never is used for exposing commands which have no default keybindings
+ "never": Never(),
+ "has_line_below": has_line_below,
+ "has_line_above": has_line_above,
+ "is_cursor_at_the_end_of_line": is_cursor_at_the_end_of_line,
+ "has_selection": has_selection,
+ "has_suggestion": has_suggestion,
+ "vi_mode": vi_mode,
+ "vi_insert_mode": vi_insert_mode,
+ "emacs_insert_mode": emacs_insert_mode,
+ # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
+ # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
+ # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
+ # in vi insert mode. Because some of the emacs bindings involve `escape`
+ # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
+ # needs to wait to see if there will be another character typed in before
+ # executing pure `escape` keybinding; in vi insert mode `escape` switches to
+ # command mode which is common and performance critical action for vi users.
+ # To avoid the delay users employ a workaround:
+ # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
+ # which involves switching `emacs_bindings_in_vi_insert_mode` off.
+ #
+ # For the workaround to work:
+ # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
+ # 2) all keybindings which would involve `escape` need to respect that
+ # toggle by including either:
+ # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
+ # predefined upstream in prompt-toolkit, or
+ # - `emacs_like_insert_mode` for actions which do not have existing
+ # emacs keybindings predefined upstream (or need overriding of the
+ # upstream bindings to modify behaviour), defined below.
+ "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
+ "has_completions": has_completions,
+ "insert_mode": vi_insert_mode | emacs_insert_mode,
+ "default_buffer_focused": default_buffer_focused,
+ "search_buffer_focused": has_focus(SEARCH_BUFFER),
+ # `ebivim` stands for emacs bindings in vi insert mode
+ "ebivim": ebivim,
+ "supports_suspend": supports_suspend,
+ "is_windows_os": is_windows_os,
+ "auto_match": auto_match,
+ "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
+ "not_inside_unclosed_string": not_inside_unclosed_string,
+ "readline_like_completions": readline_like_completions,
+ "preceded_by_paired_double_quotes": preceding_text(
+ lambda line: all_quotes_paired('"', line)
+ ),
+ "preceded_by_paired_single_quotes": preceding_text(
+ lambda line: all_quotes_paired("'", line)
+ ),
+ "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
+ "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
+ "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
+ "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
+ "preceded_by_opening_round_paren": preceding_text(r".*\($"),
+ "preceded_by_opening_bracket": preceding_text(r".*\[$"),
+ "preceded_by_opening_brace": preceding_text(r".*\{$"),
+ "preceded_by_double_quote": preceding_text('.*"$'),
+ "preceded_by_single_quote": preceding_text(r".*'$"),
+ "followed_by_closing_round_paren": following_text(r"^\)"),
+ "followed_by_closing_bracket": following_text(r"^\]"),
+ "followed_by_closing_brace": following_text(r"^\}"),
+ "followed_by_double_quote": following_text('^"'),
+ "followed_by_single_quote": following_text("^'"),
+ "navigable_suggestions": navigable_suggestions,
+ "cursor_in_leading_ws": cursor_in_leading_ws,
+ "pass_through": pass_through,
+}
+
+
+def eval_node(node: Union[ast.AST, None]):
+ if node is None:
+ return None
+ if isinstance(node, ast.Expression):
+ return eval_node(node.body)
+ if isinstance(node, ast.BinOp):
+ left = eval_node(node.left)
+ right = eval_node(node.right)
+ dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
+ if dunders:
+ return getattr(left, dunders[0])(right)
+ raise ValueError(f"Unknown binary operation: {node.op}")
+ if isinstance(node, ast.UnaryOp):
+ value = eval_node(node.operand)
+ dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
+ if dunders:
+ return getattr(value, dunders[0])()
+ raise ValueError(f"Unknown unary operation: {node.op}")
+ if isinstance(node, ast.Name):
+ if node.id in KEYBINDING_FILTERS:
+ return KEYBINDING_FILTERS[node.id]
+ else:
+ sep = "\n - "
+ known_filters = sep.join(sorted(KEYBINDING_FILTERS))
+ raise NameError(
+ f"{node.id} is not a known shortcut filter."
+ f" Known filters are: {sep}{known_filters}."
+ )
+ raise ValueError("Unhandled node", ast.dump(node))
+
+
+def filter_from_string(code: str):
+ expression = ast.parse(code, mode="eval")
+ return eval_node(expression)
+
+
+__all__ = ["KEYBINDING_FILTERS", "filter_from_string"]