fixed subscription table
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Shim to maintain backwards compatibility with old IPython.terminal.console imports.
|
||||
"""
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import sys
|
||||
from warnings import warn
|
||||
|
||||
from IPython.utils.shimmodule import ShimModule, ShimWarning
|
||||
|
||||
warn("The `IPython.terminal.console` package has been deprecated since IPython 4.0. "
|
||||
"You should import from jupyter_console instead.", ShimWarning)
|
||||
|
||||
# Unconditionally insert the shim into sys.modules so that further import calls
|
||||
# trigger the custom attribute access above
|
||||
|
||||
sys.modules['IPython.terminal.console'] = ShimModule(
|
||||
src='IPython.terminal.console', mirror='jupyter_console')
|
187
.venv/lib/python3.12/site-packages/IPython/terminal/debugger.py
Normal file
187
.venv/lib/python3.12/site-packages/IPython/terminal/debugger.py
Normal file
@@ -0,0 +1,187 @@
|
||||
import asyncio
|
||||
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
|
||||
from . import embed
|
||||
|
||||
from pathlib import Path
|
||||
from pygments.token import Token
|
||||
from prompt_toolkit.application import create_app_session
|
||||
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
|
||||
PTK3 = ptk_version.startswith('3.')
|
||||
|
||||
|
||||
# we want to avoid ptk as much as possible when using subprocesses
|
||||
# as it uses cursor positioning requests, deletes color ....
|
||||
_use_simple_prompt = "IPY_TEST_SIMPLE_PROMPT" in os.environ
|
||||
|
||||
|
||||
class TerminalPdb(Pdb):
|
||||
"""Standalone IPython debugger."""
|
||||
|
||||
def __init__(self, *args, pt_session_options=None, **kwargs):
|
||||
Pdb.__init__(self, *args, **kwargs)
|
||||
self._ptcomp = None
|
||||
self.pt_init(pt_session_options)
|
||||
self.thread_executor = ThreadPoolExecutor(1)
|
||||
|
||||
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
|
||||
)
|
||||
# add a completer for all the do_ methods
|
||||
methods_names = [m[3:] for m in dir(self) if m.startswith("do_")]
|
||||
|
||||
def gen_comp(self, text):
|
||||
return [m for m in methods_names if m.startswith(text)]
|
||||
import types
|
||||
newcomp = types.MethodType(gen_comp, compl)
|
||||
compl.custom_matchers.insert(0, newcomp)
|
||||
# end add completer.
|
||||
|
||||
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.debugger_history,
|
||||
completer=self._ptcomp,
|
||||
enable_history_search=True,
|
||||
mouse_support=self.shell.mouse_support,
|
||||
complete_style=self.shell.pt_complete_style,
|
||||
style=getattr(self.shell, "style", None),
|
||||
color_depth=self.shell.color_depth,
|
||||
)
|
||||
|
||||
if not PTK3:
|
||||
options['inputhook'] = self.shell.inputhook
|
||||
options.update(pt_session_options)
|
||||
if not _use_simple_prompt:
|
||||
self.pt_loop = asyncio.new_event_loop()
|
||||
self.pt_app = PromptSession(**options)
|
||||
|
||||
def _prompt(self):
|
||||
"""
|
||||
In case other prompt_toolkit apps have to run in parallel to this one (e.g. in madbg),
|
||||
create_app_session must be used to prevent mixing up between them. According to the prompt_toolkit docs:
|
||||
|
||||
> If you need multiple applications running at the same time, you have to create a separate
|
||||
> `AppSession` using a `with create_app_session():` block.
|
||||
"""
|
||||
with create_app_session():
|
||||
return self.pt_app.prompt()
|
||||
|
||||
def cmdloop(self, intro=None):
|
||||
"""Repeatedly issue a prompt, accept input, parse an initial prefix
|
||||
off the received input, and dispatch to action methods, passing them
|
||||
the remainder of the line as argument.
|
||||
|
||||
override the same methods from cmd.Cmd to provide prompt toolkit replacement.
|
||||
"""
|
||||
if not self.use_rawinput:
|
||||
raise ValueError('Sorry ipdb does not support use_rawinput=False')
|
||||
|
||||
# In order to make sure that prompt, which uses asyncio doesn't
|
||||
# interfere with applications in which it's used, we always run the
|
||||
# prompt itself in a different thread (we can't start an event loop
|
||||
# within an event loop). This new thread won't have any event loop
|
||||
# running, and here we run our prompt-loop.
|
||||
self.preloop()
|
||||
|
||||
try:
|
||||
if intro is not None:
|
||||
self.intro = intro
|
||||
if self.intro:
|
||||
print(self.intro, file=self.stdout)
|
||||
stop = None
|
||||
while not stop:
|
||||
if self.cmdqueue:
|
||||
line = self.cmdqueue.pop(0)
|
||||
else:
|
||||
self._ptcomp.ipy_completer.namespace = self.curframe_locals
|
||||
self._ptcomp.ipy_completer.global_namespace = self.curframe.f_globals
|
||||
|
||||
# Run the prompt in a different thread.
|
||||
if not _use_simple_prompt:
|
||||
try:
|
||||
line = self.thread_executor.submit(self._prompt).result()
|
||||
except EOFError:
|
||||
line = "EOF"
|
||||
else:
|
||||
line = input("ipdb> ")
|
||||
|
||||
line = self.precmd(line)
|
||||
stop = self.onecmd(line)
|
||||
stop = self.postcmd(stop, line)
|
||||
self.postloop()
|
||||
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):
|
||||
"""
|
||||
Start debugging from `frame`.
|
||||
|
||||
If frame is not specified, debugging starts from caller's frame.
|
||||
"""
|
||||
TerminalPdb().set_trace(frame or sys._getframe().f_back)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import pdb
|
||||
# IPython.core.debugger.Pdb.trace_dispatch shall not catch
|
||||
# bdb.BdbQuit. When started through __main__ and an exception
|
||||
# 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 # type: ignore
|
||||
pdb.Pdb.trace_dispatch = old_trace_dispatch # type: ignore
|
||||
pdb.main()
|
426
.venv/lib/python3.12/site-packages/IPython/terminal/embed.py
Normal file
426
.venv/lib/python3.12/site-packages/IPython/terminal/embed.py
Normal file
@@ -0,0 +1,426 @@
|
||||
# encoding: utf-8
|
||||
"""
|
||||
An embedded IPython shell.
|
||||
"""
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from IPython.core import ultratb, compilerop
|
||||
from IPython.core import magic_arguments
|
||||
from IPython.core.magic import Magics, magics_class, line_magic
|
||||
from IPython.core.interactiveshell import DummyMod, InteractiveShell
|
||||
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
||||
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
|
||||
# the typo. See https://github.com/ipython/ipython/pull/10706
|
||||
KillEmbeded = KillEmbedded
|
||||
|
||||
# This is an additional magic that is exposed in embedded shells.
|
||||
@magics_class
|
||||
class EmbeddedMagics(Magics):
|
||||
|
||||
@line_magic
|
||||
@magic_arguments.magic_arguments()
|
||||
@magic_arguments.argument('-i', '--instance', action='store_true',
|
||||
help='Kill instance instead of call location')
|
||||
@magic_arguments.argument('-x', '--exit', action='store_true',
|
||||
help='Also exit the current session')
|
||||
@magic_arguments.argument('-y', '--yes', action='store_true',
|
||||
help='Do not ask confirmation')
|
||||
def kill_embedded(self, parameter_s=''):
|
||||
"""%kill_embedded : deactivate for good the current embedded IPython
|
||||
|
||||
This function (after asking for confirmation) sets an internal flag so
|
||||
that an embedded IPython will never activate again for the given call
|
||||
location. This is useful to permanently disable a shell that is being
|
||||
called inside a loop: once you've figured out what you needed from it,
|
||||
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
|
||||
is created and not called, for example if you create a single
|
||||
instance in one place and debug in many locations, you can use the
|
||||
``--instance`` option to kill this specific instance. Like for the
|
||||
``call location`` killing an "instance" should work even if it is
|
||||
recreated within a loop.
|
||||
|
||||
.. note::
|
||||
|
||||
This was the default behavior before IPython 5.2
|
||||
|
||||
"""
|
||||
|
||||
args = magic_arguments.parse_argstring(self.kill_embedded, parameter_s)
|
||||
print(args)
|
||||
if args.instance:
|
||||
# let no ask
|
||||
if not args.yes:
|
||||
kill = ask_yes_no(
|
||||
"Are you sure you want to kill this embedded instance? [y/N] ", 'n')
|
||||
else:
|
||||
kill = True
|
||||
if kill:
|
||||
self.shell._disable_init_location()
|
||||
print("This embedded IPython instance will not reactivate anymore "
|
||||
"once you exit.")
|
||||
else:
|
||||
if not args.yes:
|
||||
kill = ask_yes_no(
|
||||
"Are you sure you want to kill this embedded call_location? [y/N] ", 'n')
|
||||
else:
|
||||
kill = True
|
||||
if kill:
|
||||
self.shell.embedded_active = False
|
||||
print("This embedded IPython call location will not reactivate anymore "
|
||||
"once you exit.")
|
||||
|
||||
if args.exit:
|
||||
# Ask-exit does not really ask, it just set internals flags to exit
|
||||
# on next loop.
|
||||
self.shell.ask_exit()
|
||||
|
||||
|
||||
@line_magic
|
||||
def exit_raise(self, parameter_s=''):
|
||||
"""%exit_raise Make the current embedded kernel exit and raise and exception.
|
||||
|
||||
This function sets an internal flag so that an embedded IPython will
|
||||
raise a `IPython.terminal.embed.KillEmbedded` Exception on exit, and then exit the current I. This is
|
||||
useful to permanently exit a loop that create IPython embed instance.
|
||||
"""
|
||||
|
||||
self.shell.should_raise = True
|
||||
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):
|
||||
|
||||
dummy_mode = Bool(False)
|
||||
exit_msg = Unicode('')
|
||||
embedded = CBool(True)
|
||||
should_raise = CBool(False)
|
||||
# Like the base class display_banner is not configurable, but here it
|
||||
# is True by default.
|
||||
display_banner = CBool(True)
|
||||
exit_msg = Unicode()
|
||||
|
||||
# When embedding, by default we don't change the terminal title
|
||||
term_title = Bool(False,
|
||||
help="Automatically set the terminal title"
|
||||
).tag(config=True)
|
||||
|
||||
_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)
|
||||
|
||||
@embedded_active.setter
|
||||
def embedded_active(self, value):
|
||||
if value:
|
||||
InteractiveShellEmbed._inactive_locations.discard(
|
||||
self._call_location_id)
|
||||
InteractiveShellEmbed._inactive_locations.discard(
|
||||
self._init_location_id)
|
||||
else:
|
||||
InteractiveShellEmbed._inactive_locations.add(
|
||||
self._call_location_id)
|
||||
|
||||
def __init__(self, **kw):
|
||||
assert (
|
||||
"user_global_ns" not in kw
|
||||
), "Key word argument `user_global_ns` has been replaced by `user_module` since IPython 4.0."
|
||||
# temporary fix for https://github.com/ipython/ipython/issues/14164
|
||||
cls = type(self)
|
||||
if cls._instance is None:
|
||||
for subclass in cls._walk_mro():
|
||||
subclass._instance = self
|
||||
cls._instance = self
|
||||
|
||||
clid = kw.pop('_init_location_id', None)
|
||||
if not clid:
|
||||
frame = sys._getframe(1)
|
||||
clid = '%s:%s' % (frame.f_code.co_filename, frame.f_lineno)
|
||||
self._init_location_id = clid
|
||||
|
||||
super(InteractiveShellEmbed,self).__init__(**kw)
|
||||
|
||||
# don't use the ipython crash handler so that user exceptions aren't
|
||||
# trapped
|
||||
sys.excepthook = ultratb.FormattedTB(color_scheme=self.colors,
|
||||
mode=self.xmode,
|
||||
call_pdb=self.pdb)
|
||||
|
||||
def init_sys_modules(self):
|
||||
"""
|
||||
Explicitly overwrite :mod:`IPython.core.interactiveshell` to do nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def init_magics(self):
|
||||
super(InteractiveShellEmbed, self).init_magics()
|
||||
self.register_magics(EmbeddedMagics)
|
||||
|
||||
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
|
||||
the interpreter shell with the given local and global namespaces, and
|
||||
optionally print a header string at startup.
|
||||
|
||||
The shell can be globally activated/deactivated using the
|
||||
dummy_mode attribute. This allows you to turn off a shell used
|
||||
for debugging globally.
|
||||
|
||||
However, *each* time you call the shell you can override the current
|
||||
state of dummy_mode with the optional keyword parameter 'dummy'. For
|
||||
example, if you set dummy mode on with IPShell.dummy_mode = True, you
|
||||
can still have a specific call work by making it as IPShell(dummy=False).
|
||||
"""
|
||||
|
||||
# we are called, set the underlying interactiveshell not to exit.
|
||||
self.keep_running = True
|
||||
|
||||
# If the user has turned it off, go away
|
||||
clid = kw.pop('_call_location_id', None)
|
||||
if not clid:
|
||||
frame = sys._getframe(1)
|
||||
clid = '%s:%s' % (frame.f_code.co_filename, frame.f_lineno)
|
||||
self._call_location_id = clid
|
||||
|
||||
if not self.embedded_active:
|
||||
return
|
||||
|
||||
# Normal exits from interactive mode set this flag, so the shell can't
|
||||
# re-enter (it checks this variable at the start of interactive mode).
|
||||
self.exit_now = False
|
||||
|
||||
# Allow the dummy parameter to override the global __dummy_mode
|
||||
if dummy or (dummy != 0 and self.dummy_mode):
|
||||
return
|
||||
|
||||
# self.banner is auto computed
|
||||
if header:
|
||||
self.old_banner2 = self.banner2
|
||||
self.banner2 = self.banner2 + '\n' + header + '\n'
|
||||
else:
|
||||
self.old_banner2 = ''
|
||||
|
||||
if self.display_banner:
|
||||
self.show_banner()
|
||||
|
||||
# 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, compile_flags=compile_flags
|
||||
)
|
||||
|
||||
self.banner2 = self.old_banner2
|
||||
|
||||
if self.exit_msg is not None:
|
||||
print(self.exit_msg)
|
||||
|
||||
if self.should_raise:
|
||||
raise KillEmbedded('Embedded IPython raising error, as user requested.')
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
# 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):
|
||||
call_frame = sys._getframe(stack_depth).f_back
|
||||
|
||||
if local_ns is None:
|
||||
local_ns = call_frame.f_locals
|
||||
if module is None:
|
||||
global_ns = call_frame.f_globals
|
||||
try:
|
||||
module = sys.modules[global_ns['__name__']]
|
||||
except KeyError:
|
||||
warnings.warn("Failed to get module %s" % \
|
||||
global_ns.get('__name__', 'unknown module')
|
||||
)
|
||||
module = DummyMod()
|
||||
module.__dict__ = global_ns
|
||||
if compile_flags is None:
|
||||
compile_flags = (call_frame.f_code.co_flags &
|
||||
compilerop.PyCF_MASK)
|
||||
|
||||
# Save original namespace and module so we can restore them after
|
||||
# embedding; otherwise the shell doesn't shut down correctly.
|
||||
orig_user_module = self.user_module
|
||||
orig_user_ns = self.user_ns
|
||||
orig_compile_flags = self.compile.flags
|
||||
|
||||
# Update namespaces and fire up interpreter
|
||||
|
||||
# The global one is easy, we can just throw it in
|
||||
if module is not None:
|
||||
self.user_module = module
|
||||
|
||||
# But the user/local one is tricky: ipython needs it to store internal
|
||||
# data, but we also need the locals. We'll throw our hidden variables
|
||||
# like _ih and get_ipython() into the local namespace, but delete them
|
||||
# later.
|
||||
if local_ns is not None:
|
||||
reentrant_local_ns = {k: v for (k, v) in local_ns.items() if k not in self.user_ns_hidden.keys()}
|
||||
self.user_ns = reentrant_local_ns
|
||||
self.init_user_ns()
|
||||
|
||||
# Compiler flags
|
||||
if compile_flags is not None:
|
||||
self.compile.flags = compile_flags
|
||||
|
||||
# make sure the tab-completer has the correct frame information, so it
|
||||
# actually completes using the frame's locals/globals
|
||||
self.set_completer_frame()
|
||||
|
||||
with self.builtin_trap, self.display_trap:
|
||||
self.interact()
|
||||
|
||||
# now, purge out the local namespace of IPython's hidden variables.
|
||||
if local_ns is not None:
|
||||
local_ns.update({k: v for (k, v) in self.user_ns.items() if k not in self.user_ns_hidden.keys()})
|
||||
|
||||
|
||||
# Restore original namespace so shell can shut down when we exit.
|
||||
self.user_module = orig_user_module
|
||||
self.user_ns = orig_user_ns
|
||||
self.compile.flags = orig_compile_flags
|
||||
|
||||
|
||||
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 a :class:`terminal.embed.InteractiveShellEmbed`
|
||||
instance and then call it. Consecutive calls just call the already
|
||||
created instance.
|
||||
|
||||
If you don't want the kernel to initialize the namespace
|
||||
from the scope of the surrounding function,
|
||||
and/or you want to load full IPython configuration,
|
||||
you probably want `IPython.start_ipython()` instead.
|
||||
|
||||
Here is a simple example::
|
||||
|
||||
from IPython import embed
|
||||
a = 10
|
||||
b = 20
|
||||
embed(header='First time')
|
||||
c = 30
|
||||
d = 40
|
||||
embed()
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
header : str
|
||||
Optional header string to print at startup.
|
||||
compile_flags
|
||||
Passed to the `compile_flags` parameter of :py:meth:`terminal.embed.InteractiveShellEmbed.mainloop()`,
|
||||
which is called when the :class:`terminal.embed.InteractiveShellEmbed` instance is called.
|
||||
**kwargs : various, optional
|
||||
Any other kwargs will be passed to the :class:`terminal.embed.InteractiveShellEmbed` constructor.
|
||||
Full customization can be done by passing a traitlets :class:`Config` in as the
|
||||
`config` argument (see :ref:`configure_start_ipython` and :ref:`terminal_options`).
|
||||
"""
|
||||
config = kwargs.get('config')
|
||||
if config is None:
|
||||
config = load_default_config()
|
||||
config.InteractiveShellEmbed = config.TerminalInteractiveShell
|
||||
kwargs['config'] = config
|
||||
using = kwargs.get('using', 'sync')
|
||||
if using :
|
||||
kwargs['config'].update({'TerminalInteractiveShell':{'loop_runner':using, 'colors':'NoColor', 'autoawait': using!='sync'}})
|
||||
#save ps1/ps2 if defined
|
||||
ps1 = None
|
||||
ps2 = None
|
||||
try:
|
||||
ps1 = sys.ps1
|
||||
ps2 = sys.ps2
|
||||
except AttributeError:
|
||||
pass
|
||||
#save previous instance
|
||||
saved_shell_instance = InteractiveShell._instance
|
||||
if saved_shell_instance is not None:
|
||||
cls = type(saved_shell_instance)
|
||||
cls.clear_instance()
|
||||
frame = sys._getframe(1)
|
||||
shell = InteractiveShellEmbed.instance(_init_location_id='%s:%s' % (
|
||||
frame.f_code.co_filename, frame.f_lineno), **kwargs)
|
||||
shell(header=header, stack_depth=2, compile_flags=compile_flags,
|
||||
_call_location_id='%s:%s' % (frame.f_code.co_filename, frame.f_lineno))
|
||||
InteractiveShellEmbed.clear_instance()
|
||||
#restore previous instance
|
||||
if saved_shell_instance is not None:
|
||||
cls = type(saved_shell_instance)
|
||||
cls.clear_instance()
|
||||
for subclass in cls._walk_mro():
|
||||
subclass._instance = saved_shell_instance
|
||||
if ps1 is not None:
|
||||
sys.ps1 = ps1
|
||||
sys.ps2 = ps2
|
File diff suppressed because it is too large
Load Diff
338
.venv/lib/python3.12/site-packages/IPython/terminal/ipapp.py
Normal file
338
.venv/lib/python3.12/site-packages/IPython/terminal/ipapp.py
Normal file
@@ -0,0 +1,338 @@
|
||||
# encoding: utf-8
|
||||
"""
|
||||
The :class:`~traitlets.config.application.Application` object for the command
|
||||
line :command:`ipython` program.
|
||||
"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from traitlets.config.loader import Config
|
||||
from traitlets.config.application import boolean_flag, catch_config_error
|
||||
from IPython.core import release
|
||||
from IPython.core import usage
|
||||
from IPython.core.completer import IPCompleter
|
||||
from IPython.core.crashhandler import CrashHandler
|
||||
from IPython.core.formatters import PlainTextFormatter
|
||||
from IPython.core.history import HistoryManager
|
||||
from IPython.core.application import (
|
||||
ProfileDir, BaseIPythonApplication, base_flags, base_aliases
|
||||
)
|
||||
from IPython.core.magic import MagicsManager
|
||||
from IPython.core.magics import (
|
||||
ScriptMagics, LoggingMagics
|
||||
)
|
||||
from IPython.core.shellapp import (
|
||||
InteractiveShellApp, shell_flags, shell_aliases
|
||||
)
|
||||
from IPython.extensions.storemagic import StoreMagics
|
||||
from .interactiveshell import TerminalInteractiveShell
|
||||
from IPython.paths import get_ipython_dir
|
||||
from traitlets import (
|
||||
Bool, List, default, observe, Type
|
||||
)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Globals, utilities and helpers
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
_examples = """
|
||||
ipython --matplotlib # enable matplotlib integration
|
||||
ipython --matplotlib=qt # enable matplotlib integration with qt4 backend
|
||||
|
||||
ipython --log-level=DEBUG # set logging to DEBUG
|
||||
ipython --profile=foo # start with profile foo
|
||||
|
||||
ipython profile create foo # create profile foo w/ default config files
|
||||
ipython help profile # show the help for the profile subcmd
|
||||
|
||||
ipython locate # print the path to the IPython directory
|
||||
ipython locate profile foo # print the path to the directory for profile `foo`
|
||||
"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Crash handler for this application
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class IPAppCrashHandler(CrashHandler):
|
||||
"""sys.excepthook for IPython itself, leaves a detailed report on disk."""
|
||||
|
||||
def __init__(self, app):
|
||||
contact_name = release.author
|
||||
contact_email = release.author_email
|
||||
bug_tracker = 'https://github.com/ipython/ipython/issues'
|
||||
super(IPAppCrashHandler,self).__init__(
|
||||
app, contact_name, contact_email, bug_tracker
|
||||
)
|
||||
|
||||
def make_report(self,traceback):
|
||||
"""Return a string containing a crash report."""
|
||||
|
||||
sec_sep = self.section_sep
|
||||
# Start with parent report
|
||||
report = [super(IPAppCrashHandler, self).make_report(traceback)]
|
||||
# Add interactive-specific info we may have
|
||||
rpt_add = report.append
|
||||
try:
|
||||
rpt_add(sec_sep+"History of session input:")
|
||||
for line in self.app.shell.user_ns['_ih']:
|
||||
rpt_add(line)
|
||||
rpt_add('\n*** Last line of input (may not be in above history):\n')
|
||||
rpt_add(self.app.shell._last_input_line+'\n')
|
||||
except:
|
||||
pass
|
||||
|
||||
return ''.join(report)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Aliases and Flags
|
||||
#-----------------------------------------------------------------------------
|
||||
flags = dict(base_flags)
|
||||
flags.update(shell_flags)
|
||||
frontend_flags = {}
|
||||
addflag = lambda *args: frontend_flags.update(boolean_flag(*args))
|
||||
addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
|
||||
'Turn on auto editing of files with syntax errors.',
|
||||
'Turn off auto editing of files with syntax errors.'
|
||||
)
|
||||
addflag('simple-prompt', 'TerminalInteractiveShell.simple_prompt',
|
||||
"Force simple minimal prompt using `raw_input`",
|
||||
"Use a rich interactive prompt with prompt_toolkit",
|
||||
)
|
||||
|
||||
addflag('banner', 'TerminalIPythonApp.display_banner',
|
||||
"Display a banner upon starting IPython.",
|
||||
"Don't display a banner upon starting IPython."
|
||||
)
|
||||
addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
|
||||
"""Set to confirm when you try to exit IPython with an EOF (Control-D
|
||||
in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
|
||||
you can force a direct exit without any confirmation.""",
|
||||
"Don't prompt the user when exiting."
|
||||
)
|
||||
addflag('term-title', 'TerminalInteractiveShell.term_title',
|
||||
"Enable auto setting the terminal title.",
|
||||
"Disable auto setting the terminal title."
|
||||
)
|
||||
classic_config = Config()
|
||||
classic_config.InteractiveShell.cache_size = 0
|
||||
classic_config.PlainTextFormatter.pprint = False
|
||||
classic_config.TerminalInteractiveShell.prompts_class='IPython.terminal.prompts.ClassicPrompts'
|
||||
classic_config.InteractiveShell.separate_in = ''
|
||||
classic_config.InteractiveShell.separate_out = ''
|
||||
classic_config.InteractiveShell.separate_out2 = ''
|
||||
classic_config.InteractiveShell.colors = 'NoColor'
|
||||
classic_config.InteractiveShell.xmode = 'Plain'
|
||||
|
||||
frontend_flags['classic']=(
|
||||
classic_config,
|
||||
"Gives IPython a similar feel to the classic Python prompt."
|
||||
)
|
||||
# # log doesn't make so much sense this way anymore
|
||||
# paa('--log','-l',
|
||||
# action='store_true', dest='InteractiveShell.logstart',
|
||||
# help="Start logging to the default log file (./ipython_log.py).")
|
||||
#
|
||||
# # quick is harder to implement
|
||||
frontend_flags['quick']=(
|
||||
{'TerminalIPythonApp' : {'quick' : True}},
|
||||
"Enable quick startup with no config files."
|
||||
)
|
||||
|
||||
frontend_flags['i'] = (
|
||||
{'TerminalIPythonApp' : {'force_interact' : True}},
|
||||
"""If running code from the command line, become interactive afterwards.
|
||||
It is often useful to follow this with `--` to treat remaining flags as
|
||||
script arguments.
|
||||
"""
|
||||
)
|
||||
flags.update(frontend_flags)
|
||||
|
||||
aliases = dict(base_aliases)
|
||||
aliases.update(shell_aliases) # type: ignore[arg-type]
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Main classes and functions
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LocateIPythonApp(BaseIPythonApplication):
|
||||
description = """print the path to the IPython dir"""
|
||||
subcommands = dict(
|
||||
profile=('IPython.core.profileapp.ProfileLocate',
|
||||
"print the path to an IPython profile directory",
|
||||
),
|
||||
)
|
||||
def start(self):
|
||||
if self.subapp is not None:
|
||||
return self.subapp.start()
|
||||
else:
|
||||
print(self.ipython_dir)
|
||||
|
||||
|
||||
class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
|
||||
name = "ipython"
|
||||
description = usage.cl_usage
|
||||
crash_handler_class = IPAppCrashHandler # typing: ignore[assignment]
|
||||
examples = _examples
|
||||
|
||||
flags = flags
|
||||
aliases = aliases
|
||||
classes = List()
|
||||
|
||||
interactive_shell_class = Type(
|
||||
klass=object, # use default_value otherwise which only allow subclasses.
|
||||
default_value=TerminalInteractiveShell,
|
||||
help="Class to use to instantiate the TerminalInteractiveShell object. Useful for custom Frontends"
|
||||
).tag(config=True)
|
||||
|
||||
@default('classes')
|
||||
def _classes_default(self):
|
||||
"""This has to be in a method, for TerminalIPythonApp to be available."""
|
||||
return [
|
||||
InteractiveShellApp, # ShellApp comes before TerminalApp, because
|
||||
self.__class__, # it will also affect subclasses (e.g. QtConsole)
|
||||
TerminalInteractiveShell,
|
||||
HistoryManager,
|
||||
MagicsManager,
|
||||
ProfileDir,
|
||||
PlainTextFormatter,
|
||||
IPCompleter,
|
||||
ScriptMagics,
|
||||
LoggingMagics,
|
||||
StoreMagics,
|
||||
]
|
||||
|
||||
subcommands = dict(
|
||||
profile = ("IPython.core.profileapp.ProfileApp",
|
||||
"Create and manage IPython profiles."
|
||||
),
|
||||
kernel = ("ipykernel.kernelapp.IPKernelApp",
|
||||
"Start a kernel without an attached frontend."
|
||||
),
|
||||
locate=('IPython.terminal.ipapp.LocateIPythonApp',
|
||||
LocateIPythonApp.description
|
||||
),
|
||||
history=('IPython.core.historyapp.HistoryApp',
|
||||
"Manage the IPython history database."
|
||||
),
|
||||
)
|
||||
|
||||
# *do* autocreate requested profile, but don't create the config file.
|
||||
auto_create = Bool(True).tag(config=True)
|
||||
|
||||
# configurables
|
||||
quick = Bool(False,
|
||||
help="""Start IPython quickly by skipping the loading of config files."""
|
||||
).tag(config=True)
|
||||
@observe('quick')
|
||||
def _quick_changed(self, change):
|
||||
if change['new']:
|
||||
self.load_config_file = lambda *a, **kw: None
|
||||
|
||||
display_banner = Bool(True,
|
||||
help="Whether to display a banner upon starting IPython."
|
||||
).tag(config=True)
|
||||
|
||||
# if there is code of files to run from the cmd line, don't interact
|
||||
# unless the --i flag (App.force_interact) is true.
|
||||
force_interact = Bool(False,
|
||||
help="""If a command or file is given via the command-line,
|
||||
e.g. 'ipython foo.py', start an interactive shell after executing the
|
||||
file or command."""
|
||||
).tag(config=True)
|
||||
@observe('force_interact')
|
||||
def _force_interact_changed(self, change):
|
||||
if change['new']:
|
||||
self.interact = True
|
||||
|
||||
@observe('file_to_run', 'code_to_run', 'module_to_run')
|
||||
def _file_to_run_changed(self, change):
|
||||
new = change['new']
|
||||
if new:
|
||||
self.something_to_run = True
|
||||
if new and not self.force_interact:
|
||||
self.interact = False
|
||||
|
||||
# internal, not-configurable
|
||||
something_to_run=Bool(False)
|
||||
|
||||
@catch_config_error
|
||||
def initialize(self, argv=None):
|
||||
"""Do actions after construct, but before starting the app."""
|
||||
super(TerminalIPythonApp, self).initialize(argv)
|
||||
if self.subapp is not None:
|
||||
# don't bother initializing further, starting subapp
|
||||
return
|
||||
# print(self.extra_args)
|
||||
if self.extra_args and not self.something_to_run:
|
||||
self.file_to_run = self.extra_args[0]
|
||||
self.init_path()
|
||||
# create the shell
|
||||
self.init_shell()
|
||||
# and draw the banner
|
||||
self.init_banner()
|
||||
# Now a variety of things that happen after the banner is printed.
|
||||
self.init_gui_pylab()
|
||||
self.init_extensions()
|
||||
self.init_code()
|
||||
|
||||
def init_shell(self):
|
||||
"""initialize the InteractiveShell instance"""
|
||||
# Create an InteractiveShell instance.
|
||||
# shell.display_banner should always be False for the terminal
|
||||
# based app, because we call shell.show_banner() by hand below
|
||||
# so the banner shows *before* all extension loading stuff.
|
||||
self.shell = self.interactive_shell_class.instance(parent=self,
|
||||
profile_dir=self.profile_dir,
|
||||
ipython_dir=self.ipython_dir, user_ns=self.user_ns)
|
||||
self.shell.configurables.append(self)
|
||||
|
||||
def init_banner(self):
|
||||
"""optionally display the banner"""
|
||||
if self.display_banner and self.interact:
|
||||
self.shell.show_banner()
|
||||
# Make sure there is a space below the banner.
|
||||
if self.log_level <= logging.INFO: print()
|
||||
|
||||
def _pylab_changed(self, name, old, new):
|
||||
"""Replace --pylab='inline' with --pylab='auto'"""
|
||||
if new == 'inline':
|
||||
warnings.warn("'inline' not available as pylab backend, "
|
||||
"using 'auto' instead.")
|
||||
self.pylab = 'auto'
|
||||
|
||||
def start(self):
|
||||
if self.subapp is not None:
|
||||
return self.subapp.start()
|
||||
# perform any prexec steps:
|
||||
if self.interact:
|
||||
self.log.debug("Starting IPython's mainloop...")
|
||||
self.shell.mainloop()
|
||||
else:
|
||||
self.log.debug("IPython not interactive...")
|
||||
self.shell.restore_term_title()
|
||||
if not self.shell.last_execution_succeeded:
|
||||
sys.exit(1)
|
||||
|
||||
def load_default_config(ipython_dir=None):
|
||||
"""Load the default config file from the default ipython_dir.
|
||||
|
||||
This is useful for embedded shells.
|
||||
"""
|
||||
if ipython_dir is None:
|
||||
ipython_dir = get_ipython_dir()
|
||||
|
||||
profile_dir = os.path.join(ipython_dir, 'profile_default')
|
||||
app = TerminalIPythonApp()
|
||||
app.config_file_paths.append(profile_dir)
|
||||
app.load_config_file()
|
||||
return app.config
|
||||
|
||||
launch_new_instance = TerminalIPythonApp.launch_instance
|
214
.venv/lib/python3.12/site-packages/IPython/terminal/magics.py
Normal file
214
.venv/lib/python3.12/site-packages/IPython/terminal/magics.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Extra magics for terminal use."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
from logging import error
|
||||
import os
|
||||
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
|
||||
|
||||
def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False):
|
||||
""" Yield pasted lines until the user enters the given sentinel value.
|
||||
"""
|
||||
if not quiet:
|
||||
print("Pasting code; enter '%s' alone on the line to stop or use Ctrl-D." \
|
||||
% sentinel)
|
||||
prompt = ":"
|
||||
else:
|
||||
prompt = ""
|
||||
while True:
|
||||
try:
|
||||
l = l_input(prompt)
|
||||
if l == sentinel:
|
||||
return
|
||||
else:
|
||||
yield l
|
||||
except EOFError:
|
||||
print('<EOF>')
|
||||
return
|
||||
|
||||
|
||||
@magics_class
|
||||
class TerminalMagics(Magics):
|
||||
def __init__(self, shell):
|
||||
super(TerminalMagics, self).__init__(shell)
|
||||
|
||||
def store_or_execute(self, block, name, store_history=False):
|
||||
""" Execute a block, or store it in a variable, per the user's request.
|
||||
"""
|
||||
if name:
|
||||
# If storing it for further editing
|
||||
self.shell.user_ns[name] = SList(block.splitlines())
|
||||
print("Block assigned to '%s'" % name)
|
||||
else:
|
||||
b = self.preclean_input(block)
|
||||
self.shell.user_ns['pasted_block'] = b
|
||||
self.shell.using_paste_magics = True
|
||||
try:
|
||||
self.shell.run_cell(b, store_history)
|
||||
finally:
|
||||
self.shell.using_paste_magics = False
|
||||
|
||||
def preclean_input(self, block):
|
||||
lines = block.splitlines()
|
||||
while lines and not lines[0].strip():
|
||||
lines = lines[1:]
|
||||
return strip_email_quotes('\n'.join(lines))
|
||||
|
||||
def rerun_pasted(self, name='pasted_block'):
|
||||
""" Rerun a previously pasted command.
|
||||
"""
|
||||
b = self.shell.user_ns.get(name)
|
||||
|
||||
# Sanity checks
|
||||
if b is None:
|
||||
raise UsageError('No previous pasted block available')
|
||||
if not isinstance(b, str):
|
||||
raise UsageError(
|
||||
"Variable 'pasted_block' is not a string, can't execute")
|
||||
|
||||
print("Re-executing '%s...' (%d chars)"% (b.split('\n',1)[0], len(b)))
|
||||
self.shell.run_cell(b)
|
||||
|
||||
@line_magic
|
||||
def autoindent(self, parameter_s = ''):
|
||||
"""Toggle autoindent on/off (deprecated)"""
|
||||
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.
|
||||
|
||||
You must terminate the block with '--' (two minus-signs) or Ctrl-D
|
||||
alone on the line. You can also provide your own sentinel with '%paste
|
||||
-s %%' ('%%' is the new sentinel for this operation).
|
||||
|
||||
The block is dedented prior to execution to enable execution of method
|
||||
definitions. '>' and '+' characters at the beginning of a line are
|
||||
ignored, to allow pasting directly from e-mails, diff files and
|
||||
doctests (the '...' continuation prompt is also stripped). The
|
||||
executed block is also assigned to variable named 'pasted_block' for
|
||||
later editing with '%edit pasted_block'.
|
||||
|
||||
You can also pass a variable name as an argument, e.g. '%cpaste foo'.
|
||||
This assigns the pasted block to variable 'foo' as string, without
|
||||
dedenting or executing it (preceding >>> and + is still stripped)
|
||||
|
||||
'%cpaste -r' re-executes the block previously entered by cpaste.
|
||||
'%cpaste -q' suppresses any additional output messages.
|
||||
|
||||
Do not be alarmed by garbled output on Windows (it's a readline bug).
|
||||
Just press enter and type -- (and press enter again) and the block
|
||||
will be what was just pasted.
|
||||
|
||||
Shell escapes are not supported (yet).
|
||||
|
||||
See Also
|
||||
--------
|
||||
paste : automatically pull code from clipboard.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
In [8]: %cpaste
|
||||
Pasting code; enter '--' alone on the line to stop.
|
||||
:>>> a = ["world!", "Hello"]
|
||||
:>>> print(" ".join(sorted(a)))
|
||||
:--
|
||||
Hello world!
|
||||
|
||||
::
|
||||
In [8]: %cpaste
|
||||
Pasting code; enter '--' alone on the line to stop.
|
||||
:>>> %alias_magic t timeit
|
||||
:>>> %t -n1 pass
|
||||
:--
|
||||
Created `%t` as an alias for `%timeit`.
|
||||
Created `%%t` as an alias for `%%timeit`.
|
||||
354 ns ± 224 ns per loop (mean ± std. dev. of 7 runs, 1 loop each)
|
||||
"""
|
||||
opts, name = self.parse_options(parameter_s, 'rqs:', mode='string')
|
||||
if 'r' in opts:
|
||||
self.rerun_pasted()
|
||||
return
|
||||
|
||||
quiet = ('q' in opts)
|
||||
|
||||
sentinel = opts.get('s', u'--')
|
||||
block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet))
|
||||
self.store_or_execute(block, name, store_history=True)
|
||||
|
||||
@line_magic
|
||||
def paste(self, parameter_s=''):
|
||||
"""Paste & execute a pre-formatted code block from clipboard.
|
||||
|
||||
The text is pulled directly from the clipboard without user
|
||||
intervention and printed back on the screen before execution (unless
|
||||
the -q flag is given to force quiet mode).
|
||||
|
||||
The block is dedented prior to execution to enable execution of method
|
||||
definitions. '>' and '+' characters at the beginning of a line are
|
||||
ignored, to allow pasting directly from e-mails, diff files and
|
||||
doctests (the '...' continuation prompt is also stripped). The
|
||||
executed block is also assigned to variable named 'pasted_block' for
|
||||
later editing with '%edit pasted_block'.
|
||||
|
||||
You can also pass a variable name as an argument, e.g. '%paste foo'.
|
||||
This assigns the pasted block to variable 'foo' as string, without
|
||||
executing it (preceding >>> and + is still stripped).
|
||||
|
||||
Options:
|
||||
|
||||
-r: re-executes the block previously entered by cpaste.
|
||||
|
||||
-q: quiet mode: do not echo the pasted text back to the terminal.
|
||||
|
||||
IPython statements (magics, shell escapes) are not supported (yet).
|
||||
|
||||
See Also
|
||||
--------
|
||||
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:
|
||||
self.rerun_pasted()
|
||||
return
|
||||
try:
|
||||
block = self.shell.hooks.clipboard_get()
|
||||
except TryNext as clipboard_exc:
|
||||
message = getattr(clipboard_exc, 'args')
|
||||
if message:
|
||||
error(message[0])
|
||||
else:
|
||||
error('Could not get text from the clipboard.')
|
||||
return
|
||||
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:
|
||||
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, store_history=True)
|
||||
|
||||
# Class-level: add a '%cls' magic only on Windows
|
||||
if sys.platform == 'win32':
|
||||
@line_magic
|
||||
def cls(self, s):
|
||||
"""Clear screen.
|
||||
"""
|
||||
os.system("cls")
|
128
.venv/lib/python3.12/site-packages/IPython/terminal/prompts.py
Normal file
128
.venv/lib/python3.12/site-packages/IPython/terminal/prompts.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Terminal input and output prompts."""
|
||||
|
||||
from pygments.token import Token
|
||||
import sys
|
||||
|
||||
from IPython.core.displayhook import DisplayHook
|
||||
|
||||
from prompt_toolkit.formatted_text import fragment_list_width, PygmentsTokens
|
||||
from prompt_toolkit.shortcuts import print_formatted_text
|
||||
from prompt_toolkit.enums import EditingMode
|
||||
|
||||
|
||||
class Prompts(object):
|
||||
def __init__(self, shell):
|
||||
self.shell = shell
|
||||
|
||||
def vi_mode(self):
|
||||
if (getattr(self.shell.pt_app, 'editing_mode', None) == EditingMode.VI
|
||||
and self.shell.prompt_includes_vi_mode):
|
||||
mode = str(self.shell.pt_app.app.vi_state.input_mode)
|
||||
if mode.startswith('InputMode.'):
|
||||
mode = mode[10:13].lower()
|
||||
elif mode.startswith('vi-'):
|
||||
mode = mode[3:6]
|
||||
return '['+mode+'] '
|
||||
return ''
|
||||
|
||||
def current_line(self) -> int:
|
||||
if self.shell.pt_app is not None:
|
||||
return self.shell.pt_app.default_buffer.document.cursor_position_row or 0
|
||||
return 0
|
||||
|
||||
def in_prompt_tokens(self):
|
||||
return [
|
||||
(Token.Prompt, self.vi_mode()),
|
||||
(
|
||||
Token.Prompt,
|
||||
self.shell.prompt_line_number_format.format(
|
||||
line=1, rel_line=-self.current_line()
|
||||
),
|
||||
),
|
||||
(Token.Prompt, "In ["),
|
||||
(Token.PromptNum, str(self.shell.execution_count)),
|
||||
(Token.Prompt, ']: '),
|
||||
]
|
||||
|
||||
def _width(self):
|
||||
return fragment_list_width(self.in_prompt_tokens())
|
||||
|
||||
def continuation_prompt_tokens(self, width=None, *, lineno=None):
|
||||
if width is None:
|
||||
width = self._width()
|
||||
line = lineno + 1 if lineno is not None else 0
|
||||
prefix = " " * len(
|
||||
self.vi_mode()
|
||||
) + self.shell.prompt_line_number_format.format(
|
||||
line=line, rel_line=line - self.current_line() - 1
|
||||
)
|
||||
return [
|
||||
(
|
||||
Token.Prompt,
|
||||
prefix + (" " * (width - len(prefix) - 5)) + "...: ",
|
||||
),
|
||||
]
|
||||
|
||||
def rewrite_prompt_tokens(self):
|
||||
width = self._width()
|
||||
return [
|
||||
(Token.Prompt, ('-' * (width - 2)) + '> '),
|
||||
]
|
||||
|
||||
def out_prompt_tokens(self):
|
||||
return [
|
||||
(Token.OutPrompt, 'Out['),
|
||||
(Token.OutPromptNum, str(self.shell.execution_count)),
|
||||
(Token.OutPrompt, ']: '),
|
||||
]
|
||||
|
||||
class ClassicPrompts(Prompts):
|
||||
def in_prompt_tokens(self):
|
||||
return [
|
||||
(Token.Prompt, '>>> '),
|
||||
]
|
||||
|
||||
def continuation_prompt_tokens(self, width=None):
|
||||
return [
|
||||
(Token.Prompt, '... ')
|
||||
]
|
||||
|
||||
def rewrite_prompt_tokens(self):
|
||||
return []
|
||||
|
||||
def out_prompt_tokens(self):
|
||||
return []
|
||||
|
||||
class RichPromptDisplayHook(DisplayHook):
|
||||
"""Subclass of base display hook using coloured prompt"""
|
||||
def write_output_prompt(self):
|
||||
sys.stdout.write(self.shell.separate_out)
|
||||
# If we're not displaying a prompt, it effectively ends with a newline,
|
||||
# because the output will be left-aligned.
|
||||
self.prompt_end_newline = True
|
||||
|
||||
if self.do_full_cache:
|
||||
tokens = self.shell.prompts.out_prompt_tokens()
|
||||
prompt_txt = "".join(s for _, s in tokens)
|
||||
if prompt_txt and not prompt_txt.endswith("\n"):
|
||||
# Ask for a newline before multiline output
|
||||
self.prompt_end_newline = False
|
||||
|
||||
if self.shell.pt_app:
|
||||
print_formatted_text(PygmentsTokens(tokens),
|
||||
style=self.shell.pt_app.app.style, end='',
|
||||
)
|
||||
else:
|
||||
sys.stdout.write(prompt_txt)
|
||||
|
||||
def write_format_data(self, format_dict, md_dict=None) -> None:
|
||||
assert self.shell is not None
|
||||
if self.shell.mime_renderers:
|
||||
|
||||
for mime, handler in self.shell.mime_renderers.items():
|
||||
if mime in format_dict:
|
||||
handler(format_dict[mime], None)
|
||||
return
|
||||
|
||||
super().write_format_data(format_dict, md_dict)
|
||||
|
@@ -0,0 +1,139 @@
|
||||
import importlib
|
||||
import os
|
||||
from typing import Tuple, Callable
|
||||
|
||||
aliases = {
|
||||
'qt4': 'qt',
|
||||
'gtk2': 'gtk',
|
||||
}
|
||||
|
||||
backends = [
|
||||
"qt",
|
||||
"qt5",
|
||||
"qt6",
|
||||
"gtk",
|
||||
"gtk2",
|
||||
"gtk3",
|
||||
"gtk4",
|
||||
"tk",
|
||||
"wx",
|
||||
"pyglet",
|
||||
"glut",
|
||||
"osx",
|
||||
"asyncio",
|
||||
]
|
||||
|
||||
registered = {}
|
||||
|
||||
def register(name, inputhook):
|
||||
"""Register the function *inputhook* as an event loop integration."""
|
||||
registered[name] = inputhook
|
||||
|
||||
|
||||
class UnknownBackend(KeyError):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return ("No event loop integration for {!r}. "
|
||||
"Supported event loops are: {}").format(self.name,
|
||||
', '.join(backends + sorted(registered)))
|
||||
|
||||
|
||||
def set_qt_api(gui):
|
||||
"""Sets the `QT_API` environment variable if it isn't already set."""
|
||||
|
||||
qt_api = os.environ.get("QT_API", None)
|
||||
|
||||
from IPython.external.qt_loaders import (
|
||||
QT_API_PYQT,
|
||||
QT_API_PYQT5,
|
||||
QT_API_PYQT6,
|
||||
QT_API_PYSIDE,
|
||||
QT_API_PYSIDE2,
|
||||
QT_API_PYSIDE6,
|
||||
QT_API_PYQTv1,
|
||||
loaded_api,
|
||||
)
|
||||
|
||||
loaded = loaded_api()
|
||||
|
||||
qt_env2gui = {
|
||||
QT_API_PYSIDE: "qt4",
|
||||
QT_API_PYQTv1: "qt4",
|
||||
QT_API_PYQT: "qt4",
|
||||
QT_API_PYSIDE2: "qt5",
|
||||
QT_API_PYQT5: "qt5",
|
||||
QT_API_PYSIDE6: "qt6",
|
||||
QT_API_PYQT6: "qt6",
|
||||
}
|
||||
if loaded is not None and gui != "qt":
|
||||
if qt_env2gui[loaded] != gui:
|
||||
print(
|
||||
f"Cannot switch Qt versions for this session; will use {qt_env2gui[loaded]}."
|
||||
)
|
||||
return qt_env2gui[loaded]
|
||||
|
||||
if qt_api is not None and gui != "qt":
|
||||
if qt_env2gui[qt_api] != gui:
|
||||
print(
|
||||
f'Request for "{gui}" will be ignored because `QT_API` '
|
||||
f'environment variable is set to "{qt_api}"'
|
||||
)
|
||||
return qt_env2gui[qt_api]
|
||||
else:
|
||||
if gui == "qt5":
|
||||
try:
|
||||
import PyQt5 # noqa
|
||||
|
||||
os.environ["QT_API"] = "pyqt5"
|
||||
except ImportError:
|
||||
try:
|
||||
import PySide2 # noqa
|
||||
|
||||
os.environ["QT_API"] = "pyside2"
|
||||
except ImportError:
|
||||
os.environ["QT_API"] = "pyqt5"
|
||||
elif gui == "qt6":
|
||||
try:
|
||||
import PyQt6 # noqa
|
||||
|
||||
os.environ["QT_API"] = "pyqt6"
|
||||
except ImportError:
|
||||
try:
|
||||
import PySide6 # noqa
|
||||
|
||||
os.environ["QT_API"] = "pyside6"
|
||||
except ImportError:
|
||||
os.environ["QT_API"] = "pyqt6"
|
||||
elif gui == "qt":
|
||||
# Don't set QT_API; let IPython logic choose the version.
|
||||
if "QT_API" in os.environ.keys():
|
||||
del os.environ["QT_API"]
|
||||
else:
|
||||
print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
|
||||
return
|
||||
|
||||
# Import it now so we can figure out which version it is.
|
||||
from IPython.external.qt_for_kernel import QT_API
|
||||
|
||||
return qt_env2gui[QT_API]
|
||||
|
||||
|
||||
def get_inputhook_name_and_func(gui: str) -> Tuple[str, Callable]:
|
||||
if gui in registered:
|
||||
return gui, registered[gui]
|
||||
|
||||
if gui not in backends:
|
||||
raise UnknownBackend(gui)
|
||||
|
||||
if gui in aliases:
|
||||
return get_inputhook_name_and_func(aliases[gui])
|
||||
|
||||
gui_mod = gui
|
||||
if gui.startswith("qt"):
|
||||
gui = set_qt_api(gui)
|
||||
gui_mod = "qt"
|
||||
|
||||
mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod)
|
||||
return gui, mod.inputhook
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Inputhook for running the original asyncio event loop while we're waiting for
|
||||
input.
|
||||
|
||||
By default, in IPython, we run the prompt with a different asyncio event loop,
|
||||
because otherwise we risk that people are freezing the prompt by scheduling bad
|
||||
coroutines. E.g., a coroutine that does a while/true and never yield back
|
||||
control to the loop. We can't cancel that.
|
||||
|
||||
However, sometimes we want the asyncio loop to keep running while waiting for
|
||||
a prompt.
|
||||
|
||||
The following example will print the numbers from 1 to 10 above the prompt,
|
||||
while we are waiting for input. (This works also because we use
|
||||
prompt_toolkit`s `patch_stdout`)::
|
||||
|
||||
In [1]: import asyncio
|
||||
|
||||
In [2]: %gui asyncio
|
||||
|
||||
In [3]: async def f():
|
||||
...: for i in range(10):
|
||||
...: await asyncio.sleep(1)
|
||||
...: print(i)
|
||||
|
||||
|
||||
In [4]: asyncio.ensure_future(f())
|
||||
|
||||
"""
|
||||
|
||||
from prompt_toolkit import __version__ as ptk_version
|
||||
|
||||
from IPython.core.async_helpers import get_asyncio_loop
|
||||
|
||||
PTK3 = ptk_version.startswith("3.")
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
"""
|
||||
Inputhook for asyncio event loop integration.
|
||||
"""
|
||||
# For prompt_toolkit 3.0, this input hook literally doesn't do anything.
|
||||
# The event loop integration here is implemented in `interactiveshell.py`
|
||||
# by running the prompt itself in the current asyncio loop. The main reason
|
||||
# for this is that nesting asyncio event loops is unreliable.
|
||||
if PTK3:
|
||||
return
|
||||
|
||||
# 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()
|
||||
|
||||
fileno = context.fileno()
|
||||
loop.add_reader(fileno, stop)
|
||||
try:
|
||||
loop.run_forever()
|
||||
finally:
|
||||
loop.remove_reader(fileno)
|
@@ -0,0 +1,140 @@
|
||||
"""GLUT Input hook for interactive use with prompt_toolkit
|
||||
"""
|
||||
|
||||
|
||||
# GLUT is quite an old library and it is difficult to ensure proper
|
||||
# integration within IPython since original GLUT does not allow to handle
|
||||
# events one by one. Instead, it requires for the mainloop to be entered
|
||||
# and never returned (there is not even a function to exit he
|
||||
# mainloop). Fortunately, there are alternatives such as freeglut
|
||||
# (available for linux and windows) and the OSX implementation gives
|
||||
# access to a glutCheckLoop() function that blocks itself until a new
|
||||
# event is received. This means we have to setup the idle callback to
|
||||
# ensure we got at least one event that will unblock the function.
|
||||
#
|
||||
# Furthermore, it is not possible to install these handlers without a window
|
||||
# being first created. We choose to make this window invisible. This means that
|
||||
# display mode options are set at this level and user won't be able to change
|
||||
# them later without modifying the code. This should probably be made available
|
||||
# via IPython options system.
|
||||
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import OpenGL.GLUT as glut
|
||||
import OpenGL.platform as platform
|
||||
from timeit import default_timer as clock
|
||||
|
||||
# Frame per second : 60
|
||||
# Should probably be an IPython option
|
||||
glut_fps = 60
|
||||
|
||||
# Display mode : double buffeed + rgba + depth
|
||||
# Should probably be an IPython option
|
||||
glut_display_mode = (glut.GLUT_DOUBLE |
|
||||
glut.GLUT_RGBA |
|
||||
glut.GLUT_DEPTH)
|
||||
|
||||
glutMainLoopEvent = None
|
||||
if sys.platform == 'darwin':
|
||||
try:
|
||||
glutCheckLoop = platform.createBaseFunction(
|
||||
'glutCheckLoop', dll=platform.GLUT, resultType=None,
|
||||
argTypes=[],
|
||||
doc='glutCheckLoop( ) -> None',
|
||||
argNames=(),
|
||||
)
|
||||
except AttributeError as e:
|
||||
raise RuntimeError(
|
||||
'''Your glut implementation does not allow interactive sessions. '''
|
||||
'''Consider installing freeglut.''') from e
|
||||
glutMainLoopEvent = glutCheckLoop
|
||||
elif glut.HAVE_FREEGLUT:
|
||||
glutMainLoopEvent = glut.glutMainLoopEvent
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'''Your glut implementation does not allow interactive sessions. '''
|
||||
'''Consider installing freeglut.''')
|
||||
|
||||
|
||||
def glut_display():
|
||||
# Dummy display function
|
||||
pass
|
||||
|
||||
def glut_idle():
|
||||
# Dummy idle function
|
||||
pass
|
||||
|
||||
def glut_close():
|
||||
# Close function only hides the current window
|
||||
glut.glutHideWindow()
|
||||
glutMainLoopEvent()
|
||||
|
||||
def glut_int_handler(signum, frame):
|
||||
# Catch sigint and print the defaultipyt message
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
print('\nKeyboardInterrupt')
|
||||
# Need to reprint the prompt at this stage
|
||||
|
||||
# Initialisation code
|
||||
glut.glutInit( sys.argv )
|
||||
glut.glutInitDisplayMode( glut_display_mode )
|
||||
# This is specific to freeglut
|
||||
if bool(glut.glutSetOption):
|
||||
glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
|
||||
glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
|
||||
glut.glutCreateWindow( b'ipython' )
|
||||
glut.glutReshapeWindow( 1, 1 )
|
||||
glut.glutHideWindow( )
|
||||
glut.glutWMCloseFunc( glut_close )
|
||||
glut.glutDisplayFunc( glut_display )
|
||||
glut.glutIdleFunc( glut_idle )
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
"""Run the pyglet event loop by processing pending events only.
|
||||
|
||||
This keeps processing pending events until stdin is ready. After
|
||||
processing all pending events, a call to time.sleep is inserted. This is
|
||||
needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
|
||||
though for best performance.
|
||||
"""
|
||||
# We need to protect against a user pressing Control-C when IPython is
|
||||
# idle and this is running. We trap KeyboardInterrupt and pass.
|
||||
|
||||
signal.signal(signal.SIGINT, glut_int_handler)
|
||||
|
||||
try:
|
||||
t = clock()
|
||||
|
||||
# Make sure the default window is set after a window has been closed
|
||||
if glut.glutGetWindow() == 0:
|
||||
glut.glutSetWindow( 1 )
|
||||
glutMainLoopEvent()
|
||||
return 0
|
||||
|
||||
while not context.input_is_ready():
|
||||
glutMainLoopEvent()
|
||||
# We need to sleep at this point to keep the idle CPU load
|
||||
# low. However, if sleep to long, GUI response is poor. As
|
||||
# a compromise, we watch how often GUI events are being processed
|
||||
# and switch between a short and long sleep time. Here are some
|
||||
# stats useful in helping to tune this.
|
||||
# time CPU load
|
||||
# 0.001 13%
|
||||
# 0.005 3%
|
||||
# 0.01 1.5%
|
||||
# 0.05 0.5%
|
||||
used_time = clock() - t
|
||||
if used_time > 10.0:
|
||||
# print('Sleep for 1 s') # dbg
|
||||
time.sleep(1.0)
|
||||
elif used_time > 0.1:
|
||||
# Few GUI events coming in, so we can sleep longer
|
||||
# print('Sleep for 0.05 s') # dbg
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
# Many GUI events coming in, so sleep only very little
|
||||
time.sleep(0.001)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
@@ -0,0 +1,60 @@
|
||||
# Code borrowed from python-prompt-toolkit examples
|
||||
# https://github.com/jonathanslenders/python-prompt-toolkit/blob/77cdcfbc7f4b4c34a9d2f9a34d422d7152f16209/examples/inputhook.py
|
||||
|
||||
# Copyright (c) 2014, Jonathan Slenders
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright notice, this
|
||||
# list of conditions and the following disclaimer in the documentation and/or
|
||||
# other materials provided with the distribution.
|
||||
#
|
||||
# * Neither the name of the {organization} nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
PyGTK input hook for prompt_toolkit.
|
||||
|
||||
Listens on the pipe prompt_toolkit sets up for a notification that it should
|
||||
return control to the terminal event loop.
|
||||
"""
|
||||
|
||||
import gtk, gobject
|
||||
|
||||
# Enable threading in GTK. (Otherwise, GTK will keep the GIL.)
|
||||
gtk.gdk.threads_init()
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
"""
|
||||
When the eventloop of prompt-toolkit is idle, call this inputhook.
|
||||
|
||||
This will run the GTK main loop until the file descriptor
|
||||
`context.fileno()` becomes ready.
|
||||
|
||||
:param context: An `InputHookContext` instance.
|
||||
"""
|
||||
|
||||
def _main_quit(*a, **kw):
|
||||
gtk.main_quit()
|
||||
return False
|
||||
|
||||
gobject.io_add_watch(context.fileno(), gobject.IO_IN, _main_quit)
|
||||
gtk.main()
|
@@ -0,0 +1,14 @@
|
||||
"""prompt_toolkit input hook for GTK 3
|
||||
"""
|
||||
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
|
||||
def _main_quit(*args, **kwargs):
|
||||
Gtk.main_quit()
|
||||
return False
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
GLib.io_add_watch(context.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, _main_quit)
|
||||
Gtk.main()
|
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
prompt_toolkit input hook for GTK 4.
|
||||
"""
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
class _InputHook:
|
||||
def __init__(self, context):
|
||||
self._quit = False
|
||||
GLib.io_add_watch(
|
||||
context.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, self.quit
|
||||
)
|
||||
|
||||
def quit(self, *args, **kwargs):
|
||||
self._quit = True
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
context = GLib.MainContext.default()
|
||||
while not self._quit:
|
||||
context.iteration(True)
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
hook = _InputHook(context)
|
||||
hook.run()
|
@@ -0,0 +1,147 @@
|
||||
"""Inputhook for OS X
|
||||
|
||||
Calls NSApp / CoreFoundation APIs via ctypes.
|
||||
"""
|
||||
|
||||
# obj-c boilerplate from appnope, used under BSD 2-clause
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
from threading import Event
|
||||
|
||||
objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type: ignore
|
||||
|
||||
void_p = ctypes.c_void_p
|
||||
|
||||
objc.objc_getClass.restype = void_p
|
||||
objc.sel_registerName.restype = void_p
|
||||
objc.objc_msgSend.restype = void_p
|
||||
objc.objc_msgSend.argtypes = [void_p, void_p]
|
||||
|
||||
msg = objc.objc_msgSend
|
||||
|
||||
def _utf8(s):
|
||||
"""ensure utf8 bytes"""
|
||||
if not isinstance(s, bytes):
|
||||
s = s.encode('utf8')
|
||||
return s
|
||||
|
||||
def n(name):
|
||||
"""create a selector name (for ObjC methods)"""
|
||||
return objc.sel_registerName(_utf8(name))
|
||||
|
||||
def C(classname):
|
||||
"""get an ObjC Class by name"""
|
||||
return objc.objc_getClass(_utf8(classname))
|
||||
|
||||
# end obj-c boilerplate from appnope
|
||||
|
||||
# CoreFoundation C-API calls we will use:
|
||||
CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation")) # type: ignore
|
||||
|
||||
CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate
|
||||
CFFileDescriptorCreate.restype = void_p
|
||||
CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p, void_p]
|
||||
|
||||
CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor
|
||||
CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int
|
||||
CFFileDescriptorGetNativeDescriptor.argtypes = [void_p]
|
||||
|
||||
CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks
|
||||
CFFileDescriptorEnableCallBacks.restype = None
|
||||
CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong]
|
||||
|
||||
CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource
|
||||
CFFileDescriptorCreateRunLoopSource.restype = void_p
|
||||
CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p]
|
||||
|
||||
CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
|
||||
CFRunLoopGetCurrent.restype = void_p
|
||||
|
||||
CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource
|
||||
CFRunLoopAddSource.restype = None
|
||||
CFRunLoopAddSource.argtypes = [void_p, void_p, void_p]
|
||||
|
||||
CFRelease = CoreFoundation.CFRelease
|
||||
CFRelease.restype = None
|
||||
CFRelease.argtypes = [void_p]
|
||||
|
||||
CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate
|
||||
CFFileDescriptorInvalidate.restype = None
|
||||
CFFileDescriptorInvalidate.argtypes = [void_p]
|
||||
|
||||
# From CFFileDescriptor.h
|
||||
kCFFileDescriptorReadCallBack = 1
|
||||
kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes')
|
||||
|
||||
|
||||
def _NSApp():
|
||||
"""Return the global NSApplication instance (NSApp)"""
|
||||
objc.objc_msgSend.argtypes = [void_p, void_p]
|
||||
return msg(C('NSApplication'), n('sharedApplication'))
|
||||
|
||||
|
||||
def _wake(NSApp):
|
||||
"""Wake the Application"""
|
||||
objc.objc_msgSend.argtypes = [
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
void_p,
|
||||
]
|
||||
event = msg(
|
||||
C("NSEvent"),
|
||||
n(
|
||||
"otherEventWithType:location:modifierFlags:"
|
||||
"timestamp:windowNumber:context:subtype:data1:data2:"
|
||||
),
|
||||
15, # Type
|
||||
0, # location
|
||||
0, # flags
|
||||
0, # timestamp
|
||||
0, # window
|
||||
None, # context
|
||||
0, # subtype
|
||||
0, # data1
|
||||
0, # data2
|
||||
)
|
||||
objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p]
|
||||
msg(NSApp, n('postEvent:atStart:'), void_p(event), True)
|
||||
|
||||
|
||||
def _input_callback(fdref, flags, info):
|
||||
"""Callback to fire when there's input to be read"""
|
||||
CFFileDescriptorInvalidate(fdref)
|
||||
CFRelease(fdref)
|
||||
NSApp = _NSApp()
|
||||
objc.objc_msgSend.argtypes = [void_p, void_p, void_p]
|
||||
msg(NSApp, n('stop:'), NSApp)
|
||||
_wake(NSApp)
|
||||
|
||||
_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p)
|
||||
_c_input_callback = _c_callback_func_type(_input_callback)
|
||||
|
||||
|
||||
def _stop_on_read(fd):
|
||||
"""Register callback to stop eventloop when there's data on fd"""
|
||||
fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None)
|
||||
CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack)
|
||||
source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0)
|
||||
loop = CFRunLoopGetCurrent()
|
||||
CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes)
|
||||
CFRelease(source)
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
"""Inputhook for Cocoa (NSApp)"""
|
||||
NSApp = _NSApp()
|
||||
_stop_on_read(context.fileno())
|
||||
objc.objc_msgSend.argtypes = [void_p, void_p]
|
||||
msg(NSApp, n('run'))
|
@@ -0,0 +1,66 @@
|
||||
"""Enable pyglet to be used interactively with prompt_toolkit
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from timeit import default_timer as clock
|
||||
import pyglet
|
||||
|
||||
# On linux only, window.flip() has a bug that causes an AttributeError on
|
||||
# window close. For details, see:
|
||||
# http://groups.google.com/group/pyglet-users/browse_thread/thread/47c1aab9aa4a3d23/c22f9e819826799e?#c22f9e819826799e
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
def flip(window):
|
||||
try:
|
||||
window.flip()
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
def flip(window):
|
||||
window.flip()
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
"""Run the pyglet event loop by processing pending events only.
|
||||
|
||||
This keeps processing pending events until stdin is ready. After
|
||||
processing all pending events, a call to time.sleep is inserted. This is
|
||||
needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
|
||||
though for best performance.
|
||||
"""
|
||||
# We need to protect against a user pressing Control-C when IPython is
|
||||
# idle and this is running. We trap KeyboardInterrupt and pass.
|
||||
try:
|
||||
t = clock()
|
||||
while not context.input_is_ready():
|
||||
pyglet.clock.tick()
|
||||
for window in pyglet.app.windows:
|
||||
window.switch_to()
|
||||
window.dispatch_events()
|
||||
window.dispatch_event('on_draw')
|
||||
flip(window)
|
||||
|
||||
# We need to sleep at this point to keep the idle CPU load
|
||||
# low. However, if sleep to long, GUI response is poor. As
|
||||
# a compromise, we watch how often GUI events are being processed
|
||||
# and switch between a short and long sleep time. Here are some
|
||||
# stats useful in helping to tune this.
|
||||
# time CPU load
|
||||
# 0.001 13%
|
||||
# 0.005 3%
|
||||
# 0.01 1.5%
|
||||
# 0.05 0.5%
|
||||
used_time = clock() - t
|
||||
if used_time > 10.0:
|
||||
# print('Sleep for 1 s') # dbg
|
||||
time.sleep(1.0)
|
||||
elif used_time > 0.1:
|
||||
# Few GUI events coming in, so we can sleep longer
|
||||
# print('Sleep for 0.05 s') # dbg
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
# Many GUI events coming in, so sleep only very little
|
||||
time.sleep(0.001)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
@@ -0,0 +1,90 @@
|
||||
import sys
|
||||
import os
|
||||
from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper
|
||||
from IPython import get_ipython
|
||||
|
||||
# If we create a QApplication, keep a reference to it so that it doesn't get
|
||||
# garbage collected.
|
||||
_appref = None
|
||||
_already_warned = False
|
||||
|
||||
|
||||
def _exec(obj):
|
||||
# exec on PyQt6, exec_ elsewhere.
|
||||
obj.exec() if hasattr(obj, "exec") else obj.exec_()
|
||||
|
||||
|
||||
def _reclaim_excepthook():
|
||||
shell = get_ipython()
|
||||
if shell is not None:
|
||||
sys.excepthook = shell.excepthook
|
||||
|
||||
|
||||
def inputhook(context):
|
||||
global _appref
|
||||
app = QtCore.QCoreApplication.instance()
|
||||
if not app:
|
||||
if sys.platform == 'linux':
|
||||
if not os.environ.get('DISPLAY') \
|
||||
and not os.environ.get('WAYLAND_DISPLAY'):
|
||||
import warnings
|
||||
global _already_warned
|
||||
if not _already_warned:
|
||||
_already_warned = True
|
||||
warnings.warn(
|
||||
'The DISPLAY or WAYLAND_DISPLAY environment variable is '
|
||||
'not set or empty and Qt5 requires this environment '
|
||||
'variable. Deactivate Qt5 code.'
|
||||
)
|
||||
return
|
||||
try:
|
||||
QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
except AttributeError: # Only for Qt>=5.6, <6.
|
||||
pass
|
||||
try:
|
||||
QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||
)
|
||||
except AttributeError: # Only for Qt>=5.14.
|
||||
pass
|
||||
_appref = app = QtGui.QApplication([" "])
|
||||
|
||||
# "reclaim" IPython sys.excepthook after event loop starts
|
||||
# without this, it defaults back to BaseIPythonApplication.excepthook
|
||||
# and exceptions in the Qt event loop are rendered without traceback
|
||||
# formatting and look like "bug in IPython".
|
||||
QtCore.QTimer.singleShot(0, _reclaim_excepthook)
|
||||
|
||||
event_loop = QtCore.QEventLoop(app)
|
||||
|
||||
if sys.platform == 'win32':
|
||||
# The QSocketNotifier method doesn't appear to work on Windows.
|
||||
# Use polling instead.
|
||||
timer = QtCore.QTimer()
|
||||
timer.timeout.connect(event_loop.quit)
|
||||
while not context.input_is_ready():
|
||||
# NOTE: run the event loop, and after 50 ms, call `quit` to exit it.
|
||||
timer.start(50) # 50 ms
|
||||
_exec(event_loop)
|
||||
timer.stop()
|
||||
else:
|
||||
# On POSIX platforms, we can use a file descriptor to quit the event
|
||||
# loop when there is input ready to read.
|
||||
notifier = QtCore.QSocketNotifier(
|
||||
context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read
|
||||
)
|
||||
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
|
||||
if not context.input_is_ready():
|
||||
_exec(event_loop)
|
||||
finally:
|
||||
notifier.setEnabled(False)
|
||||
|
||||
# This makes sure that the event loop is garbage collected.
|
||||
# See issue 14240.
|
||||
event_loop.setParent(None)
|
@@ -0,0 +1,90 @@
|
||||
# Code borrowed from ptpython
|
||||
# https://github.com/jonathanslenders/ptpython/blob/86b71a89626114b18898a0af463978bdb32eeb70/ptpython/eventloop.py
|
||||
|
||||
# Copyright (c) 2015, Jonathan Slenders
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright notice, this
|
||||
# list of conditions and the following disclaimer in the documentation and/or
|
||||
# other materials provided with the distribution.
|
||||
#
|
||||
# * Neither the name of the {organization} nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Wrapper around the eventloop that gives some time to the Tkinter GUI to process
|
||||
events when it's loaded and while we are waiting for input at the REPL. This
|
||||
way we don't block the UI of for instance ``turtle`` and other Tk libraries.
|
||||
|
||||
(Normally Tkinter registers it's callbacks in ``PyOS_InputHook`` to integrate
|
||||
in readline. ``prompt-toolkit`` doesn't understand that input hook, but this
|
||||
will fix it for Tk.)
|
||||
"""
|
||||
import time
|
||||
|
||||
import _tkinter
|
||||
import tkinter
|
||||
|
||||
def inputhook(inputhook_context):
|
||||
"""
|
||||
Inputhook for Tk.
|
||||
Run the Tk eventloop until prompt-toolkit needs to process the next input.
|
||||
"""
|
||||
# Get the current TK application.
|
||||
root = tkinter._default_root
|
||||
|
||||
def wait_using_filehandler():
|
||||
"""
|
||||
Run the TK eventloop until the file handler that we got from the
|
||||
inputhook becomes readable.
|
||||
"""
|
||||
# Add a handler that sets the stop flag when `prompt-toolkit` has input
|
||||
# to process.
|
||||
stop = [False]
|
||||
def done(*a):
|
||||
stop[0] = True
|
||||
|
||||
root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done)
|
||||
|
||||
# Run the TK event loop as long as we don't receive input.
|
||||
while root.dooneevent(_tkinter.ALL_EVENTS):
|
||||
if stop[0]:
|
||||
break
|
||||
|
||||
root.deletefilehandler(inputhook_context.fileno())
|
||||
|
||||
def wait_using_polling():
|
||||
"""
|
||||
Windows TK doesn't support 'createfilehandler'.
|
||||
So, run the TK eventloop and poll until input is ready.
|
||||
"""
|
||||
while not inputhook_context.input_is_ready():
|
||||
while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
|
||||
pass
|
||||
# Sleep to make the CPU idle, but not too long, so that the UI
|
||||
# stays responsive.
|
||||
time.sleep(.01)
|
||||
|
||||
if root is not None:
|
||||
if hasattr(root, 'createfilehandler'):
|
||||
wait_using_filehandler()
|
||||
else:
|
||||
wait_using_polling()
|
@@ -0,0 +1,219 @@
|
||||
"""Enable wxPython to be used interactively in prompt_toolkit
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import time
|
||||
from timeit import default_timer as clock
|
||||
import wx
|
||||
|
||||
|
||||
def ignore_keyboardinterrupts(func):
|
||||
"""Decorator which causes KeyboardInterrupt exceptions to be ignored during
|
||||
execution of the decorated function.
|
||||
|
||||
This is used by the inputhook functions to handle the event where the user
|
||||
presses CTRL+C while IPython is idle, and the inputhook loop is running. In
|
||||
this case, we want to ignore interrupts.
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return wrapper
|
||||
|
||||
|
||||
@ignore_keyboardinterrupts
|
||||
def inputhook_wx1(context):
|
||||
"""Run the wx event loop by processing pending events only.
|
||||
|
||||
This approach seems to work, but its performance is not great as it
|
||||
relies on having PyOS_InputHook called regularly.
|
||||
"""
|
||||
app = wx.GetApp()
|
||||
if app is not None:
|
||||
assert wx.Thread_IsMain()
|
||||
|
||||
# Make a temporary event loop and process system events until
|
||||
# there are no more waiting, then allow idle events (which
|
||||
# will also deal with pending or posted wx events.)
|
||||
evtloop = wx.EventLoop()
|
||||
ea = wx.EventLoopActivator(evtloop)
|
||||
while evtloop.Pending():
|
||||
evtloop.Dispatch()
|
||||
app.ProcessIdle()
|
||||
del ea
|
||||
return 0
|
||||
|
||||
|
||||
class EventLoopTimer(wx.Timer):
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
wx.Timer.__init__(self)
|
||||
|
||||
def Notify(self):
|
||||
self.func()
|
||||
|
||||
|
||||
class EventLoopRunner(object):
|
||||
|
||||
def Run(self, time, input_is_ready):
|
||||
self.input_is_ready = input_is_ready
|
||||
self.evtloop = wx.EventLoop()
|
||||
self.timer = EventLoopTimer(self.check_stdin)
|
||||
self.timer.Start(time)
|
||||
self.evtloop.Run()
|
||||
|
||||
def check_stdin(self):
|
||||
if self.input_is_ready():
|
||||
self.timer.Stop()
|
||||
self.evtloop.Exit()
|
||||
|
||||
|
||||
@ignore_keyboardinterrupts
|
||||
def inputhook_wx2(context):
|
||||
"""Run the wx event loop, polling for stdin.
|
||||
|
||||
This version runs the wx eventloop for an undetermined amount of time,
|
||||
during which it periodically checks to see if anything is ready on
|
||||
stdin. If anything is ready on stdin, the event loop exits.
|
||||
|
||||
The argument to elr.Run controls how often the event loop looks at stdin.
|
||||
This determines the responsiveness at the keyboard. A setting of 1000
|
||||
enables a user to type at most 1 char per second. I have found that a
|
||||
setting of 10 gives good keyboard response. We can shorten it further,
|
||||
but eventually performance would suffer from calling select/kbhit too
|
||||
often.
|
||||
"""
|
||||
app = wx.GetApp()
|
||||
if app is not None:
|
||||
assert wx.Thread_IsMain()
|
||||
elr = EventLoopRunner()
|
||||
# As this time is made shorter, keyboard response improves, but idle
|
||||
# CPU load goes up. 10 ms seems like a good compromise.
|
||||
elr.Run(time=10, # CHANGE time here to control polling interval
|
||||
input_is_ready=context.input_is_ready)
|
||||
return 0
|
||||
|
||||
|
||||
@ignore_keyboardinterrupts
|
||||
def inputhook_wx3(context):
|
||||
"""Run the wx event loop by processing pending events only.
|
||||
|
||||
This is like inputhook_wx1, but it keeps processing pending events
|
||||
until stdin is ready. After processing all pending events, a call to
|
||||
time.sleep is inserted. This is needed, otherwise, CPU usage is at 100%.
|
||||
This sleep time should be tuned though for best performance.
|
||||
"""
|
||||
app = wx.GetApp()
|
||||
if app is not None:
|
||||
assert wx.Thread_IsMain()
|
||||
|
||||
# The import of wx on Linux sets the handler for signal.SIGINT
|
||||
# to 0. This is a bug in wx or gtk. We fix by just setting it
|
||||
# back to the Python default.
|
||||
if not callable(signal.getsignal(signal.SIGINT)):
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
|
||||
evtloop = wx.EventLoop()
|
||||
ea = wx.EventLoopActivator(evtloop)
|
||||
t = clock()
|
||||
while not context.input_is_ready():
|
||||
while evtloop.Pending():
|
||||
t = clock()
|
||||
evtloop.Dispatch()
|
||||
app.ProcessIdle()
|
||||
# We need to sleep at this point to keep the idle CPU load
|
||||
# low. However, if sleep to long, GUI response is poor. As
|
||||
# a compromise, we watch how often GUI events are being processed
|
||||
# and switch between a short and long sleep time. Here are some
|
||||
# stats useful in helping to tune this.
|
||||
# time CPU load
|
||||
# 0.001 13%
|
||||
# 0.005 3%
|
||||
# 0.01 1.5%
|
||||
# 0.05 0.5%
|
||||
used_time = clock() - t
|
||||
if used_time > 10.0:
|
||||
# print('Sleep for 1 s') # dbg
|
||||
time.sleep(1.0)
|
||||
elif used_time > 0.1:
|
||||
# Few GUI events coming in, so we can sleep longer
|
||||
# print('Sleep for 0.05 s') # dbg
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
# Many GUI events coming in, so sleep only very little
|
||||
time.sleep(0.001)
|
||||
del ea
|
||||
return 0
|
||||
|
||||
|
||||
@ignore_keyboardinterrupts
|
||||
def inputhook_wxphoenix(context):
|
||||
"""Run the wx event loop until the user provides more input.
|
||||
|
||||
This input hook is suitable for use with wxPython >= 4 (a.k.a. Phoenix).
|
||||
|
||||
It uses the same approach to that used in
|
||||
ipykernel.eventloops.loop_wx. The wx.MainLoop is executed, and a wx.Timer
|
||||
is used to periodically poll the context for input. As soon as input is
|
||||
ready, the wx.MainLoop is stopped.
|
||||
"""
|
||||
|
||||
app = wx.GetApp()
|
||||
|
||||
if app is None:
|
||||
return
|
||||
|
||||
if context.input_is_ready():
|
||||
return
|
||||
|
||||
assert wx.IsMainThread()
|
||||
|
||||
# Wx uses milliseconds
|
||||
poll_interval = 100
|
||||
|
||||
# Use a wx.Timer to periodically check whether input is ready - as soon as
|
||||
# it is, we exit the main loop
|
||||
timer = wx.Timer()
|
||||
|
||||
def poll(ev):
|
||||
if context.input_is_ready():
|
||||
timer.Stop()
|
||||
app.ExitMainLoop()
|
||||
|
||||
timer.Start(poll_interval)
|
||||
timer.Bind(wx.EVT_TIMER, poll)
|
||||
|
||||
# The import of wx on Linux sets the handler for signal.SIGINT to 0. This
|
||||
# is a bug in wx or gtk. We fix by just setting it back to the Python
|
||||
# default.
|
||||
if not callable(signal.getsignal(signal.SIGINT)):
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
|
||||
# The SetExitOnFrameDelete call allows us to run the wx mainloop without
|
||||
# having a frame open.
|
||||
app.SetExitOnFrameDelete(False)
|
||||
app.MainLoop()
|
||||
|
||||
|
||||
# Get the major wx version number to figure out what input hook we should use.
|
||||
major_version = 3
|
||||
|
||||
try:
|
||||
major_version = int(wx.__version__[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Use the phoenix hook on all platforms for wxpython >= 4
|
||||
if major_version >= 4:
|
||||
inputhook = inputhook_wxphoenix
|
||||
# On OSX, evtloop.Pending() always returns True, regardless of there being
|
||||
# any events pending. As such we can't use implementations 1 or 3 of the
|
||||
# inputhook as those depend on a pending/dispatch loop.
|
||||
elif sys.platform == 'darwin':
|
||||
inputhook = inputhook_wx2
|
||||
else:
|
||||
inputhook = inputhook_wx3
|
204
.venv/lib/python3.12/site-packages/IPython/terminal/ptutils.py
Normal file
204
.venv/lib/python3.12/site-packages/IPython/terminal/ptutils.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""prompt-toolkit utilities
|
||||
|
||||
Everything in this module is a private API,
|
||||
not to be used outside IPython.
|
||||
"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import unicodedata
|
||||
from wcwidth import wcwidth
|
||||
|
||||
from IPython.core.completer import (
|
||||
provisionalcompleter, cursor_to_position,
|
||||
_deduplicate_completions)
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.lexers import Lexer
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
|
||||
import pygments.lexers as pygments_lexers
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
_completion_sentinel = object()
|
||||
|
||||
def _elide_point(string:str, *, min_elide=30)->str:
|
||||
"""
|
||||
If a string is long enough, and has at least 3 dots,
|
||||
replace the middle part with ellipses.
|
||||
|
||||
If a string naming a file is long enough, and has at least 3 slashes,
|
||||
replace the middle part with ellipses.
|
||||
|
||||
If three consecutive dots, or two consecutive dots are encountered these are
|
||||
replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode
|
||||
equivalents
|
||||
"""
|
||||
string = string.replace('...','\N{HORIZONTAL ELLIPSIS}')
|
||||
string = string.replace('..','\N{TWO DOT LEADER}')
|
||||
if len(string) < min_elide:
|
||||
return string
|
||||
|
||||
object_parts = string.split('.')
|
||||
file_parts = string.split(os.sep)
|
||||
if file_parts[-1] == '':
|
||||
file_parts.pop()
|
||||
|
||||
if len(object_parts) > 3:
|
||||
return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format(
|
||||
object_parts[0],
|
||||
object_parts[1][:1],
|
||||
object_parts[-2][-1:],
|
||||
object_parts[-1],
|
||||
)
|
||||
|
||||
elif len(file_parts) > 3:
|
||||
return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format(
|
||||
file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1]
|
||||
)
|
||||
|
||||
return string
|
||||
|
||||
def _elide_typed(string:str, typed:str, *, min_elide:int=30)->str:
|
||||
"""
|
||||
Elide the middle of a long string if the beginning has already been typed.
|
||||
"""
|
||||
|
||||
if len(string) < min_elide:
|
||||
return string
|
||||
cut_how_much = len(typed)-3
|
||||
if cut_how_much < 7:
|
||||
return string
|
||||
if string.startswith(typed) and len(string)> len(typed):
|
||||
return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}"
|
||||
return string
|
||||
|
||||
def _elide(string:str, typed:str, min_elide=30)->str:
|
||||
return _elide_typed(
|
||||
_elide_point(string, min_elide=min_elide),
|
||||
typed, min_elide=min_elide)
|
||||
|
||||
|
||||
|
||||
def _adjust_completion_text_based_on_context(text, body, offset):
|
||||
if text.endswith('=') and len(body) > offset and body[offset] == '=':
|
||||
return text[:-1]
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
class IPythonPTCompleter(Completer):
|
||||
"""Adaptor to provide IPython completions to prompt_toolkit"""
|
||||
def __init__(self, ipy_completer=None, shell=None):
|
||||
if shell is None and ipy_completer is None:
|
||||
raise TypeError("Please pass shell=an InteractiveShell instance.")
|
||||
self._ipy_completer = ipy_completer
|
||||
self.shell = shell
|
||||
|
||||
@property
|
||||
def ipy_completer(self):
|
||||
if self._ipy_completer:
|
||||
return self._ipy_completer
|
||||
else:
|
||||
return self.shell.Completer
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
if not document.current_line.strip():
|
||||
return
|
||||
# Some bits of our completion system may print stuff (e.g. if a module
|
||||
# is imported). This context manager ensures that doesn't interfere with
|
||||
# the prompt.
|
||||
|
||||
with patch_stdout(), provisionalcompleter():
|
||||
body = document.text
|
||||
cursor_row = document.cursor_position_row
|
||||
cursor_col = document.cursor_position_col
|
||||
cursor_position = document.cursor_position
|
||||
offset = cursor_to_position(body, cursor_row, cursor_col)
|
||||
try:
|
||||
yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
|
||||
except Exception as e:
|
||||
try:
|
||||
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||
traceback.print_exception(exc_type, exc_value, exc_tb)
|
||||
except AttributeError:
|
||||
print('Unrecoverable Error in completions')
|
||||
|
||||
@staticmethod
|
||||
def _get_completions(body, offset, cursor_position, ipyc):
|
||||
"""
|
||||
Private equivalent of get_completions() use only for unit_testing.
|
||||
"""
|
||||
debug = getattr(ipyc, 'debug', False)
|
||||
completions = _deduplicate_completions(
|
||||
body, ipyc.completions(body, offset))
|
||||
for c in completions:
|
||||
if not c.text:
|
||||
# Guard against completion machinery giving us an empty string.
|
||||
continue
|
||||
text = unicodedata.normalize('NFC', c.text)
|
||||
# When the first character of the completion has a zero length,
|
||||
# then it's probably a decomposed unicode character. E.g. caused by
|
||||
# the "\dot" completion. Try to compose again with the previous
|
||||
# character.
|
||||
if wcwidth(text[0]) == 0:
|
||||
if cursor_position + c.start > 0:
|
||||
char_before = body[c.start - 1]
|
||||
fixed_text = unicodedata.normalize(
|
||||
'NFC', char_before + text)
|
||||
|
||||
# Yield the modified completion instead, if this worked.
|
||||
if wcwidth(text[0:1]) == 1:
|
||||
yield Completion(fixed_text, start_position=c.start - offset - 1)
|
||||
continue
|
||||
|
||||
# TODO: Use Jedi to determine meta_text
|
||||
# (Jedi currently has a bug that results in incorrect information.)
|
||||
# meta_text = ''
|
||||
# yield Completion(m, start_position=start_pos,
|
||||
# display_meta=meta_text)
|
||||
display_text = c.text
|
||||
|
||||
adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
|
||||
if c.type == 'function':
|
||||
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()', body[c.start:c.end]), display_meta=c.type+c.signature)
|
||||
else:
|
||||
yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text, body[c.start:c.end]), display_meta=c.type)
|
||||
|
||||
class IPythonPTLexer(Lexer):
|
||||
"""
|
||||
Wrapper around PythonLexer and BashLexer.
|
||||
"""
|
||||
def __init__(self):
|
||||
l = pygments_lexers
|
||||
self.python_lexer = PygmentsLexer(l.Python3Lexer)
|
||||
self.shell_lexer = PygmentsLexer(l.BashLexer)
|
||||
|
||||
self.magic_lexers = {
|
||||
'HTML': PygmentsLexer(l.HtmlLexer),
|
||||
'html': PygmentsLexer(l.HtmlLexer),
|
||||
'javascript': PygmentsLexer(l.JavascriptLexer),
|
||||
'js': PygmentsLexer(l.JavascriptLexer),
|
||||
'perl': PygmentsLexer(l.PerlLexer),
|
||||
'ruby': PygmentsLexer(l.RubyLexer),
|
||||
'latex': PygmentsLexer(l.TexLexer),
|
||||
}
|
||||
|
||||
def lex_document(self, document):
|
||||
text = document.text.lstrip()
|
||||
|
||||
lexer = self.python_lexer
|
||||
|
||||
if text.startswith('!') or text.startswith('%%bash'):
|
||||
lexer = self.shell_lexer
|
||||
|
||||
elif text.startswith('%%'):
|
||||
for magic, l in self.magic_lexers.items():
|
||||
if text.startswith('%%' + magic):
|
||||
lexer = l
|
||||
break
|
||||
|
||||
return lexer.lex_document(document)
|
@@ -0,0 +1,638 @@
|
||||
"""
|
||||
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,
|
||||
]
|
||||
|
||||
UNASSIGNED_ALLOWED_COMMANDS = [
|
||||
nc.beginning_of_buffer,
|
||||
nc.end_of_buffer,
|
||||
nc.end_of_line,
|
||||
nc.forward_word,
|
||||
nc.unix_line_discard,
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
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,
|
||||
}
|
@@ -0,0 +1,648 @@
|
||||
import re
|
||||
import asyncio
|
||||
import tokenize
|
||||
from io import StringIO
|
||||
from typing import Callable, List, Optional, Union, Generator, Tuple, ClassVar, Any
|
||||
import warnings
|
||||
|
||||
import prompt_toolkit
|
||||
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, AutoSuggest
|
||||
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
|
||||
|
||||
try:
|
||||
import jupyter_ai_magics
|
||||
import jupyter_ai.completions.models as jai_models
|
||||
except ModuleNotFoundError:
|
||||
jai_models = None
|
||||
|
||||
|
||||
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).
|
||||
|
||||
This has a private `_debug` attribute that can be set to True to display
|
||||
debug information as virtual suggestion on the end of any line. You can do
|
||||
so with:
|
||||
|
||||
>>> from IPython.terminal.shortcuts.auto_suggest import AppendAutoSuggestionInAnyLine
|
||||
>>> AppendAutoSuggestionInAnyLine._debug = True
|
||||
|
||||
"""
|
||||
|
||||
_debug: ClassVar[bool] = False
|
||||
|
||||
def __init__(self, style: str = "class:auto-suggestion") -> None:
|
||||
self.style = style
|
||||
|
||||
def apply_transformation(self, ti: TransformationInput) -> Transformation:
|
||||
"""
|
||||
Apply transformation to the line that is currently being edited.
|
||||
|
||||
This is a variation of the original implementation in prompt toolkit
|
||||
that allows to not only append suggestions to any line, but also to show
|
||||
multi-line suggestions.
|
||||
|
||||
As transformation are applied on a line-by-line basis; we need to trick
|
||||
a bit, and elide any line that is after the line we are currently
|
||||
editing, until we run out of completions. We cannot shift the existing
|
||||
lines
|
||||
|
||||
There are multiple cases to handle:
|
||||
|
||||
The completions ends before the end of the buffer:
|
||||
We can resume showing the normal line, and say that some code may
|
||||
be hidden.
|
||||
|
||||
The completions ends at the end of the buffer
|
||||
We can just say that some code may be hidden.
|
||||
|
||||
And separately:
|
||||
|
||||
The completions ends beyond the end of the buffer
|
||||
We need to both say that some code may be hidden, and that some
|
||||
lines are not shown.
|
||||
|
||||
"""
|
||||
last_line_number = ti.document.line_count - 1
|
||||
is_last_line = ti.lineno == last_line_number
|
||||
|
||||
noop = lambda text: Transformation(
|
||||
fragments=ti.fragments + [(self.style, " " + text if self._debug else "")]
|
||||
)
|
||||
if ti.document.line_count == 1:
|
||||
return noop("noop:oneline")
|
||||
if ti.document.cursor_position_row == last_line_number and is_last_line:
|
||||
# prompt toolkit already appends something; just leave it be
|
||||
return noop("noop:last line and cursor")
|
||||
|
||||
# first everything before the current line is unchanged.
|
||||
if ti.lineno < ti.document.cursor_position_row:
|
||||
return noop("noop:before cursor")
|
||||
|
||||
buffer = ti.buffer_control.buffer
|
||||
if not buffer.suggestion or not ti.document.is_cursor_at_the_end_of_line:
|
||||
return noop("noop:not eol")
|
||||
|
||||
delta = ti.lineno - ti.document.cursor_position_row
|
||||
suggestions = buffer.suggestion.text.splitlines()
|
||||
|
||||
if len(suggestions) == 0:
|
||||
return noop("noop: no suggestions")
|
||||
|
||||
suggestions_longer_than_buffer: bool = (
|
||||
len(suggestions) + ti.document.cursor_position_row > ti.document.line_count
|
||||
)
|
||||
|
||||
if len(suggestions) >= 1 and prompt_toolkit.VERSION < (3, 0, 49):
|
||||
if ti.lineno == ti.document.cursor_position_row:
|
||||
return Transformation(
|
||||
fragments=ti.fragments
|
||||
+ [
|
||||
(
|
||||
"red",
|
||||
"(Cannot show multiline suggestion; requires prompt_toolkit > 3.0.49)",
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
return Transformation(fragments=ti.fragments)
|
||||
if delta == 0:
|
||||
suggestion = suggestions[0]
|
||||
return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
|
||||
if is_last_line:
|
||||
if delta < len(suggestions):
|
||||
extra = f"; {len(suggestions) - delta} line(s) hidden"
|
||||
suggestion = f"… rest of suggestion ({len(suggestions) - delta} lines) and code hidden"
|
||||
return Transformation([(self.style, suggestion)])
|
||||
|
||||
n_elided = len(suggestions)
|
||||
for i in range(len(suggestions)):
|
||||
ll = ti.get_line(last_line_number - i)
|
||||
el = "".join(l[1] for l in ll).strip()
|
||||
if el:
|
||||
break
|
||||
else:
|
||||
n_elided -= 1
|
||||
if n_elided:
|
||||
return Transformation([(self.style, f"… {n_elided} line(s) hidden")])
|
||||
else:
|
||||
return Transformation(
|
||||
ti.get_line(last_line_number - len(suggestions) + 1)
|
||||
+ ([(self.style, "shift-last-line")] if self._debug else [])
|
||||
)
|
||||
|
||||
elif delta < len(suggestions):
|
||||
suggestion = suggestions[delta]
|
||||
return Transformation([(self.style, suggestion)])
|
||||
else:
|
||||
shift = ti.lineno - len(suggestions) + 1
|
||||
return Transformation(ti.get_line(shift))
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
skip_lines: int
|
||||
_connected_apps: list[PromptSession]
|
||||
|
||||
# handle to the currently running llm task that appends suggestions to the
|
||||
# current buffer; we keep a handle to it in order to cancell it when there is a cursor movement, or
|
||||
# another request.
|
||||
_llm_task: asyncio.Task | None = None
|
||||
|
||||
# This is the instance of the LLM provider from jupyter-ai to which we forward the request
|
||||
# to generate inline completions.
|
||||
_llm_provider: Any | None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.skip_lines = 0
|
||||
self._connected_apps = []
|
||||
self._llm_provider = None
|
||||
|
||||
def reset_history_position(self, _: Buffer):
|
||||
self.skip_lines = 0
|
||||
|
||||
def disconnect(self) -> None:
|
||||
self._cancel_running_llm_task()
|
||||
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. resetting 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) -> None:
|
||||
self._cancel_running_llm_task()
|
||||
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:
|
||||
self._cancel_running_llm_task()
|
||||
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:
|
||||
self._cancel_running_llm_task()
|
||||
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 _cancel_running_llm_task(self) -> None:
|
||||
"""
|
||||
Try to cancell the currently running llm_task if exists, and set it to None.
|
||||
"""
|
||||
if self._llm_task is not None:
|
||||
if self._llm_task.done():
|
||||
self._llm_task = None
|
||||
return
|
||||
cancelled = self._llm_task.cancel()
|
||||
if cancelled:
|
||||
self._llm_task = None
|
||||
if not cancelled:
|
||||
warnings.warn(
|
||||
"LLM task not cancelled, does your provider support cancellation?"
|
||||
)
|
||||
|
||||
async def _trigger_llm(self, buffer) -> None:
|
||||
"""
|
||||
This will ask the current llm provider a suggestion for the current buffer.
|
||||
|
||||
If there is a currently running llm task, it will cancel it.
|
||||
"""
|
||||
# we likely want to store the current cursor position, and cancel if the cursor has moved.
|
||||
if not self._llm_provider:
|
||||
warnings.warn("No LLM provider found, cannot trigger LLM completions")
|
||||
return
|
||||
if jai_models is None:
|
||||
warnings.warn(
|
||||
"LLM Completion requires `jupyter_ai_magics` and `jupyter_ai` to be installed"
|
||||
)
|
||||
|
||||
self._cancel_running_llm_task()
|
||||
|
||||
async def error_catcher(buffer):
|
||||
"""
|
||||
This catches and log any errors, as otherwise this is just
|
||||
lost in the void of the future running task.
|
||||
"""
|
||||
try:
|
||||
await self._trigger_llm_core(buffer)
|
||||
except Exception as e:
|
||||
get_ipython().log.error("error")
|
||||
raise
|
||||
|
||||
# here we need a cancellable task so we can't just await the error catched
|
||||
self._llm_task = asyncio.create_task(error_catcher(buffer))
|
||||
await self._llm_task
|
||||
|
||||
async def _trigger_llm_core(self, buffer: Buffer):
|
||||
"""
|
||||
This is the core of the current llm request.
|
||||
|
||||
Here we build a compatible `InlineCompletionRequest` and ask the llm
|
||||
provider to stream it's response back to us iteratively setting it as
|
||||
the suggestion on the current buffer.
|
||||
|
||||
Unlike with JupyterAi, as we do not have multiple cell, the cell number
|
||||
is always set to `0`, note that we _could_ set it to a new number each
|
||||
time and ignore threply from past numbers.
|
||||
|
||||
We set the prefix to the current cell content, but could also inset the
|
||||
rest of the history or even just the non-fail history.
|
||||
|
||||
In the same way, we do not have cell id.
|
||||
|
||||
LLM provider may return multiple suggestion stream, but for the time
|
||||
being we only support one.
|
||||
|
||||
Here we make the assumption that the provider will have
|
||||
stream_inline_completions, I'm not sure it is the case for all
|
||||
providers.
|
||||
"""
|
||||
|
||||
request = jai_models.InlineCompletionRequest(
|
||||
number=0,
|
||||
prefix=buffer.document.text,
|
||||
suffix="",
|
||||
mime="text/x-python",
|
||||
stream=True,
|
||||
path=None,
|
||||
language="python",
|
||||
cell_id=None,
|
||||
)
|
||||
|
||||
async for reply_and_chunks in self._llm_provider.stream_inline_completions(
|
||||
request
|
||||
):
|
||||
if isinstance(reply_and_chunks, jai_models.InlineCompletionReply):
|
||||
if len(reply_and_chunks.list.items) > 1:
|
||||
raise ValueError(
|
||||
"Terminal IPython cannot deal with multiple LLM suggestions at once"
|
||||
)
|
||||
buffer.suggestion = Suggestion(
|
||||
reply_and_chunks.list.items[0].insertText
|
||||
)
|
||||
buffer.on_suggestion_set.fire()
|
||||
elif isinstance(reply_and_chunks, jai_models.InlineCompletionStreamChunk):
|
||||
buffer.suggestion = Suggestion(reply_and_chunks.response.insertText)
|
||||
buffer.on_suggestion_set.fire()
|
||||
return
|
||||
|
||||
|
||||
_MIN_LINES = 5
|
||||
|
||||
|
||||
async def llm_autosuggestion(event: KeyPressEvent):
|
||||
"""
|
||||
Ask the AutoSuggester from history to delegate to ask an LLM for completion
|
||||
|
||||
This will first make sure that the current buffer have _MIN_LINES (7)
|
||||
available lines to insert the LLM completion
|
||||
|
||||
Provisional as of 8.32, may change without warnigns
|
||||
|
||||
"""
|
||||
provider = get_ipython().auto_suggest
|
||||
if not isinstance(provider, NavigableAutoSuggestFromHistory):
|
||||
return
|
||||
doc = event.current_buffer.document
|
||||
lines_to_insert = max(0, _MIN_LINES - doc.line_count + doc.cursor_position_row)
|
||||
for _ in range(lines_to_insert):
|
||||
event.current_buffer.insert_text("\n", move_cursor=False)
|
||||
|
||||
await provider._trigger_llm(event.current_buffer)
|
||||
|
||||
|
||||
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
|
@@ -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 beforehand 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"]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,82 @@
|
||||
"""Test embedding of IPython"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (C) 2013 The IPython Development Team
|
||||
#
|
||||
# Distributed under the terms of the BSD License. The full license is in
|
||||
# the file COPYING, distributed as part of this software.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import sys
|
||||
from IPython.testing.decorators import skip_win32
|
||||
from IPython.testing import IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Tests
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@skip_win32
|
||||
def test_debug_magic_passes_through_generators():
|
||||
"""
|
||||
This test that we can correctly pass through frames of a generator post-mortem.
|
||||
"""
|
||||
import pexpect
|
||||
import re
|
||||
in_prompt = re.compile(br'In ?\[\d+\]:')
|
||||
ipdb_prompt = 'ipdb>'
|
||||
env = os.environ.copy()
|
||||
child = pexpect.spawn(sys.executable, ['-m', 'IPython', '--colors=nocolor', '--simple-prompt'],
|
||||
env=env)
|
||||
child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
|
||||
child.expect(in_prompt)
|
||||
|
||||
child.timeout = 2 * IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
|
||||
child.sendline("def f(x):")
|
||||
child.sendline(" raise Exception")
|
||||
child.sendline("")
|
||||
|
||||
child.expect(in_prompt)
|
||||
child.sendline("gen = (f(x) for x in [0])")
|
||||
child.sendline("")
|
||||
|
||||
child.expect(in_prompt)
|
||||
child.sendline("for x in gen:")
|
||||
child.sendline(" pass")
|
||||
child.sendline("")
|
||||
|
||||
child.timeout = 10 * IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
|
||||
child.expect('Exception:')
|
||||
|
||||
child.expect(in_prompt)
|
||||
child.sendline(r'%debug')
|
||||
child.expect('----> 2 raise Exception')
|
||||
|
||||
child.expect(ipdb_prompt)
|
||||
child.sendline('u')
|
||||
child.expect_exact(r'----> 1 gen = (f(x) for x in [0])')
|
||||
|
||||
child.expect(ipdb_prompt)
|
||||
child.sendline('u')
|
||||
child.expect_exact('----> 1 for x in gen:')
|
||||
|
||||
child.expect(ipdb_prompt)
|
||||
child.sendline("u")
|
||||
child.expect_exact(
|
||||
"*** all frames above hidden, use `skip_hidden False` to get get into those."
|
||||
)
|
||||
|
||||
child.expect(ipdb_prompt)
|
||||
child.sendline('exit')
|
||||
|
||||
child.expect(in_prompt)
|
||||
child.sendline('exit')
|
||||
|
||||
child.close()
|
@@ -0,0 +1,138 @@
|
||||
"""Test embedding of IPython"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (C) 2013 The IPython Development Team
|
||||
#
|
||||
# Distributed under the terms of the BSD License. The full license is in
|
||||
# the file COPYING, distributed as part of this software.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from IPython.utils.tempdir import NamedFileInTemporaryDirectory
|
||||
from IPython.testing.decorators import skip_win32
|
||||
from IPython.testing import IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Tests
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
_sample_embed = b"""
|
||||
import IPython
|
||||
|
||||
a = 3
|
||||
b = 14
|
||||
print(a, '.', b)
|
||||
|
||||
IPython.embed()
|
||||
|
||||
print('bye!')
|
||||
"""
|
||||
|
||||
_exit = b"exit\r"
|
||||
|
||||
def test_ipython_embed():
|
||||
"""test that `IPython.embed()` works"""
|
||||
with NamedFileInTemporaryDirectory('file_with_embed.py') as f:
|
||||
f.write(_sample_embed)
|
||||
f.flush()
|
||||
f.close() # otherwise msft won't be able to read the file
|
||||
|
||||
# run `python file_with_embed.py`
|
||||
cmd = [sys.executable, f.name]
|
||||
env = os.environ.copy()
|
||||
env['IPY_TEST_SIMPLE_PROMPT'] = '1'
|
||||
|
||||
p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate(_exit)
|
||||
std = out.decode('UTF-8')
|
||||
|
||||
assert p.returncode == 0
|
||||
assert "3 . 14" in std
|
||||
if os.name != "nt":
|
||||
# TODO: Fix up our different stdout references, see issue gh-14
|
||||
assert "IPython" in std
|
||||
assert "bye!" in std
|
||||
|
||||
|
||||
@skip_win32
|
||||
def test_nest_embed():
|
||||
"""test that `IPython.embed()` is nestable"""
|
||||
import pexpect
|
||||
ipy_prompt = r']:' #ansi color codes give problems matching beyond this
|
||||
env = os.environ.copy()
|
||||
env['IPY_TEST_SIMPLE_PROMPT'] = '1'
|
||||
|
||||
|
||||
child = pexpect.spawn(sys.executable, ['-m', 'IPython', '--colors=nocolor'],
|
||||
env=env)
|
||||
child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
child.expect(ipy_prompt)
|
||||
child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE
|
||||
child.sendline("import IPython")
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("ip0 = get_ipython()")
|
||||
#enter first nested embed
|
||||
child.sendline("IPython.embed()")
|
||||
#skip the banner until we get to a prompt
|
||||
try:
|
||||
prompted = -1
|
||||
while prompted != 0:
|
||||
prompted = child.expect([ipy_prompt, '\r\n'])
|
||||
except pexpect.TIMEOUT as e:
|
||||
print(e)
|
||||
#child.interact()
|
||||
child.sendline("embed1 = get_ipython()")
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if embed1 is not ip0 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if IPython.get_ipython() is embed1 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
#enter second nested embed
|
||||
child.sendline("IPython.embed()")
|
||||
#skip the banner until we get to a prompt
|
||||
try:
|
||||
prompted = -1
|
||||
while prompted != 0:
|
||||
prompted = child.expect([ipy_prompt, '\r\n'])
|
||||
except pexpect.TIMEOUT as e:
|
||||
print(e)
|
||||
#child.interact()
|
||||
child.sendline("embed2 = get_ipython()")
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if embed2 is not embed1 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if embed2 is IPython.get_ipython() else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline('exit')
|
||||
#back at first embed
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if get_ipython() is embed1 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if IPython.get_ipython() is embed1 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline('exit')
|
||||
#back at launching scope
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if get_ipython() is ip0 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline("print('true' if IPython.get_ipython() is ip0 else 'false')")
|
||||
assert(child.expect(['true\r\n', 'false\r\n']) == 0)
|
||||
child.expect(ipy_prompt)
|
||||
child.sendline('exit')
|
||||
child.close()
|
@@ -0,0 +1,30 @@
|
||||
"""Test help output of various IPython entry points"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import pytest
|
||||
import IPython.testing.tools as tt
|
||||
|
||||
|
||||
def test_ipython_help():
|
||||
tt.help_all_output_test()
|
||||
|
||||
def test_profile_help():
|
||||
tt.help_all_output_test("profile")
|
||||
|
||||
def test_profile_list_help():
|
||||
tt.help_all_output_test("profile list")
|
||||
|
||||
def test_profile_create_help():
|
||||
tt.help_all_output_test("profile create")
|
||||
|
||||
def test_locate_help():
|
||||
tt.help_all_output_test("locate")
|
||||
|
||||
def test_locate_profile_help():
|
||||
tt.help_all_output_test("locate profile")
|
||||
|
||||
def test_trust_help():
|
||||
pytest.importorskip("nbformat")
|
||||
tt.help_all_output_test("trust")
|
@@ -0,0 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the TerminalInteractiveShell and related pieces."""
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
|
||||
|
||||
from IPython.testing import tools as tt
|
||||
|
||||
from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
|
||||
from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
|
||||
|
||||
|
||||
class TestAutoSuggest(unittest.TestCase):
|
||||
def test_changing_provider(self):
|
||||
ip = get_ipython()
|
||||
ip.autosuggestions_provider = None
|
||||
self.assertEqual(ip.auto_suggest, None)
|
||||
ip.autosuggestions_provider = "AutoSuggestFromHistory"
|
||||
self.assertIsInstance(ip.auto_suggest, AutoSuggestFromHistory)
|
||||
ip.autosuggestions_provider = "NavigableAutoSuggestFromHistory"
|
||||
self.assertIsInstance(ip.auto_suggest, NavigableAutoSuggestFromHistory)
|
||||
|
||||
|
||||
class TestElide(unittest.TestCase):
|
||||
def test_elide(self):
|
||||
_elide("concatenate((a1, a2, ...), axis", "") # do not raise
|
||||
_elide("concatenate((a1, a2, ..), . axis", "") # do not raise
|
||||
self.assertEqual(
|
||||
_elide("aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh", ""),
|
||||
"aaaa.b…g.hhhhhh",
|
||||
)
|
||||
|
||||
test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""])
|
||||
expect_string = (
|
||||
os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c"
|
||||
)
|
||||
self.assertEqual(_elide(test_string, ""), expect_string)
|
||||
|
||||
def test_elide_typed_normal(self):
|
||||
self.assertEqual(
|
||||
_elide(
|
||||
"the quick brown fox jumped over the lazy dog",
|
||||
"the quick brown fox",
|
||||
min_elide=10,
|
||||
),
|
||||
"the…fox jumped over the lazy dog",
|
||||
)
|
||||
|
||||
def test_elide_typed_short_match(self):
|
||||
"""
|
||||
if the match is too short we don't elide.
|
||||
avoid the "the...the"
|
||||
"""
|
||||
self.assertEqual(
|
||||
_elide("the quick brown fox jumped over the lazy dog", "the", min_elide=10),
|
||||
"the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
|
||||
def test_elide_typed_no_match(self):
|
||||
"""
|
||||
if the match is too short we don't elide.
|
||||
avoid the "the...the"
|
||||
"""
|
||||
# here we typed red instead of brown
|
||||
self.assertEqual(
|
||||
_elide(
|
||||
"the quick brown fox jumped over the lazy dog",
|
||||
"the quick red fox",
|
||||
min_elide=10,
|
||||
),
|
||||
"the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
|
||||
|
||||
class TestContextAwareCompletion(unittest.TestCase):
|
||||
def test_adjust_completion_text_based_on_context(self):
|
||||
# Adjusted case
|
||||
self.assertEqual(
|
||||
_adjust_completion_text_based_on_context("arg1=", "func1(a=)", 7), "arg1"
|
||||
)
|
||||
|
||||
# Untouched cases
|
||||
self.assertEqual(
|
||||
_adjust_completion_text_based_on_context("arg1=", "func1(a)", 7), "arg1="
|
||||
)
|
||||
self.assertEqual(
|
||||
_adjust_completion_text_based_on_context("arg1=", "func1(a", 7), "arg1="
|
||||
)
|
||||
self.assertEqual(
|
||||
_adjust_completion_text_based_on_context("%magic", "func1(a=)", 7), "%magic"
|
||||
)
|
||||
self.assertEqual(
|
||||
_adjust_completion_text_based_on_context("func2", "func1(a=)", 7), "func2"
|
||||
)
|
||||
|
||||
|
||||
# Decorator for interaction loop tests -----------------------------------------
|
||||
|
||||
|
||||
class mock_input_helper(object):
|
||||
"""Machinery for tests of the main interact loop.
|
||||
|
||||
Used by the mock_input decorator.
|
||||
"""
|
||||
def __init__(self, testgen):
|
||||
self.testgen = testgen
|
||||
self.exception = None
|
||||
self.ip = get_ipython()
|
||||
|
||||
def __enter__(self):
|
||||
self.orig_prompt_for_code = self.ip.prompt_for_code
|
||||
self.ip.prompt_for_code = self.fake_input
|
||||
return self
|
||||
|
||||
def __exit__(self, etype, value, tb):
|
||||
self.ip.prompt_for_code = self.orig_prompt_for_code
|
||||
|
||||
def fake_input(self):
|
||||
try:
|
||||
return next(self.testgen)
|
||||
except StopIteration:
|
||||
self.ip.keep_running = False
|
||||
return u''
|
||||
except:
|
||||
self.exception = sys.exc_info()
|
||||
self.ip.keep_running = False
|
||||
return u''
|
||||
|
||||
def mock_input(testfunc):
|
||||
"""Decorator for tests of the main interact loop.
|
||||
|
||||
Write the test as a generator, yield-ing the input strings, which IPython
|
||||
will see as if they were typed in at the prompt.
|
||||
"""
|
||||
def test_method(self):
|
||||
testgen = testfunc(self)
|
||||
with mock_input_helper(testgen) as mih:
|
||||
mih.ip.interact()
|
||||
|
||||
if mih.exception is not None:
|
||||
# Re-raise captured exception
|
||||
etype, value, tb = mih.exception
|
||||
import traceback
|
||||
traceback.print_tb(tb, file=sys.stdout)
|
||||
del tb # Avoid reference loop
|
||||
raise value
|
||||
|
||||
return test_method
|
||||
|
||||
# Test classes -----------------------------------------------------------------
|
||||
|
||||
class InteractiveShellTestCase(unittest.TestCase):
|
||||
def rl_hist_entries(self, rl, n):
|
||||
"""Get last n readline history entries as a list"""
|
||||
return [rl.get_history_item(rl.get_current_history_length() - x)
|
||||
for x in range(n - 1, -1, -1)]
|
||||
|
||||
@mock_input
|
||||
def test_inputtransformer_syntaxerror(self):
|
||||
ip = get_ipython()
|
||||
ip.input_transformers_post.append(syntax_error_transformer)
|
||||
|
||||
try:
|
||||
#raise Exception
|
||||
with tt.AssertPrints('4', suppress=False):
|
||||
yield u'print(2*2)'
|
||||
|
||||
with tt.AssertPrints('SyntaxError: input contains', suppress=False):
|
||||
yield u'print(2345) # syntaxerror'
|
||||
|
||||
with tt.AssertPrints('16', suppress=False):
|
||||
yield u'print(4*4)'
|
||||
|
||||
finally:
|
||||
ip.input_transformers_post.remove(syntax_error_transformer)
|
||||
|
||||
def test_repl_not_plain_text(self):
|
||||
ip = get_ipython()
|
||||
formatter = ip.display_formatter
|
||||
assert formatter.active_types == ['text/plain']
|
||||
|
||||
# terminal may have arbitrary mimetype handler to open external viewer
|
||||
# or inline images.
|
||||
assert formatter.ipython_display_formatter.enabled
|
||||
|
||||
class Test(object):
|
||||
def __repr__(self):
|
||||
return "<Test %i>" % id(self)
|
||||
|
||||
def _repr_html_(self):
|
||||
return '<html>'
|
||||
|
||||
# verify that HTML repr isn't computed
|
||||
obj = Test()
|
||||
data, _ = formatter.format(obj)
|
||||
self.assertEqual(data, {'text/plain': repr(obj)})
|
||||
|
||||
class Test2(Test):
|
||||
def _ipython_display_(self):
|
||||
from IPython.display import display, HTML
|
||||
|
||||
display(HTML("<custom>"))
|
||||
|
||||
# verify that mimehandlers are called
|
||||
called = False
|
||||
|
||||
def handler(data, metadata):
|
||||
print("Handler called")
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
ip.display_formatter.active_types.append("text/html")
|
||||
ip.display_formatter.formatters["text/html"].enabled = True
|
||||
ip.mime_renderers["text/html"] = handler
|
||||
try:
|
||||
obj = Test()
|
||||
display(obj)
|
||||
finally:
|
||||
ip.display_formatter.formatters["text/html"].enabled = False
|
||||
del ip.mime_renderers["text/html"]
|
||||
|
||||
assert called == True
|
||||
|
||||
|
||||
def syntax_error_transformer(lines):
|
||||
"""Transformer that throws SyntaxError if 'syntaxerror' is in the code."""
|
||||
for line in lines:
|
||||
pos = line.find('syntaxerror')
|
||||
if pos >= 0:
|
||||
e = SyntaxError('input contains "syntaxerror"')
|
||||
e.text = line
|
||||
e.offset = pos + 1
|
||||
raise e
|
||||
return lines
|
||||
|
||||
|
||||
class TerminalMagicsTestCase(unittest.TestCase):
|
||||
def test_paste_magics_blankline(self):
|
||||
"""Test that code with a blank line doesn't get split (gh-3246)."""
|
||||
ip = get_ipython()
|
||||
s = ('def pasted_func(a):\n'
|
||||
' b = a+1\n'
|
||||
'\n'
|
||||
' return b')
|
||||
|
||||
tm = ip.magics_manager.registry['TerminalMagics']
|
||||
tm.store_or_execute(s, name=None)
|
||||
|
||||
self.assertEqual(ip.user_ns['pasted_func'](54), 55)
|
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from IPython.terminal.pt_inputhooks import set_qt_api, get_inputhook_name_and_func
|
||||
|
||||
|
||||
guis_avail = []
|
||||
|
||||
|
||||
def _get_qt_vers():
|
||||
"""If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due
|
||||
to the import mechanism, we can't import multiple versions of Qt in one session."""
|
||||
for gui in ["qt", "qt6", "qt5"]:
|
||||
print(f"Trying {gui}")
|
||||
try:
|
||||
set_qt_api(gui)
|
||||
importlib.import_module("IPython.terminal.pt_inputhooks.qt")
|
||||
guis_avail.append(gui)
|
||||
if "QT_API" in os.environ.keys():
|
||||
del os.environ["QT_API"]
|
||||
except ImportError:
|
||||
pass # that version of Qt isn't available.
|
||||
except RuntimeError:
|
||||
pass # the version of IPython doesn't know what to do with this Qt version.
|
||||
|
||||
|
||||
_get_qt_vers()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed."
|
||||
)
|
||||
def test_inputhook_qt():
|
||||
# Choose the "best" Qt version.
|
||||
gui_ret, _ = get_inputhook_name_and_func("qt")
|
||||
|
||||
assert gui_ret != "qt" # you get back the specific version that was loaded.
|
||||
assert gui_ret in guis_avail
|
||||
|
||||
if len(guis_avail) > 2:
|
||||
# ...and now we're stuck with this version of Qt for good; can't switch.
|
||||
for not_gui in ["qt6", "qt5"]:
|
||||
if not_gui != gui_ret:
|
||||
break
|
||||
# Try to import the other gui; it won't work.
|
||||
gui_ret2, _ = get_inputhook_name_and_func(not_gui)
|
||||
assert gui_ret2 == gui_ret
|
||||
assert gui_ret2 != not_gui
|
@@ -0,0 +1,485 @@
|
||||
import pytest
|
||||
from IPython.terminal.shortcuts.auto_suggest import (
|
||||
accept,
|
||||
accept_or_jump_to_end,
|
||||
accept_token,
|
||||
accept_character,
|
||||
accept_word,
|
||||
accept_and_keep_cursor,
|
||||
discard,
|
||||
NavigableAutoSuggestFromHistory,
|
||||
swap_autosuggestion_up,
|
||||
swap_autosuggestion_down,
|
||||
)
|
||||
from IPython.terminal.shortcuts.auto_match import skip_over
|
||||
from IPython.terminal.shortcuts import create_ipython_shortcuts, reset_search_buffer
|
||||
|
||||
from prompt_toolkit.history import InMemoryHistory
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.enums import DEFAULT_BUFFER
|
||||
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
|
||||
def test_deprected():
|
||||
import IPython.terminal.shortcuts.auto_suggest as iptsa
|
||||
|
||||
with pytest.warns(DeprecationWarning, match=r"8\.12.+accept_or_jump_to_end"):
|
||||
iptsa.accept_in_vi_insert_mode
|
||||
|
||||
|
||||
def make_event(text, cursor, suggestion):
|
||||
event = Mock()
|
||||
event.current_buffer = Mock()
|
||||
event.current_buffer.suggestion = Mock()
|
||||
event.current_buffer.text = text
|
||||
event.current_buffer.cursor_position = cursor
|
||||
event.current_buffer.suggestion.text = suggestion
|
||||
event.current_buffer.document = Document(text=text, cursor_position=cursor)
|
||||
return event
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, suggestion, expected",
|
||||
[
|
||||
("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
|
||||
("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
|
||||
],
|
||||
)
|
||||
def test_accept(text, suggestion, expected):
|
||||
event = make_event(text, len(text), suggestion)
|
||||
buffer = event.current_buffer
|
||||
buffer.insert_text = Mock()
|
||||
accept(event)
|
||||
assert buffer.insert_text.called
|
||||
assert buffer.insert_text.call_args[0] == (expected,)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, suggestion",
|
||||
[
|
||||
("", "def out(tag: str, n=50):"),
|
||||
("def ", "out(tag: str, n=50):"),
|
||||
],
|
||||
)
|
||||
def test_discard(text, suggestion):
|
||||
event = make_event(text, len(text), suggestion)
|
||||
buffer = event.current_buffer
|
||||
buffer.insert_text = Mock()
|
||||
discard(event)
|
||||
assert not buffer.insert_text.called
|
||||
assert buffer.suggestion is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, cursor, suggestion, called",
|
||||
[
|
||||
("123456", 6, "123456789", True),
|
||||
("123456", 3, "123456789", False),
|
||||
("123456 \n789", 6, "123456789", True),
|
||||
],
|
||||
)
|
||||
def test_autosuggest_at_EOL(text, cursor, suggestion, called):
|
||||
"""
|
||||
test that autosuggest is only applied at end of line.
|
||||
"""
|
||||
|
||||
event = make_event(text, cursor, suggestion)
|
||||
event.current_buffer.insert_text = Mock()
|
||||
accept_or_jump_to_end(event)
|
||||
if called:
|
||||
event.current_buffer.insert_text.assert_called()
|
||||
else:
|
||||
event.current_buffer.insert_text.assert_not_called()
|
||||
# event.current_buffer.document.get_end_of_line_position.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, suggestion, expected",
|
||||
[
|
||||
("", "def out(tag: str, n=50):", "def "),
|
||||
("d", "ef out(tag: str, n=50):", "ef "),
|
||||
("de ", "f out(tag: str, n=50):", "f "),
|
||||
("def", " out(tag: str, n=50):", " "),
|
||||
("def ", "out(tag: str, n=50):", "out("),
|
||||
("def o", "ut(tag: str, n=50):", "ut("),
|
||||
("def ou", "t(tag: str, n=50):", "t("),
|
||||
("def out", "(tag: str, n=50):", "("),
|
||||
("def out(", "tag: str, n=50):", "tag: "),
|
||||
("def out(t", "ag: str, n=50):", "ag: "),
|
||||
("def out(ta", "g: str, n=50):", "g: "),
|
||||
("def out(tag", ": str, n=50):", ": "),
|
||||
("def out(tag:", " str, n=50):", " "),
|
||||
("def out(tag: ", "str, n=50):", "str, "),
|
||||
("def out(tag: s", "tr, n=50):", "tr, "),
|
||||
("def out(tag: st", "r, n=50):", "r, "),
|
||||
("def out(tag: str", ", n=50):", ", n"),
|
||||
("def out(tag: str,", " n=50):", " n"),
|
||||
("def out(tag: str, ", "n=50):", "n="),
|
||||
("def out(tag: str, n", "=50):", "="),
|
||||
("def out(tag: str, n=", "50):", "50)"),
|
||||
("def out(tag: str, n=5", "0):", "0)"),
|
||||
("def out(tag: str, n=50", "):", "):"),
|
||||
("def out(tag: str, n=50)", ":", ":"),
|
||||
],
|
||||
)
|
||||
def test_autosuggest_token(text, suggestion, expected):
|
||||
event = make_event(text, len(text), suggestion)
|
||||
event.current_buffer.insert_text = Mock()
|
||||
accept_token(event)
|
||||
assert event.current_buffer.insert_text.called
|
||||
assert event.current_buffer.insert_text.call_args[0] == (expected,)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, suggestion, expected",
|
||||
[
|
||||
("", "def out(tag: str, n=50):", "d"),
|
||||
("d", "ef out(tag: str, n=50):", "e"),
|
||||
("de ", "f out(tag: str, n=50):", "f"),
|
||||
("def", " out(tag: str, n=50):", " "),
|
||||
],
|
||||
)
|
||||
def test_accept_character(text, suggestion, expected):
|
||||
event = make_event(text, len(text), suggestion)
|
||||
event.current_buffer.insert_text = Mock()
|
||||
accept_character(event)
|
||||
assert event.current_buffer.insert_text.called
|
||||
assert event.current_buffer.insert_text.call_args[0] == (expected,)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, suggestion, expected",
|
||||
[
|
||||
("", "def out(tag: str, n=50):", "def "),
|
||||
("d", "ef out(tag: str, n=50):", "ef "),
|
||||
("de", "f out(tag: str, n=50):", "f "),
|
||||
("def", " out(tag: str, n=50):", " "),
|
||||
# (this is why we also have accept_token)
|
||||
("def ", "out(tag: str, n=50):", "out(tag: "),
|
||||
],
|
||||
)
|
||||
def test_accept_word(text, suggestion, expected):
|
||||
event = make_event(text, len(text), suggestion)
|
||||
event.current_buffer.insert_text = Mock()
|
||||
accept_word(event)
|
||||
assert event.current_buffer.insert_text.called
|
||||
assert event.current_buffer.insert_text.call_args[0] == (expected,)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, suggestion, expected, cursor",
|
||||
[
|
||||
("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
|
||||
("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
|
||||
],
|
||||
)
|
||||
def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
|
||||
event = make_event(text, cursor, suggestion)
|
||||
buffer = event.current_buffer
|
||||
buffer.insert_text = Mock()
|
||||
accept_and_keep_cursor(event)
|
||||
assert buffer.insert_text.called
|
||||
assert buffer.insert_text.call_args[0] == (expected,)
|
||||
assert buffer.cursor_position == cursor
|
||||
|
||||
|
||||
def test_autosuggest_token_empty():
|
||||
full = "def out(tag: str, n=50):"
|
||||
event = make_event(full, len(full), "")
|
||||
event.current_buffer.insert_text = Mock()
|
||||
|
||||
with patch(
|
||||
"prompt_toolkit.key_binding.bindings.named_commands.forward_word"
|
||||
) as forward_word:
|
||||
accept_token(event)
|
||||
assert not event.current_buffer.insert_text.called
|
||||
assert forward_word.called
|
||||
|
||||
|
||||
def test_reset_search_buffer():
|
||||
event_with_text = Mock()
|
||||
event_with_text.current_buffer.document.text = "some text"
|
||||
event_with_text.current_buffer.reset = Mock()
|
||||
|
||||
event_empty = Mock()
|
||||
event_empty.current_buffer.document.text = ""
|
||||
event_empty.app.layout.focus = Mock()
|
||||
|
||||
reset_search_buffer(event_with_text)
|
||||
event_with_text.current_buffer.reset.assert_called_once()
|
||||
|
||||
reset_search_buffer(event_empty)
|
||||
event_empty.app.layout.focus.assert_called_once_with(DEFAULT_BUFFER)
|
||||
|
||||
|
||||
def test_other_providers():
|
||||
"""Ensure that swapping autosuggestions does not break with other providers"""
|
||||
provider = AutoSuggestFromHistory()
|
||||
ip = get_ipython()
|
||||
ip.auto_suggest = provider
|
||||
event = Mock()
|
||||
event.current_buffer = Buffer()
|
||||
assert swap_autosuggestion_up(event) is None
|
||||
assert swap_autosuggestion_down(event) is None
|
||||
|
||||
|
||||
async def test_navigable_provider():
|
||||
provider = NavigableAutoSuggestFromHistory()
|
||||
history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
|
||||
buffer = Buffer(history=history)
|
||||
ip = get_ipython()
|
||||
ip.auto_suggest = provider
|
||||
|
||||
async for _ in history.load():
|
||||
pass
|
||||
|
||||
buffer.cursor_position = 5
|
||||
buffer.text = "very"
|
||||
|
||||
up = swap_autosuggestion_up
|
||||
down = swap_autosuggestion_down
|
||||
|
||||
event = Mock()
|
||||
event.current_buffer = buffer
|
||||
|
||||
def get_suggestion():
|
||||
suggestion = provider.get_suggestion(buffer, buffer.document)
|
||||
buffer.suggestion = suggestion
|
||||
return suggestion
|
||||
|
||||
assert get_suggestion().text == "_c"
|
||||
|
||||
# should go up
|
||||
up(event)
|
||||
assert get_suggestion().text == "_b"
|
||||
|
||||
# should skip over 'very' which is identical to buffer content
|
||||
up(event)
|
||||
assert get_suggestion().text == "_a"
|
||||
|
||||
# should cycle back to beginning
|
||||
up(event)
|
||||
assert get_suggestion().text == "_c"
|
||||
|
||||
# should cycle back through end boundary
|
||||
down(event)
|
||||
assert get_suggestion().text == "_a"
|
||||
|
||||
down(event)
|
||||
assert get_suggestion().text == "_b"
|
||||
|
||||
down(event)
|
||||
assert get_suggestion().text == "_c"
|
||||
|
||||
down(event)
|
||||
assert get_suggestion().text == "_a"
|
||||
|
||||
|
||||
async def test_navigable_provider_multiline_entries():
|
||||
provider = NavigableAutoSuggestFromHistory()
|
||||
history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
|
||||
buffer = Buffer(history=history)
|
||||
ip = get_ipython()
|
||||
ip.auto_suggest = provider
|
||||
|
||||
async for _ in history.load():
|
||||
pass
|
||||
|
||||
buffer.cursor_position = 5
|
||||
buffer.text = "very"
|
||||
up = swap_autosuggestion_up
|
||||
down = swap_autosuggestion_down
|
||||
|
||||
event = Mock()
|
||||
event.current_buffer = buffer
|
||||
|
||||
def get_suggestion():
|
||||
suggestion = provider.get_suggestion(buffer, buffer.document)
|
||||
buffer.suggestion = suggestion
|
||||
return suggestion
|
||||
|
||||
assert get_suggestion().text == "_c"
|
||||
|
||||
up(event)
|
||||
assert get_suggestion().text == "_b"
|
||||
|
||||
up(event)
|
||||
assert get_suggestion().text == "_a"
|
||||
|
||||
down(event)
|
||||
assert get_suggestion().text == "_b"
|
||||
|
||||
down(event)
|
||||
assert get_suggestion().text == "_c"
|
||||
|
||||
|
||||
def create_session_mock():
|
||||
session = Mock()
|
||||
session.default_buffer = Buffer()
|
||||
return session
|
||||
|
||||
|
||||
def test_navigable_provider_connection():
|
||||
provider = NavigableAutoSuggestFromHistory()
|
||||
provider.skip_lines = 1
|
||||
|
||||
session_1 = create_session_mock()
|
||||
provider.connect(session_1)
|
||||
|
||||
assert provider.skip_lines == 1
|
||||
session_1.default_buffer.on_text_insert.fire()
|
||||
assert provider.skip_lines == 0
|
||||
|
||||
session_2 = create_session_mock()
|
||||
provider.connect(session_2)
|
||||
provider.skip_lines = 2
|
||||
|
||||
assert provider.skip_lines == 2
|
||||
session_2.default_buffer.on_text_insert.fire()
|
||||
assert provider.skip_lines == 0
|
||||
|
||||
provider.skip_lines = 3
|
||||
provider.disconnect()
|
||||
session_1.default_buffer.on_text_insert.fire()
|
||||
session_2.default_buffer.on_text_insert.fire()
|
||||
assert provider.skip_lines == 3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ipython_with_prompt():
|
||||
ip = get_ipython()
|
||||
ip.pt_app = Mock()
|
||||
ip.pt_app.key_bindings = create_ipython_shortcuts(ip)
|
||||
try:
|
||||
yield ip
|
||||
finally:
|
||||
ip.pt_app = None
|
||||
|
||||
|
||||
def find_bindings_by_command(command):
|
||||
ip = get_ipython()
|
||||
return [
|
||||
binding
|
||||
for binding in ip.pt_app.key_bindings.bindings
|
||||
if binding.handler == command
|
||||
]
|
||||
|
||||
|
||||
def test_modify_unique_shortcut(ipython_with_prompt):
|
||||
original = find_bindings_by_command(accept_token)
|
||||
assert len(original) == 1
|
||||
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "IPython:auto_suggest.accept_token", "new_keys": ["a", "b", "c"]}
|
||||
]
|
||||
matched = find_bindings_by_command(accept_token)
|
||||
assert len(matched) == 1
|
||||
assert list(matched[0].keys) == ["a", "b", "c"]
|
||||
assert list(matched[0].keys) != list(original[0].keys)
|
||||
assert matched[0].filter == original[0].filter
|
||||
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "IPython:auto_suggest.accept_token", "new_filter": "always"}
|
||||
]
|
||||
matched = find_bindings_by_command(accept_token)
|
||||
assert len(matched) == 1
|
||||
assert list(matched[0].keys) != ["a", "b", "c"]
|
||||
assert list(matched[0].keys) == list(original[0].keys)
|
||||
assert matched[0].filter != original[0].filter
|
||||
|
||||
|
||||
def test_disable_shortcut(ipython_with_prompt):
|
||||
matched = find_bindings_by_command(accept_token)
|
||||
assert len(matched) == 1
|
||||
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "IPython:auto_suggest.accept_token", "new_keys": []}
|
||||
]
|
||||
matched = find_bindings_by_command(accept_token)
|
||||
assert len(matched) == 0
|
||||
|
||||
ipython_with_prompt.shortcuts = []
|
||||
matched = find_bindings_by_command(accept_token)
|
||||
assert len(matched) == 1
|
||||
|
||||
|
||||
def test_modify_shortcut_with_filters(ipython_with_prompt):
|
||||
matched = find_bindings_by_command(skip_over)
|
||||
matched_keys = {m.keys[0] for m in matched}
|
||||
assert matched_keys == {")", "]", "}", "'", '"'}
|
||||
|
||||
with pytest.raises(ValueError, match="Multiple shortcuts matching"):
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "IPython:auto_match.skip_over", "new_keys": ["x"]}
|
||||
]
|
||||
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{
|
||||
"command": "IPython:auto_match.skip_over",
|
||||
"new_keys": ["x"],
|
||||
"match_filter": "focused_insert & auto_match & followed_by_single_quote",
|
||||
}
|
||||
]
|
||||
matched = find_bindings_by_command(skip_over)
|
||||
matched_keys = {m.keys[0] for m in matched}
|
||||
assert matched_keys == {")", "]", "}", "x", '"'}
|
||||
|
||||
|
||||
def example_command():
|
||||
pass
|
||||
|
||||
|
||||
def test_add_shortcut_for_new_command(ipython_with_prompt):
|
||||
matched = find_bindings_by_command(example_command)
|
||||
assert len(matched) == 0
|
||||
|
||||
with pytest.raises(ValueError, match="example_command is not a known"):
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "example_command", "new_keys": ["x"]}
|
||||
]
|
||||
matched = find_bindings_by_command(example_command)
|
||||
assert len(matched) == 0
|
||||
|
||||
|
||||
def test_modify_shortcut_failure(ipython_with_prompt):
|
||||
with pytest.raises(ValueError, match="No shortcuts matching"):
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{
|
||||
"command": "IPython:auto_match.skip_over",
|
||||
"match_keys": ["x"],
|
||||
"new_keys": ["y"],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_shortcut_for_existing_command(ipython_with_prompt):
|
||||
matched = find_bindings_by_command(skip_over)
|
||||
assert len(matched) == 5
|
||||
|
||||
with pytest.raises(ValueError, match="Cannot add a shortcut without keys"):
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "IPython:auto_match.skip_over", "new_keys": [], "create": True}
|
||||
]
|
||||
|
||||
ipython_with_prompt.shortcuts = [
|
||||
{"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
|
||||
]
|
||||
matched = find_bindings_by_command(skip_over)
|
||||
assert len(matched) == 6
|
||||
|
||||
ipython_with_prompt.shortcuts = []
|
||||
matched = find_bindings_by_command(skip_over)
|
||||
assert len(matched) == 5
|
||||
|
||||
|
||||
def test_setting_shortcuts_before_pt_app_init():
|
||||
ipython = get_ipython()
|
||||
assert ipython.pt_app is None
|
||||
shortcuts = [
|
||||
{"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
|
||||
]
|
||||
ipython.shortcuts = shortcuts
|
||||
assert ipython.shortcuts == shortcuts
|
Reference in New Issue
Block a user