fixed subscription table

This commit is contained in:
2025-02-02 00:02:31 -05:00
parent a1ab31acfe
commit ef5f57e678
5389 changed files with 686710 additions and 28 deletions

View File

@@ -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')

View 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()

View 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

View 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

View 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")

View 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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'))

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View 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)

View File

@@ -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,
]

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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"]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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