diff options
author | robot-contrib <robot-contrib@yandex-team.ru> | 2022-05-18 00:43:36 +0300 |
---|---|---|
committer | robot-contrib <robot-contrib@yandex-team.ru> | 2022-05-18 00:43:36 +0300 |
commit | 9e5f436a8b2a27bcc7802e443ea3ef3e41a82a75 (patch) | |
tree | 78b522cab9f76336e62064d4d8ff7c897659b20e /contrib/python/ipython/py3/IPython/terminal | |
parent | 8113a823ffca6451bb5ff8f0334560885a939a24 (diff) | |
download | ydb-9e5f436a8b2a27bcc7802e443ea3ef3e41a82a75.tar.gz |
Update contrib/python/ipython/py3 to 8.3.0
ref:e84342d4d30476f9148137f37fd0c6405fd36f55
Diffstat (limited to 'contrib/python/ipython/py3/IPython/terminal')
12 files changed, 607 insertions, 232 deletions
diff --git a/contrib/python/ipython/py3/IPython/terminal/debugger.py b/contrib/python/ipython/py3/IPython/terminal/debugger.py index db8ecac0d2..8448d96370 100644 --- a/contrib/python/ipython/py3/IPython/terminal/debugger.py +++ b/contrib/python/ipython/py3/IPython/terminal/debugger.py @@ -1,21 +1,19 @@ import asyncio -import signal +import os import sys from IPython.core.debugger import Pdb from IPython.core.completer import IPCompleter from .ptutils import IPythonPTCompleter -from .shortcuts import create_ipython_shortcuts, suspend_to_bg, cursor_in_leading_ws +from .shortcuts import create_ipython_shortcuts +from . import embed -from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import (Condition, has_focus, has_selection, - vi_insert_mode, emacs_insert_mode) -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline +from pathlib import Path from pygments.token import Token from prompt_toolkit.shortcuts.prompt import PromptSession from prompt_toolkit.enums import EditingMode from prompt_toolkit.formatted_text import PygmentsTokens +from prompt_toolkit.history import InMemoryHistory, FileHistory from concurrent.futures import ThreadPoolExecutor from prompt_toolkit import __version__ as ptk_version @@ -34,22 +32,20 @@ class TerminalPdb(Pdb): def pt_init(self, pt_session_options=None): """Initialize the prompt session and the prompt loop and store them in self.pt_app and self.pt_loop. - + Additional keyword arguments for the PromptSession class can be specified in pt_session_options. """ if pt_session_options is None: pt_session_options = {} - + def get_prompt_tokens(): return [(Token.Prompt, self.prompt)] if self._ptcomp is None: - compl = IPCompleter(shell=self.shell, - namespace={}, - global_namespace={}, - parent=self.shell, - ) + compl = IPCompleter( + shell=self.shell, namespace={}, global_namespace={}, parent=self.shell + ) # add a completer for all the do_ methods methods_names = [m[3:] for m in dir(self) if m.startswith("do_")] @@ -62,11 +58,24 @@ class TerminalPdb(Pdb): self._ptcomp = IPythonPTCompleter(compl) + # setup history only when we start pdb + if self.shell.debugger_history is None: + if self.shell.debugger_history_file is not None: + + p = Path(self.shell.debugger_history_file).expanduser() + if not p.exists(): + p.touch() + self.debugger_history = FileHistory(os.path.expanduser(str(p))) + else: + self.debugger_history = InMemoryHistory() + else: + self.debugger_history = self.shell.debugger_history + options = dict( message=(lambda: PygmentsTokens(get_prompt_tokens())), editing_mode=getattr(EditingMode, self.shell.editing_mode.upper()), key_bindings=create_ipython_shortcuts(self.shell), - history=self.shell.debugger_history, + history=self.debugger_history, completer=self._ptcomp, enable_history_search=True, mouse_support=self.shell.mouse_support, @@ -124,6 +133,18 @@ class TerminalPdb(Pdb): except Exception: raise + def do_interact(self, arg): + ipshell = embed.InteractiveShellEmbed( + config=self.shell.config, + banner1="*interactive*", + exit_msg="*exiting interactive console...*", + ) + global_ns = self.curframe.f_globals + ipshell( + module=sys.modules.get(global_ns["__name__"], None), + local_ns=self.curframe_locals, + ) + def set_trace(frame=None): """ @@ -141,6 +162,6 @@ if __name__ == '__main__': # happened after hitting "c", this is needed in order to # be able to quit the debugging session (see #9950). old_trace_dispatch = pdb.Pdb.trace_dispatch - pdb.Pdb = TerminalPdb - pdb.Pdb.trace_dispatch = old_trace_dispatch + pdb.Pdb = TerminalPdb # type: ignore + pdb.Pdb.trace_dispatch = old_trace_dispatch # type: ignore pdb.main() diff --git a/contrib/python/ipython/py3/IPython/terminal/embed.py b/contrib/python/ipython/py3/IPython/terminal/embed.py index 188844fadd..85e76d5558 100644 --- a/contrib/python/ipython/py3/IPython/terminal/embed.py +++ b/contrib/python/ipython/py3/IPython/terminal/embed.py @@ -19,6 +19,8 @@ from IPython.terminal.ipapp import load_default_config from traitlets import Bool, CBool, Unicode from IPython.utils.io import ask_yes_no +from typing import Set + class KillEmbedded(Exception):pass # kept for backward compatibility as IPython 6 was released with @@ -47,7 +49,6 @@ class EmbeddedMagics(Magics): you may then kill it and the program will then continue to run without the interactive shell interfering again. - Kill Instance Option: If for some reasons you need to kill the location where the instance @@ -106,6 +107,14 @@ class EmbeddedMagics(Magics): self.shell.ask_exit() +class _Sentinel: + def __init__(self, repr): + assert isinstance(repr, str) + self.repr = repr + + def __repr__(self): + return repr + class InteractiveShellEmbed(TerminalInteractiveShell): @@ -123,17 +132,17 @@ class InteractiveShellEmbed(TerminalInteractiveShell): help="Automatically set the terminal title" ).tag(config=True) - _inactive_locations = set() + _inactive_locations: Set[str] = set() + + def _disable_init_location(self): + """Disable the current Instance creation location""" + InteractiveShellEmbed._inactive_locations.add(self._init_location_id) @property def embedded_active(self): return (self._call_location_id not in InteractiveShellEmbed._inactive_locations)\ and (self._init_location_id not in InteractiveShellEmbed._inactive_locations) - def _disable_init_location(self): - """Disable the current Instance creation location""" - InteractiveShellEmbed._inactive_locations.add(self._init_location_id) - @embedded_active.setter def embedded_active(self, value): if value: @@ -146,9 +155,9 @@ class InteractiveShellEmbed(TerminalInteractiveShell): self._call_location_id) def __init__(self, **kw): - if kw.get('user_global_ns', None) is not None: - raise DeprecationWarning( - "Key word argument `user_global_ns` has been replaced by `user_module` since IPython 4.0.") + assert ( + "user_global_ns" not in kw + ), "Key word argument `user_global_ns` has been replaced by `user_module` since IPython 4.0." clid = kw.pop('_init_location_id', None) if not clid: @@ -174,8 +183,16 @@ class InteractiveShellEmbed(TerminalInteractiveShell): super(InteractiveShellEmbed, self).init_magics() self.register_magics(EmbeddedMagics) - def __call__(self, header='', local_ns=None, module=None, dummy=None, - stack_depth=1, global_ns=None, compile_flags=None, **kw): + def __call__( + self, + header="", + local_ns=None, + module=None, + dummy=None, + stack_depth=1, + compile_flags=None, + **kw + ): """Activate the interactive interpreter. __call__(self,header='',local_ns=None,module=None,dummy=None) -> Start @@ -225,8 +242,9 @@ class InteractiveShellEmbed(TerminalInteractiveShell): # Call the embedding code with a stack depth of 1 so it can skip over # our call and get the original caller's namespaces. - self.mainloop(local_ns, module, stack_depth=stack_depth, - global_ns=global_ns, compile_flags=compile_flags) + self.mainloop( + local_ns, module, stack_depth=stack_depth, compile_flags=compile_flags + ) self.banner2 = self.old_banner2 @@ -236,40 +254,35 @@ class InteractiveShellEmbed(TerminalInteractiveShell): if self.should_raise: raise KillEmbedded('Embedded IPython raising error, as user requested.') - - def mainloop(self, local_ns=None, module=None, stack_depth=0, - display_banner=None, global_ns=None, compile_flags=None): + def mainloop( + self, + local_ns=None, + module=None, + stack_depth=0, + compile_flags=None, + ): """Embeds IPython into a running python program. Parameters ---------- - local_ns, module - Working local namespace (a dict) and module (a module or similar - object). If given as None, they are automatically taken from the scope - where the shell was called, so that program variables become visible. - + Working local namespace (a dict) and module (a module or similar + object). If given as None, they are automatically taken from the scope + where the shell was called, so that program variables become visible. stack_depth : int - How many levels in the stack to go to looking for namespaces (when - local_ns or module is None). This allows an intermediate caller to - make sure that this function gets the namespace from the intended - level in the stack. By default (0) it will get its locals and globals - from the immediate caller. - + How many levels in the stack to go to looking for namespaces (when + local_ns or module is None). This allows an intermediate caller to + make sure that this function gets the namespace from the intended + level in the stack. By default (0) it will get its locals and globals + from the immediate caller. compile_flags - A bit field identifying the __future__ features - that are enabled, as passed to the builtin :func:`compile` function. - If given as None, they are automatically taken from the scope where - the shell was called. + A bit field identifying the __future__ features + that are enabled, as passed to the builtin :func:`compile` function. + If given as None, they are automatically taken from the scope where + the shell was called. """ - if (global_ns is not None) and (module is None): - raise DeprecationWarning("'global_ns' keyword argument is deprecated, and has been removed in IPython 5.0 use `module` keyword argument instead.") - - if (display_banner is not None): - warnings.warn("The display_banner parameter is deprecated since IPython 4.0", DeprecationWarning) - # Get locals and globals from caller if ((local_ns is None or module is None or compile_flags is None) and self.default_user_namespaces): @@ -334,7 +347,7 @@ class InteractiveShellEmbed(TerminalInteractiveShell): self.compile.flags = orig_compile_flags -def embed(**kwargs): +def embed(*, header="", compile_flags=None, **kwargs): """Call this to embed IPython at the current point in your program. The first invocation of this will create an :class:`InteractiveShellEmbed` @@ -360,8 +373,6 @@ def embed(**kwargs): config argument. """ config = kwargs.get('config') - header = kwargs.pop('header', u'') - compile_flags = kwargs.pop('compile_flags', None) if config is None: config = load_default_config() config.InteractiveShellEmbed = config.TerminalInteractiveShell diff --git a/contrib/python/ipython/py3/IPython/terminal/interactiveshell.py b/contrib/python/ipython/py3/IPython/terminal/interactiveshell.py index 4e35aadd61..06724bea87 100644 --- a/contrib/python/ipython/py3/IPython/terminal/interactiveshell.py +++ b/contrib/python/ipython/py3/IPython/terminal/interactiveshell.py @@ -3,23 +3,34 @@ import asyncio import os import sys -import warnings from warnings import warn +from IPython.core.async_helpers import get_asyncio_loop from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC -from IPython.utils import io from IPython.utils.py3compat import input from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title from IPython.utils.process import abbrev_cwd from traitlets import ( - Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union, - Any, validate + Bool, + Unicode, + Dict, + Integer, + observe, + Instance, + Type, + default, + Enum, + Union, + Any, + validate, + Float, ) +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import (HasFocus, Condition, IsDone) from prompt_toolkit.formatted_text import PygmentsTokens -from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.history import History from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor from prompt_toolkit.output import ColorDepth from prompt_toolkit.patch_stdout import patch_stdout @@ -39,7 +50,6 @@ from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook from .ptutils import IPythonPTCompleter, IPythonPTLexer from .shortcuts import create_ipython_shortcuts -DISPLAY_BANNER_DEPRECATED = object() PTK3 = ptk_version.startswith('3.') @@ -48,17 +58,17 @@ class _NoStyle(Style): pass _style_overrides_light_bg = { - Token.Prompt: '#0000ff', - Token.PromptNum: '#0000ee bold', - Token.OutPrompt: '#cc0000', - Token.OutPromptNum: '#bb0000 bold', + Token.Prompt: '#ansibrightblue', + Token.PromptNum: '#ansiblue bold', + Token.OutPrompt: '#ansibrightred', + Token.OutPromptNum: '#ansired bold', } _style_overrides_linux = { - Token.Prompt: '#00cc00', - Token.PromptNum: '#00bb00 bold', - Token.OutPrompt: '#cc0000', - Token.OutPromptNum: '#bb0000 bold', + Token.Prompt: '#ansibrightgreen', + Token.PromptNum: '#ansigreen bold', + Token.OutPrompt: '#ansibrightred', + Token.OutPromptNum: '#ansired bold', } def get_default_editor(): @@ -91,13 +101,72 @@ else: _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty) def black_reformat_handler(text_before_cursor): + """ + We do not need to protect against error, + this is taken care at a higher level where any reformat error is ignored. + Indeed we may call reformatting on incomplete code. + """ import black + formatted_text = black.format_str(text_before_cursor, mode=black.FileMode()) - if not text_before_cursor.endswith('\n') and formatted_text.endswith('\n'): - formatted_text = formatted_text[:-1] + if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"): + formatted_text = formatted_text[:-1] return formatted_text +def yapf_reformat_handler(text_before_cursor): + from yapf.yapflib import file_resources + from yapf.yapflib import yapf_api + + style_config = file_resources.GetDefaultStyleForDir(os.getcwd()) + formatted_text, was_formatted = yapf_api.FormatCode( + text_before_cursor, style_config=style_config + ) + if was_formatted: + if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"): + formatted_text = formatted_text[:-1] + return formatted_text + else: + return text_before_cursor + + +class PtkHistoryAdapter(History): + """ + Prompt toolkit has it's own way of handling history, Where it assumes it can + Push/pull from history. + + """ + + def __init__(self, shell): + super().__init__() + self.shell = shell + self._refresh() + + def append_string(self, string): + # we rely on sql for that. + self._loaded = False + self._refresh() + + def _refresh(self): + if not self._loaded: + self._loaded_strings = list(self.load_history_strings()) + + def load_history_strings(self): + last_cell = "" + res = [] + for __, ___, cell in self.shell.history_manager.get_tail( + self.shell.history_load_length, include_latest=True + ): + # Ignore blank lines and consecutive duplicates + cell = cell.rstrip() + if cell and (cell != last_cell): + res.append(cell) + last_cell = cell + yield from res[::-1] + + def store_string(self, string: str) -> None: + pass + class TerminalInteractiveShell(InteractiveShell): mime_renderers = Dict().tag(config=True) @@ -112,6 +181,10 @@ class TerminalInteractiveShell(InteractiveShell): pt_app = None debugger_history = None + debugger_history_file = Unicode( + "~/.pdbhistory", help="File in which to store and read history" + ).tag(config=True) + simple_prompt = Bool(_use_simple_prompt, help="""Use `raw_input` for the REPL, without completion and prompt colors. @@ -137,11 +210,45 @@ class TerminalInteractiveShell(InteractiveShell): help="Shortcut style to use at the prompt. 'vi' or 'emacs'.", ).tag(config=True) - autoformatter = Unicode(None, - help="Autoformatter to reformat Terminal code. Can be `'black'` or `None`", + emacs_bindings_in_vi_insert_mode = Bool( + True, + help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.", + ).tag(config=True) + + modal_cursor = Bool( + True, + help=""" + Cursor shape changes depending on vi mode: beam in vi insert mode, + block in nav mode, underscore in replace mode.""", + ).tag(config=True) + + ttimeoutlen = Float( + 0.01, + help="""The time in milliseconds that is waited for a key code + to complete.""", + ).tag(config=True) + + timeoutlen = Float( + 0.5, + help="""The time in milliseconds that is waited for a mapped key + sequence to complete.""", + ).tag(config=True) + + autoformatter = Unicode( + None, + help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`", allow_none=True ).tag(config=True) + auto_match = Bool( + False, + help=""" + Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted. + Brackets: (), [], {} + Quotes: '', \"\" + """, + ).tag(config=True) + mouse_support = Bool(False, help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)" ).tag(config=True) @@ -171,16 +278,21 @@ class TerminalInteractiveShell(InteractiveShell): if self.pt_app: self.pt_app.editing_mode = getattr(EditingMode, change.new.upper()) - @observe('autoformatter') - def _autoformatter_changed(self, change): - formatter = change.new + def _set_formatter(self, formatter): if formatter is None: self.reformat_handler = lambda x:x elif formatter == 'black': self.reformat_handler = black_reformat_handler + elif formatter == "yapf": + self.reformat_handler = yapf_reformat_handler else: raise ValueError + @observe("autoformatter") + def _autoformatter_changed(self, change): + formatter = change.new + self._set_formatter(formatter) + @observe('highlighting_style') @observe('colors') def _highlighting_style_changed(self, change): @@ -195,10 +307,12 @@ class TerminalInteractiveShell(InteractiveShell): ).tag(config=True) true_color = Bool(False, - help=("Use 24bit colors instead of 256 colors in prompt highlighting. " - "If your terminal supports true color, the following command " - "should print 'TRUECOLOR' in orange: " - "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"") + help="""Use 24bit colors instead of 256 colors in prompt highlighting. + If your terminal supports true color, the following command should + print ``TRUECOLOR`` in orange:: + + printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\" + """, ).tag(config=True) editor = Unicode(get_default_editor(), @@ -256,6 +370,29 @@ class TerminalInteractiveShell(InteractiveShell): help="Allows to enable/disable the prompt toolkit history search" ).tag(config=True) + autosuggestions_provider = Unicode( + "AutoSuggestFromHistory", + help="Specifies from which source automatic suggestions are provided. " + "Can be set to `'AutoSuggestFromHistory`' or `None` to disable" + "automatic suggestions. Default is `'AutoSuggestFromHistory`'.", + allow_none=True, + ).tag(config=True) + + def _set_autosuggestions(self, provider): + if provider is None: + self.auto_suggest = None + elif provider == "AutoSuggestFromHistory": + self.auto_suggest = AutoSuggestFromHistory() + else: + raise ValueError("No valid provider.") + if self.pt_app: + self.pt_app.auto_suggest = self.auto_suggest + + @observe("autosuggestions_provider") + def _autosuggestions_provider_changed(self, change): + provider = change.new + self._set_autosuggestions(provider) + prompt_includes_vi_mode = Bool(True, help="Display the current vi mode (when using vi editing mode)." ).tag(config=True) @@ -276,9 +413,7 @@ class TerminalInteractiveShell(InteractiveShell): def init_display_formatter(self): super(TerminalInteractiveShell, self).init_display_formatter() # terminal only supports plain text - self.display_formatter.active_types = ['text/plain'] - # disable `_ipython_display_` - self.display_formatter.ipython_display_formatter.enabled = False + self.display_formatter.active_types = ["text/plain"] def init_prompt_toolkit_cli(self): if self.simple_prompt: @@ -297,16 +432,9 @@ class TerminalInteractiveShell(InteractiveShell): # Set up keyboard shortcuts key_bindings = create_ipython_shortcuts(self) + # Pre-populate history from IPython's history database - history = InMemoryHistory() - last_cell = u"" - for __, ___, cell in self.history_manager.get_tail(self.history_load_length, - include_latest=True): - # Ignore blank lines and consecutive duplicates - cell = cell.rstrip() - if cell and (cell != last_cell): - history.append_string(cell) - last_cell = cell + history = PtkHistoryAdapter(self) self._style = self._make_style_from_name_or_cls(self.highlighting_style) self.style = DynamicStyle(lambda: self._style) @@ -315,18 +443,20 @@ class TerminalInteractiveShell(InteractiveShell): self.pt_loop = asyncio.new_event_loop() self.pt_app = PromptSession( - editing_mode=editing_mode, - key_bindings=key_bindings, - history=history, - completer=IPythonPTCompleter(shell=self), - enable_history_search = self.enable_history_search, - style=self.style, - include_default_pygments_style=False, - mouse_support=self.mouse_support, - enable_open_in_editor=self.extra_open_editor_shortcuts, - color_depth=self.color_depth, - tempfile_suffix=".py", - **self._extra_prompt_options()) + auto_suggest=self.auto_suggest, + editing_mode=editing_mode, + key_bindings=key_bindings, + history=history, + completer=IPythonPTCompleter(shell=self), + enable_history_search=self.enable_history_search, + style=self.style, + include_default_pygments_style=False, + mouse_support=self.mouse_support, + enable_open_in_editor=self.extra_open_editor_shortcuts, + color_depth=self.color_depth, + tempfile_suffix=".py", + **self._extra_prompt_options() + ) def _make_style_from_name_or_cls(self, name_or_cls): """ @@ -349,16 +479,16 @@ class TerminalInteractiveShell(InteractiveShell): # looks like. These tweaks to the default theme help with that. style_cls = get_style_by_name('default') style_overrides.update({ - Token.Number: '#007700', + Token.Number: '#ansigreen', Token.Operator: 'noinherit', - Token.String: '#BB6622', - Token.Name.Function: '#2080D0', - Token.Name.Class: 'bold #2080D0', - Token.Name.Namespace: 'bold #2080D0', + Token.String: '#ansiyellow', + Token.Name.Function: '#ansiblue', + Token.Name.Class: 'bold #ansiblue', + Token.Name.Namespace: 'bold #ansiblue', Token.Name.Variable.Magic: '#ansiblue', - Token.Prompt: '#009900', + Token.Prompt: '#ansigreen', Token.PromptNum: '#ansibrightgreen bold', - Token.OutPrompt: '#990000', + Token.OutPrompt: '#ansired', Token.OutPromptNum: '#ansibrightred bold', }) @@ -382,9 +512,9 @@ class TerminalInteractiveShell(InteractiveShell): else: style_cls = name_or_cls style_overrides = { - Token.Prompt: '#009900', + Token.Prompt: '#ansigreen', Token.PromptNum: '#ansibrightgreen bold', - Token.OutPrompt: '#990000', + Token.OutPrompt: '#ansired', Token.OutPromptNum: '#ansibrightred bold', } style_overrides.update(self.highlighting_style_overrides) @@ -461,14 +591,14 @@ class TerminalInteractiveShell(InteractiveShell): # while/true inside which will freeze the prompt. policy = asyncio.get_event_loop_policy() - try: - old_loop = policy.get_event_loop() - except RuntimeError: - # This happens when the the event loop is closed, - # e.g. by calling `asyncio.run()`. - old_loop = None - - policy.set_event_loop(self.pt_loop) + old_loop = get_asyncio_loop() + + # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop` + # to get the current event loop. + # This will probably be replaced by an attribute or input argument, + # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here. + if old_loop is not self.pt_loop: + policy.set_event_loop(self.pt_loop) try: with patch_stdout(raw=True): text = self.pt_app.prompt( @@ -476,7 +606,7 @@ class TerminalInteractiveShell(InteractiveShell): **self._extra_prompt_options()) finally: # Restore the original event loop. - if old_loop is not None: + if old_loop is not None and old_loop is not self.pt_loop: policy.set_event_loop(old_loop) return text @@ -484,7 +614,6 @@ class TerminalInteractiveShell(InteractiveShell): def enable_win_unicode_console(self): # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows # console by default, so WUC shouldn't be needed. - from warnings import warn warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future", DeprecationWarning, stacklevel=2) @@ -496,16 +625,6 @@ class TerminalInteractiveShell(InteractiveShell): import colorama colorama.init() - # For some reason we make these wrappers around stdout/stderr. - # For now, we need to reset them so all output gets coloured. - # https://github.com/ipython/ipython/issues/8669 - # io.std* are deprecated, but don't show our own deprecation warnings - # during initialization of the deprecated API. - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - io.stdout = io.IOStream(sys.stdout) - io.stderr = io.IOStream(sys.stderr) - def init_magics(self): super(TerminalInteractiveShell, self).init_magics() self.register_magics(TerminalMagics) @@ -525,22 +644,19 @@ class TerminalInteractiveShell(InteractiveShell): def __init__(self, *args, **kwargs): super(TerminalInteractiveShell, self).__init__(*args, **kwargs) + self._set_autosuggestions(self.autosuggestions_provider) self.init_prompt_toolkit_cli() self.init_term_title() self.keep_running = True + self._set_formatter(self.autoformatter) - self.debugger_history = InMemoryHistory() def ask_exit(self): self.keep_running = False rl_next_input = None - def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED): - - if display_banner is not DISPLAY_BANNER_DEPRECATED: - warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2) - + def interact(self): self.keep_running = True while self.keep_running: print(self.separate_in, end='') @@ -556,11 +672,9 @@ class TerminalInteractiveShell(InteractiveShell): if code: self.run_cell(code, store_history=True) - def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED): + def mainloop(self): # An extra layer of protection in case someone mashing Ctrl-C breaks # out of our internal code. - if display_banner is not DISPLAY_BANNER_DEPRECATED: - warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2) while True: try: self.interact() @@ -577,6 +691,13 @@ class TerminalInteractiveShell(InteractiveShell): self.restore_term_title() + # try to call some at-exit operation optimistically as some things can't + # be done during interpreter shutdown. this is technically inaccurate as + # this make mainlool not re-callable, but that should be a rare if not + # in existent use case. + + self._atexit_once() + _inputhook = None def inputhook(self, context): @@ -601,7 +722,7 @@ class TerminalInteractiveShell(InteractiveShell): # When we integrate the asyncio event loop, run the UI in the # same event loop as the rest of the code. don't use an actual # input hook. (Asyncio is not made for nesting event loops.) - self.pt_loop = asyncio.get_event_loop() + self.pt_loop = get_asyncio_loop() elif self._inputhook: # If an inputhook was set, create a new asyncio event loop with diff --git a/contrib/python/ipython/py3/IPython/terminal/ipapp.py b/contrib/python/ipython/py3/IPython/terminal/ipapp.py index 1a3c6c791b..a87eb2f443 100644 --- a/contrib/python/ipython/py3/IPython/terminal/ipapp.py +++ b/contrib/python/ipython/py3/IPython/terminal/ipapp.py @@ -210,26 +210,6 @@ class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): StoreMagics, ] - deprecated_subcommands = dict( - qtconsole=('qtconsole.qtconsoleapp.JupyterQtConsoleApp', - """DEPRECATED, Will be removed in IPython 6.0 : Launch the Jupyter Qt Console.""" - ), - notebook=('notebook.notebookapp.NotebookApp', - """DEPRECATED, Will be removed in IPython 6.0 : Launch the Jupyter HTML Notebook Server.""" - ), - console=('jupyter_console.app.ZMQTerminalIPythonApp', - """DEPRECATED, Will be removed in IPython 6.0 : Launch the Jupyter terminal-based Console.""" - ), - nbconvert=('nbconvert.nbconvertapp.NbConvertApp', - "DEPRECATED, Will be removed in IPython 6.0 : Convert notebooks to/from other formats." - ), - trust=('nbformat.sign.TrustNotebookApp', - "DEPRECATED, Will be removed in IPython 6.0 : Sign notebooks to trust their potentially unsafe contents at load." - ), - kernelspec=('jupyter_client.kernelspecapp.KernelSpecApp', - "DEPRECATED, Will be removed in IPython 6.0 : Manage Jupyter kernel specifications." - ), - ) subcommands = dict( profile = ("IPython.core.profileapp.ProfileApp", "Create and manage IPython profiles." @@ -244,11 +224,7 @@ class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): "Manage the IPython history database." ), ) - deprecated_subcommands['install-nbextension'] = ( - "notebook.nbextensions.InstallNBExtensionApp", - "DEPRECATED, Will be removed in IPython 6.0 : Install Jupyter notebook extension files" - ) - subcommands.update(deprecated_subcommands) + # *do* autocreate requested profile, but don't create the config file. auto_create=Bool(True) @@ -288,22 +264,6 @@ class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): # internal, not-configurable something_to_run=Bool(False) - def parse_command_line(self, argv=None): - """override to allow old '-pylab' flag with deprecation warning""" - - argv = sys.argv[1:] if argv is None else argv - - if '-pylab' in argv: - # deprecated `-pylab` given, - # warn and transform into current syntax - argv = argv[:] # copy, don't clobber - idx = argv.index('-pylab') - warnings.warn("`-pylab` flag has been deprecated.\n" - " Use `--matplotlib <backend>` and import pylab manually.") - argv[idx] = '--pylab' - - return super(TerminalIPythonApp, self).parse_command_line(argv) - @catch_config_error def initialize(self, argv=None): """Do actions after construct, but before starting the app.""" diff --git a/contrib/python/ipython/py3/IPython/terminal/magics.py b/contrib/python/ipython/py3/IPython/terminal/magics.py index 42231c3f80..206ff20a0f 100644 --- a/contrib/python/ipython/py3/IPython/terminal/magics.py +++ b/contrib/python/ipython/py3/IPython/terminal/magics.py @@ -11,6 +11,7 @@ import sys from IPython.core.error import TryNext, UsageError from IPython.core.magic import Magics, magics_class, line_magic from IPython.lib.clipboard import ClipboardEmpty +from IPython.testing.skipdoctest import skip_doctest from IPython.utils.text import SList, strip_email_quotes from IPython.utils import py3compat @@ -52,7 +53,7 @@ class TerminalMagics(Magics): self.shell.user_ns['pasted_block'] = b self.shell.using_paste_magics = True try: - self.shell.run_cell(b) + self.shell.run_cell(b, store_history=True) finally: self.shell.using_paste_magics = False @@ -83,6 +84,7 @@ class TerminalMagics(Magics): self.shell.set_autoindent() print("Automatic indentation is:",['OFF','ON'][self.shell.autoindent]) + @skip_doctest @line_magic def cpaste(self, parameter_s=''): """Paste & execute a pre-formatted code block from clipboard. @@ -111,9 +113,9 @@ class TerminalMagics(Magics): Shell escapes are not supported (yet). - See also + See Also -------- - paste: automatically pull code from clipboard. + paste : automatically pull code from clipboard. Examples -------- @@ -174,9 +176,9 @@ class TerminalMagics(Magics): IPython statements (magics, shell escapes) are not supported (yet). - See also + See Also -------- - cpaste: manually paste code into terminal until you mark its end. + cpaste : manually paste code into terminal until you mark its end. """ opts, name = self.parse_options(parameter_s, 'rq', mode='string') if 'r' in opts: @@ -191,16 +193,15 @@ class TerminalMagics(Magics): else: error('Could not get text from the clipboard.') return - except ClipboardEmpty: - raise UsageError("The clipboard appears to be empty") + except ClipboardEmpty as e: + raise UsageError("The clipboard appears to be empty") from e # By default, echo back to terminal unless quiet mode is requested if 'q' not in opts: - write = self.shell.write - write(self.shell.pycolorize(block)) - if not block.endswith('\n'): - write('\n') - write("## -- End pasted text --\n") + sys.stdout.write(self.shell.pycolorize(block)) + if not block.endswith("\n"): + sys.stdout.write("\n") + sys.stdout.write("## -- End pasted text --\n") self.store_or_execute(block, name) diff --git a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/asyncio.py b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/asyncio.py index 95cf194f86..2d8c128208 100644 --- a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/asyncio.py +++ b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/asyncio.py @@ -27,15 +27,12 @@ prompt_toolkit`s `patch_stdout`):: In [4]: asyncio.ensure_future(f()) """ -import asyncio from prompt_toolkit import __version__ as ptk_version -PTK3 = ptk_version.startswith('3.') +from IPython.core.async_helpers import get_asyncio_loop +PTK3 = ptk_version.startswith('3.') -# Keep reference to the original asyncio loop, because getting the event loop -# within the input hook would return the other loop. -loop = asyncio.get_event_loop() def inputhook(context): @@ -52,6 +49,9 @@ def inputhook(context): # For prompt_toolkit 2.0, we can run the current asyncio event loop, # because prompt_toolkit 2.0 uses a different event loop internally. + # get the persistent asyncio event loop + loop = get_asyncio_loop() + def stop(): loop.stop() @@ -61,4 +61,3 @@ def inputhook(context): loop.run_forever() finally: loop.remove_reader(fileno) - diff --git a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/glut.py b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/glut.py index f6d54a55b4..835aadfc97 100644 --- a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/glut.py +++ b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/glut.py @@ -44,10 +44,10 @@ if sys.platform == 'darwin': doc='glutCheckLoop( ) -> None', argNames=(), ) - except AttributeError: + except AttributeError as e: raise RuntimeError( - '''Your glut implementation does not allow interactive sessions''' - '''Consider installing freeglut.''') + '''Your glut implementation does not allow interactive sessions. ''' + '''Consider installing freeglut.''') from e glutMainLoopEvent = glutCheckLoop elif glut.HAVE_FREEGLUT: glutMainLoopEvent = glut.glutMainLoopEvent diff --git a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/osx.py b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/osx.py index 80440196fb..2754820efc 100644 --- a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/osx.py +++ b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/osx.py @@ -9,7 +9,7 @@ import ctypes import ctypes.util from threading import Event -objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) +objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type: ignore void_p = ctypes.c_void_p @@ -37,7 +37,7 @@ def C(classname): # end obj-c boilerplate from appnope # CoreFoundation C-API calls we will use: -CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) +CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation")) # type: ignore CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate CFFileDescriptorCreate.restype = void_p diff --git a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/qt.py b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/qt.py index b999f5aa17..f1e710aff5 100644 --- a/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/qt.py +++ b/contrib/python/ipython/py3/IPython/terminal/pt_inputhooks/qt.py @@ -74,6 +74,8 @@ def inputhook(context): ) try: # connect the callback we care about before we turn it on + # lambda is necessary as PyQT inspect the function signature to know + # what arguments to pass to. See https://github.com/ipython/ipython/pull/12355 notifier.activated.connect(lambda: event_loop.exit()) notifier.setEnabled(True) # only start the event loop we are not already flipped diff --git a/contrib/python/ipython/py3/IPython/terminal/ptshell.py b/contrib/python/ipython/py3/IPython/terminal/ptshell.py deleted file mode 100644 index 666d3c5b51..0000000000 --- a/contrib/python/ipython/py3/IPython/terminal/ptshell.py +++ /dev/null @@ -1,8 +0,0 @@ -raise DeprecationWarning("""DEPRECATED: - -After Popular request and decision from the BDFL: -`IPython.terminal.ptshell` has been moved back to `IPython.terminal.interactiveshell` -during the beta cycle (after IPython 5.0.beta3) Sorry about that. - -This file will be removed in 5.0 rc or final. -""") diff --git a/contrib/python/ipython/py3/IPython/terminal/ptutils.py b/contrib/python/ipython/py3/IPython/terminal/ptutils.py index 3e5d3c5c77..c390d4972a 100644 --- a/contrib/python/ipython/py3/IPython/terminal/ptutils.py +++ b/contrib/python/ipython/py3/IPython/terminal/ptutils.py @@ -9,8 +9,6 @@ not to be used outside IPython. import unicodedata from wcwidth import wcwidth -import sys -import traceback from IPython.core.completer import ( provisionalcompleter, cursor_to_position, @@ -22,6 +20,8 @@ from prompt_toolkit.patch_stdout import patch_stdout import pygments.lexers as pygments_lexers import os +import sys +import traceback _completion_sentinel = object() diff --git a/contrib/python/ipython/py3/IPython/terminal/shortcuts.py b/contrib/python/ipython/py3/IPython/terminal/shortcuts.py index a23fa091a0..615397abc5 100644 --- a/contrib/python/ipython/py3/IPython/terminal/shortcuts.py +++ b/contrib/python/ipython/py3/IPython/terminal/shortcuts.py @@ -9,6 +9,8 @@ Module to define and register Terminal IPython shortcuts with import warnings import signal import sys +import re +import os from typing import Callable @@ -18,6 +20,8 @@ from prompt_toolkit.filters import (has_focus, has_selection, Condition, vi_insert_mode, emacs_insert_mode, has_completions, vi_mode) from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.bindings import named_commands as nc +from prompt_toolkit.key_binding.vi_state import InputMode, ViState from IPython.utils.decorators import undoc @@ -53,7 +57,7 @@ def create_ipython_shortcuts(shell): & insert_mode ))(reformat_and_execute) - kb.add('c-\\')(force_exit) + kb.add("c-\\")(quit) kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) )(previous_history_or_previous_completion) @@ -82,15 +86,274 @@ def create_ipython_shortcuts(shell): kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) - if shell.display_completions == 'readlinelike': - kb.add('c-i', filter=(has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - & ~cursor_in_leading_ws - ))(display_completions_like_readline) + @Condition + def auto_match(): + return shell.auto_match - if sys.platform == 'win32': - kb.add('c-v', filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) + focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER) + _preceding_text_cache = {} + _following_text_cache = {} + + def preceding_text(pattern): + try: + return _preceding_text_cache[pattern] + except KeyError: + pass + m = re.compile(pattern) + + def _preceding_text(): + app = get_app() + return bool(m.match(app.current_buffer.document.current_line_before_cursor)) + + 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)) + + condition = Condition(_following_text) + _following_text_cache[pattern] = condition + return condition + + # auto match + @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) + def _(event): + event.current_buffer.insert_text("()") + event.current_buffer.cursor_left() + + @kb.add("[", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) + def _(event): + event.current_buffer.insert_text("[]") + event.current_buffer.cursor_left() + + @kb.add("{", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) + def _(event): + event.current_buffer.insert_text("{}") + event.current_buffer.cursor_left() + + @kb.add( + '"', + filter=focused_insert + & auto_match + & preceding_text(r'^([^"]+|"[^"]*")*$') + & following_text(r"[,)}\]]|$"), + ) + def _(event): + event.current_buffer.insert_text('""') + event.current_buffer.cursor_left() + + @kb.add( + "'", + filter=focused_insert + & auto_match + & preceding_text(r"^([^']+|'[^']*')*$") + & following_text(r"[,)}\]]|$"), + ) + def _(event): + event.current_buffer.insert_text("''") + event.current_buffer.cursor_left() + + # raw string + @kb.add( + "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + ) + def _(event): + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) or "" + event.current_buffer.insert_text("()" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + @kb.add( + "[", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + ) + def _(event): + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) or "" + event.current_buffer.insert_text("[]" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + @kb.add( + "{", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + ) + def _(event): + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) or "" + event.current_buffer.insert_text("{}" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + # just move cursor + @kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)")) + @kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]")) + @kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}")) + @kb.add('"', filter=focused_insert & auto_match & following_text('^"')) + @kb.add("'", filter=focused_insert & auto_match & following_text("^'")) + def _(event): + event.current_buffer.cursor_right() + + @kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*\($") + & auto_match + & following_text(r"^\)"), + ) + @kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*\[$") + & auto_match + & following_text(r"^\]"), + ) + @kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*\{$") + & auto_match + & following_text(r"^\}"), + ) + @kb.add( + "backspace", + filter=focused_insert + & preceding_text('.*"$') + & auto_match + & following_text('^"'), + ) + @kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*'$") + & auto_match + & following_text(r"^'"), + ) + def _(event): + event.current_buffer.delete() + event.current_buffer.delete_before_cursor() + + if shell.display_completions == "readlinelike": + kb.add( + "c-i", + filter=( + has_focus(DEFAULT_BUFFER) + & ~has_selection + & insert_mode + & ~cursor_in_leading_ws + ), + )(display_completions_like_readline) + + if sys.platform == "win32": + kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) + + @Condition + def ebivim(): + return shell.emacs_bindings_in_vi_insert_mode + + focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode + + # Needed for to accept autosuggestions in vi insert mode + def _apply_autosuggest(event): + b = event.current_buffer + suggestion = b.suggestion + if suggestion is not None and suggestion.text: + b.insert_text(suggestion.text) + else: + nc.end_of_line(event) + + @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) + def _(event): + _apply_autosuggest(event) + + @kb.add("c-e", filter=focused_insert_vi & ebivim) + def _(event): + _apply_autosuggest(event) + + @kb.add("c-f", filter=focused_insert_vi) + def _(event): + b = event.current_buffer + suggestion = b.suggestion + if suggestion: + b.insert_text(suggestion.text) + else: + nc.forward_char(event) + + @kb.add("escape", "f", filter=focused_insert_vi & ebivim) + def _(event): + b = event.current_buffer + suggestion = b.suggestion + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + b.insert_text(next((x for x in t if x), "")) + else: + nc.forward_word(event) + + # Simple Control keybindings + key_cmd_dict = { + "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, + } + + for key, cmd in key_cmd_dict.items(): + kb.add(key, filter=focused_insert_vi & ebivim)(cmd) + + # Alt and Combo Control keybindings + keys_cmd_dict = { + # 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, + } + + for keys, cmd in keys_cmd_dict.items(): + kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd) + + 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 + ViState.input_mode = property(get_input_mode, set_input_mode) return kb @@ -196,11 +459,16 @@ def reset_search_buffer(event): def suspend_to_bg(event): event.app.suspend_to_background() -def force_exit(event): +def quit(event): """ - Force exit (with a non-zero return value) + On platforms that support SIGQUIT, send SIGQUIT to the current process. + On other platforms, just exit the process with a message. """ - sys.exit("Quit") + sigquit = getattr(signal, "SIGQUIT", None) + if sigquit is not None: + os.kill(0, signal.SIGQUIT) + else: + sys.exit("Quit") def indent_buffer(event): event.current_buffer.insert_text(' ' * 4) @@ -273,4 +541,4 @@ if sys.platform == 'win32': return except ClipboardEmpty: return - event.current_buffer.insert_text(text.replace('\t', ' ' * 4)) + event.current_buffer.insert_text(text.replace("\t", " " * 4)) |