Skip to content

Commit

Permalink
Add ipython mode
Browse files Browse the repository at this point in the history
closes #8
  • Loading branch information
sloria committed May 3, 2016
1 parent e6a8bc3 commit f2d7259
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 11 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
@@ -1,6 +1,13 @@
Changelog
---------

2.5.0 (unreleased)
******************

Features

- Add ipython mode (:issue:`8`).

2.4.0 (2015-10-18)
******************

Expand Down
3 changes: 3 additions & 0 deletions dev-requirements.txt
Expand Up @@ -10,3 +10,6 @@ twine

# Syntax checking
flake8==2.4.1

# Soft deps
IPython
24 changes: 23 additions & 1 deletion docs/index.rst
Expand Up @@ -170,7 +170,7 @@ Whether to echo comments or not. If enabled, non-magic comments will be echoed b
Python mode
-----------

doitlive supports autotyping in a Python console. You can enter Python mode in a session by enclosing Python code in triple-backticks within your ``session.sh`` file, like so:
doitlive supports autotyping in a Python console. You can enter Python mode in a session by enclosing Python code in triple-backticks, like so:

.. code-block:: bash
Expand All @@ -187,6 +187,28 @@ doitlive supports autotyping in a Python console. You can enter Python mode in a
print("The sum is: {sum}".format(sum=sum))
```
IPython mode
------------
If you have `IPython <https://ipython.org/>`_ installed, you can run doitlive in ``ipython`` mode. Just enclose your Python code in triple-backticks, like so:
.. code-block:: bash
# in session.sh
```ipython
def fib(n):
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
# Magic!
% time fib(100)
```
Bash completion
---------------
Expand Down
24 changes: 17 additions & 7 deletions doitlive/__init__.py
Expand Up @@ -36,7 +36,6 @@
get_current_vcs_branch
)


__version__ = '2.4.0'
__author__ = 'Steven Loria'
__license__ = 'MIT'
Expand Down Expand Up @@ -421,6 +420,8 @@ def interact(self, banner=None):
self.write("%s\n" % str(banner))
self.run_commands()

def start_python_player(commands, speed=1):
PythonPlayerConsole(commands=commands, speed=speed).interact()

class PythonRecorderConsole(InteractiveConsole):
"""An interactive Python console that stores user input in a list."""
Expand Down Expand Up @@ -497,7 +498,7 @@ def commentecho(self, doit=None):
'commentecho': lambda state, arg: state.commentecho(arg),
}


SHELL_RE = re.compile(r'```(python|ipython)')
def run(commands, shell='/bin/bash', prompt_template='default', speed=1,
quiet=False, test_mode=False, commentecho=False):
if not quiet:
Expand All @@ -517,6 +518,7 @@ def run(commands, shell='/bin/bash', prompt_template='default', speed=1,
i += 1
if not command:
continue
shell_match = SHELL_RE.match(command)
if command.startswith('#'):
# Parse comment magic
match = OPTION_RE.match(command)
Expand All @@ -528,26 +530,34 @@ def run(commands, shell='/bin/bash', prompt_template='default', speed=1,
comment = command.lstrip("#")
secho(comment, fg='yellow', bold=True)
continue
elif command.startswith('```python'):
elif shell_match:
shell_name = shell_match.groups()[0].strip()
py_commands = []
more = True
while more: # slurp up all the python code
try:
py_command = commands[i].rstrip()
except IndexError:
raise SessionError('Unmatched python code block in '
'session file.')
raise SessionError('Unmatched {0} code block in '
'session file.'.format(shell_name))
i += 1
if py_command.startswith('```'):
i += 1
more = False
else:
py_commands.append(py_command)
# Run the player console
magictype('python',
magictype(shell_name,
prompt_template=state['prompt_template'],
speed=state['speed'])
PythonPlayerConsole(py_commands, speed=state['speed']).interact()
if shell_name == 'ipython':
try:
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'])
else:
start_python_player(py_commands, speed=state['speed'])
else:
magicrun(command, **state)
echo_prompt(state['prompt_template'])
Expand Down
79 changes: 79 additions & 0 deletions doitlive/ipython.py
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""doitlive IPython support."""
from __future__ import absolute_import

from IPython.utils import py3compat
from IPython.utils.warn import warn
from IPython.terminal.interactiveshell import TerminalInteractiveShell
from IPython.terminal.ipapp import TerminalIPythonApp
from IPython.utils.text import num_ini_spaces


from doitlive import magictype, wait_for, RETURNS
from click import echo


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
self.speed = speed
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
# 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

class PlayerTerminalIPythonApp(TerminalIPythonApp):
"""IPython app that runs the PlayerTerminalInteractiveShell."""
commands = tuple()
speed = 1

# Ignore command line args, since this will be run from the doitlive CLI
def parse_command_line(self, argv=None):
return None

def init_shell(self):
"""initialize the InteractiveShell instance"""
self.shell = PlayerTerminalInteractiveShell.instance(
commands=self.commands, speed=self.speed,
parent=self, display_banner=False, profile_dir=self.profile_dir,
ipython_dir=self.ipython_dir, user_ns=self.user_ns
)
self.shell.configurables.append(self)

def start_ipython_player(commands, speed=1):
"""Starts a new magic IPython shell."""
PlayerTerminalIPythonApp.commands = commands
PlayerTerminalIPythonApp.speed = speed
PlayerTerminalIPythonApp.launch_instance()
21 changes: 21 additions & 0 deletions examples/python_and_ipython.sh
@@ -0,0 +1,21 @@
echo 'Lets run some python'

```python
fruits = ['Banana', 'Apple', 'Lime']
loud_fruits = [fruit.upper() for fruit in fruits]
```
echo 'We can also run in ipython mode'
```ipython
def fib(n):
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
# Magic!
% time fib(100)
```
2 changes: 1 addition & 1 deletion examples/walkthrough.sh
@@ -1,7 +1,7 @@
#doitlive shell: /bin/bash
#doitlive prompt: default
#doitlive speed: 1
#doitlive env: DOCS_URL=http://doitlive.rtfd.org
#doitlive env: DOCS_URL=http://doitlive.readthdocs.io
#doitlive alias: edit="nano "

echo 'Hello there!'
Expand Down
5 changes: 3 additions & 2 deletions tasks.py
Expand Up @@ -9,7 +9,7 @@
build_dir = os.path.join(docs_dir, '_build')

@task
def test(tox=False, last_failing=False):
def test(tox=False, lint=True, last_failing=False):
"""Run the tests.
Note: --watch requires pytest-xdist to be installed.
Expand All @@ -18,7 +18,8 @@ def test(tox=False, last_failing=False):
run('tox')
else:
import pytest
flake()
if lint:
flake()
args = []
if last_failing:
args.append('--lf')
Expand Down

0 comments on commit f2d7259

Please sign in to comment.