Skip to content

Commit

Permalink
Merge pull request #23 from sloria/fix-ipython
Browse files Browse the repository at this point in the history
Fix support for IPython>=5
  • Loading branch information
sloria committed Oct 15, 2017
2 parents 568068a + 8df308f commit 1b40493
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 44 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog
Next release
************

- Support IPython>=5.0 (:issue:`20`). Thanks :user:`rplevka` for
reporting.
- Use `$SHELL` as the default interpreter for commands if not explicitly
specified.
- Remove invalid import in ``ipython`` module. Thanks :user:`axocomm`.
Expand Down
9 changes: 7 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ configures the prompt. Can be any of the built-in themes or a custom prompt temp

**Using a custom template**:

You can provide the ``prompt`` option with a custom template. To include the user, hostname, current directory, current path to working directory, current datetime, or vcs branch (git or Mercurial), use ``{user}``, ``{hostname}``, ``{dir}``, ``{cwd}``, ``{now}``, and ``{vcs_branch}``, respectively.
You can provide the ``prompt`` option with a custom template. To include the user, hostname, current directory, current path to working directory, current datetime, or vcs branch (git or Mercurial), use ``{user}``, ``{hostname}``, ``{dir}``, ``{cwd}``, ``{now}``, and ``{vcs_branch}``, respectively.

For git, ``{vcs_branch}`` just shows the branch. For Mercurial, this shows the branch name + the bookmark, except it omits the default branch name if there is a bookmark. This is equivalent to ``{git_branch}{hg_id}``. There are also specialised ``{hg_branch}``, and ``{hg_bookmark}`` keywords that only show that information, without the combined logic of ``{hg_id}``.

Expand Down Expand Up @@ -212,10 +212,15 @@ If you have `IPython <https://ipython.org/>`_ installed, you can run doitlive in
print()
# Magic!
% time fib(100)
%time fib(100)
```
.. note::
Only IPython>=5.0 is supported.
Bash completion
---------------
Expand Down
5 changes: 4 additions & 1 deletion doitlive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import socket
import subprocess
import sys
import textwrap

from click import echo as click_echo
from click import style, secho, getchar
Expand Down Expand Up @@ -573,7 +574,9 @@ def run(commands, shell=None, prompt_template='default', speed=1,
from doitlive.ipython import start_ipython_player
except ImportError:
raise RuntimeError('```ipython blocks require IPython to be installed')
start_ipython_player(py_commands, speed=state['speed'])
# dedent all the commands to account for IPython's autoindentation
ipy_commands = [textwrap.dedent(cmd) for cmd in py_commands]
start_ipython_player(ipy_commands, speed=state['speed'])
else:
start_python_player(py_commands, speed=state['speed'])
else:
Expand Down
150 changes: 109 additions & 41 deletions doitlive/ipython.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,126 @@
# -*- coding: utf-8 -*-
"""doitlive IPython support."""
from __future__ import absolute_import
from __future__ import absolute_import, print_function

from warnings import warn

from IPython.utils import py3compat
from IPython.terminal.interactiveshell import TerminalInteractiveShell
from click import Abort
from IPython.terminal.interactiveshell import (DISPLAY_BANNER_DEPRECATED,
TerminalInteractiveShell)
from IPython.terminal.ipapp import TerminalIPythonApp
from IPython.utils.text import num_ini_spaces
from prompt_toolkit.interface import (CommandLineInterface,
_InterfaceEventLoopCallbacks)
from prompt_toolkit.key_binding.input_processor import KeyPress
from prompt_toolkit.keys import Keys
from prompt_toolkit.shortcuts import create_output

from doitlive import RETURNS, wait_for, echo

class _PlayerInterfaceEventLoopCallbacks(_InterfaceEventLoopCallbacks):
def __init__(self, cli, on_feed_key):
super(_PlayerInterfaceEventLoopCallbacks, self).__init__(cli)
self.on_feed_key = on_feed_key

# Override _InterfaceEventLoopCallbacks
def feed_key(self, key_press, *args, **kwargs):
key_press = self.on_feed_key(key_press)
if key_press is not None:
return super(_PlayerInterfaceEventLoopCallbacks, self).feed_key(key_press,
*args, **kwargs)


from doitlive import magictype, wait_for, RETURNS
from click import echo
class _PlayerCommandLineInterface(CommandLineInterface):
def __init__(self, application, eventloop=None, input=None, output=None,
on_feed_key=None):
super(_PlayerCommandLineInterface, self).__init__(application, eventloop, input, output)
self.on_feed_key = on_feed_key

# Overrride CommandLineInterface
def create_eventloop_callbacks(self):
return _PlayerInterfaceEventLoopCallbacks(self, on_feed_key=self.on_feed_key)


class PlayerTerminalInteractiveShell(TerminalInteractiveShell):
"""A magic Ipython terminal shell."""
def __init__(self, commands, speed, *args, **kwargs):
self.commands = commands
self.current_command = 0 # Index of current command
"""A magic IPython terminal shell."""
def __init__(self, commands, speed=1, *args, **kwargs):
self.commands = commands or []
self.speed = speed
# Index of current command
self.current_command_index = 0
# Index of current character in current command
self.current_command_pos = 0
super(PlayerTerminalInteractiveShell, self).__init__(*args, **kwargs)

# Override raw_input to do magic-typing
# NOTE: Much of this is copy-and-pasted from the parent class's implementation
def on_feed_key(self, key_press):
"""Handles the magictyping when a key is pressed"""
if key_press.key == Keys.Escape:
echo(carriage_return=True)
raise Abort()
if key_press.key == Keys.Backspace:
self.current_command_pos -= 1
return key_press
ret = None
if key_press.key != Keys.CPRResponse:
if self.current_command_pos < len(self.current_command):
current_key = self.current_command_key
ret = KeyPress(current_key)
self.current_command_pos += self.speed
else:
# Command is finished, wait for Enter
if key_press.key != Keys.Enter:
return None
self.current_command_index += 1
self.current_command_pos = 0
ret = key_press
return ret

@property
def current_command(self):
return self.commands[self.current_command_index]

@property
def current_command_key(self):
pos = self.current_command_pos
return self.current_command[pos:pos + self.speed]

# Overrride TerminalInteractiveShell
# Much of this is copy-and-pasted from the parent class implementation
# due to lack of hooks
def raw_input(self, prompt=''):
if self.current_command > len(self.commands) - 1:
echo('Do you really want to exit ([y]/n)? ', nl=False)
wait_for(RETURNS)
self.ask_exit()
return ''
# raw_input expects str, but we pass it unicode sometimes
prompt = py3compat.cast_bytes_py2(prompt)

try:
command = self.commands[self.current_command]
magictype(command, prompt_template=prompt, speed=self.speed)
line = py3compat.cast_unicode_py2(command)
except ValueError:
warn("\n********\nYou or a %run:ed script called sys.stdin.close()"
" or sys.stdout.close()!\nExiting IPython!\n")
self.ask_exit()
return ""

# Try to be reasonably smart about not re-indenting pasted input more
# than necessary. We do this by trimming out the auto-indent initial
# spaces, if the user's actual input started itself with whitespace.
if self.autoindent:
if num_ini_spaces(line) > self.indent_current_nsp:
line = line[self.indent_current_nsp:]
self.indent_current_nsp = 0

self.current_command += 1
return line
def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):

if display_banner is not DISPLAY_BANNER_DEPRECATED:
warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2) # noqa

self.keep_running = True
while self.keep_running:
print(self.separate_in, end='')

if self.current_command_index > len(self.commands) - 1:
echo('Do you really want to exit ([y]/n)? ', nl=False)
wait_for(RETURNS)
self.ask_exit()
return None

try:
code = self.prompt_for_code()
except EOFError:
if (not self.confirm_exit) \
or self.ask_yes_no('Do you really want to exit ([y]/n)?', 'y', 'n'):
self.ask_exit()

else:
if code:
self.run_cell(code, store_history=True)

# Overrride TerminalInteractiveShell
def init_prompt_toolkit_cli(self):
super(PlayerTerminalInteractiveShell, self).init_prompt_toolkit_cli()
# override CommandLineInterface
self.pt_cli = _PlayerCommandLineInterface(
self._pt_app, eventloop=self._eventloop,
output=create_output(true_color=self.true_color),
on_feed_key=self.on_feed_key,
)

class PlayerTerminalIPythonApp(TerminalIPythonApp):
"""IPython app that runs the PlayerTerminalInteractiveShell."""
Expand Down

0 comments on commit 1b40493

Please sign in to comment.