ipython_directive.py
835 lines
| 26.5 KiB
| text/x-python
|
PythonLexer
Fernando Perez
|
r2439 | # -*- coding: utf-8 -*- | ||
"""Sphinx directive to support embedded IPython code. | ||||
This directive allows pasting of entire interactive IPython sessions, prompts | ||||
and all, and their code will actually get re-executed at doc build time, with | ||||
Skipper Seabold
|
r4432 | all prompts renumbered sequentially. It also allows you to input code as a pure | ||
python input by giving the argument python to the directive. The output looks | ||||
like an interactive ipython section. | ||||
Fernando Perez
|
r2439 | |||
To enable this directive, simply list it in your Sphinx ``conf.py`` file | ||||
(making sure the directory where you placed it is visible to sphinx, as is | ||||
needed for all Sphinx directives). | ||||
By default this directive assumes that your prompts are unchanged IPython ones, | ||||
Skipper Seabold
|
r4432 | but this can be customized. The configurable options that can be placed in | ||
conf.py are | ||||
ipython_savefig_dir: | ||||
The directory in which to save the figures. This is relative to the | ||||
Sphinx source directory. The default is `html_static_path`. | ||||
ipython_rgxin: | ||||
The compiled regular expression to denote the start of IPython input | ||||
lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You | ||||
shouldn't need to change this. | ||||
ipython_rgxout: | ||||
The compiled regular expression to denote the start of IPython output | ||||
lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You | ||||
shouldn't need to change this. | ||||
ipython_promptin: | ||||
The string to represent the IPython input prompt in the generated ReST. | ||||
The default is 'In [%d]:'. This expects that the line numbers are used | ||||
in the prompt. | ||||
ipython_promptout: | ||||
The string to represent the IPython prompt in the generated ReST. The | ||||
default is 'Out [%d]:'. This expects that the line numbers are used | ||||
in the prompt. | ||||
Fernando Perez
|
r2439 | |||
ToDo | ||||
---- | ||||
- Turn the ad-hoc test() function into a real test suite. | ||||
- Break up ipython-specific functionality from matplotlib stuff into better | ||||
separated code. | ||||
Authors | ||||
------- | ||||
- John D Hunter: orignal author. | ||||
- Fernando Perez: refactoring, documentation, cleanups, port to 0.11. | ||||
- VáclavŠmilauer <eudoxos-AT-arcig.cz>: Prompt generalizations. | ||||
Skipper Seabold
|
r4432 | - Skipper Seabold, refactoring, cleanups, pure python addition | ||
Fernando Perez
|
r2439 | """ | ||
Thomas Kluyver
|
r13348 | from __future__ import print_function | ||
Fernando Perez
|
r2439 | |||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
# Stdlib | ||||
import os | ||||
import re | ||||
import sys | ||||
Skipper Seabold
|
r4432 | import tempfile | ||
Skipper Seabold
|
r6136 | import ast | ||
Fernando Perez
|
r2439 | |||
# To keep compatibility with various python versions | ||||
try: | ||||
from hashlib import md5 | ||||
except ImportError: | ||||
from md5 import md5 | ||||
# Third-party | ||||
import matplotlib | ||||
import sphinx | ||||
from docutils.parsers.rst import directives | ||||
Skipper Seabold
|
r4432 | from docutils import nodes | ||
from sphinx.util.compat import Directive | ||||
Fernando Perez
|
r2439 | |||
Ahmet Bakan
|
r10599 | matplotlib.use('Agg') | ||
Fernando Perez
|
r2439 | |||
# Our own | ||||
Brian Granger
|
r2499 | from IPython import Config, InteractiveShell | ||
Skipper Seabold
|
r4432 | from IPython.core.profiledir import ProfileDir | ||
from IPython.utils import io | ||||
Thomas Kluyver
|
r13366 | from IPython.utils.py3compat import PY3 | ||
if PY3: | ||||
from io import StringIO | ||||
else: | ||||
from StringIO import StringIO | ||||
Fernando Perez
|
r2439 | |||
#----------------------------------------------------------------------------- | ||||
# Globals | ||||
#----------------------------------------------------------------------------- | ||||
Skipper Seabold
|
r4432 | # for tokenizing blocks | ||
Fernando Perez
|
r2439 | COMMENT, INPUT, OUTPUT = range(3) | ||
#----------------------------------------------------------------------------- | ||||
# Functions and class declarations | ||||
#----------------------------------------------------------------------------- | ||||
Skipper Seabold
|
r4432 | def block_parser(part, rgxin, rgxout, fmtin, fmtout): | ||
Fernando Perez
|
r2439 | """ | ||
part is a string of ipython text, comprised of at most one | ||||
input, one ouput, comments, and blank lines. The block parser | ||||
parses the text into a list of:: | ||||
blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...] | ||||
where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and | ||||
data is, depending on the type of token:: | ||||
COMMENT : the comment string | ||||
INPUT: the (DECORATOR, INPUT_LINE, REST) where | ||||
DECORATOR: the input decorator (or None) | ||||
INPUT_LINE: the input as string (possibly multi-line) | ||||
REST : any stdout generated by the input line (not OUTPUT) | ||||
OUTPUT: the output string, possibly multi-line | ||||
""" | ||||
block = [] | ||||
lines = part.split('\n') | ||||
N = len(lines) | ||||
i = 0 | ||||
decorator = None | ||||
while 1: | ||||
if i==N: | ||||
# nothing left to parse -- the last line | ||||
break | ||||
line = lines[i] | ||||
i += 1 | ||||
line_stripped = line.strip() | ||||
if line_stripped.startswith('#'): | ||||
block.append((COMMENT, line)) | ||||
continue | ||||
if line_stripped.startswith('@'): | ||||
# we're assuming at most one decorator -- may need to | ||||
# rethink | ||||
decorator = line_stripped | ||||
continue | ||||
# does this look like an input line? | ||||
matchin = rgxin.match(line) | ||||
if matchin: | ||||
lineno, inputline = int(matchin.group(1)), matchin.group(2) | ||||
# the ....: continuation string | ||||
continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) | ||||
Nc = len(continuation) | ||||
# input lines can continue on for more than one line, if | ||||
# we have a '\' line continuation char or a function call | ||||
# echo line 'print'. The input line can only be | ||||
# terminated by the end of the block or an output line, so | ||||
# we parse out the rest of the input line if it is | ||||
# multiline as well as any echo text | ||||
rest = [] | ||||
while i<N: | ||||
# look ahead; if the next line is blank, or a comment, or | ||||
# an output line, we're done | ||||
nextline = lines[i] | ||||
matchout = rgxout.match(nextline) | ||||
#print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation)) | ||||
if matchout or nextline.startswith('#'): | ||||
break | ||||
elif nextline.startswith(continuation): | ||||
inputline += '\n' + nextline[Nc:] | ||||
else: | ||||
rest.append(nextline) | ||||
i+= 1 | ||||
block.append((INPUT, (decorator, inputline, '\n'.join(rest)))) | ||||
continue | ||||
# if it looks like an output line grab all the text to the end | ||||
# of the block | ||||
matchout = rgxout.match(line) | ||||
if matchout: | ||||
lineno, output = int(matchout.group(1)), matchout.group(2) | ||||
if i<N-1: | ||||
output = '\n'.join([output] + lines[i:]) | ||||
block.append((OUTPUT, output)) | ||||
break | ||||
return block | ||||
class EmbeddedSphinxShell(object): | ||||
"""An embedded IPython instance to run inside Sphinx""" | ||||
def __init__(self): | ||||
Thomas Kluyver
|
r13366 | self.cout = StringIO() | ||
Fernando Perez
|
r2439 | |||
# Create config object for IPython | ||||
config = Config() | ||||
config.Global.display_banner = False | ||||
config.Global.exec_lines = ['import numpy as np', | ||||
'from pylab import *' | ||||
] | ||||
config.InteractiveShell.autocall = False | ||||
config.InteractiveShell.autoindent = False | ||||
config.InteractiveShell.colors = 'NoColor' | ||||
Skipper Seabold
|
r4432 | # create a profile so instance history isn't saved | ||
tmp_profile_dir = tempfile.mkdtemp(prefix='profile_') | ||||
profname = 'auto_profile_sphinx_build' | ||||
pdir = os.path.join(tmp_profile_dir,profname) | ||||
profile = ProfileDir.create_profile_dir(pdir) | ||||
Fernando Perez
|
r2439 | # Create and initialize ipython, but don't start its mainloop | ||
Skipper Seabold
|
r4432 | IP = InteractiveShell.instance(config=config, profile_dir=profile) | ||
# io.stdout redirect must be done *after* instantiating InteractiveShell | ||||
io.stdout = self.cout | ||||
io.stderr = self.cout | ||||
# For debugging, so we can see normal output, use this: | ||||
#from IPython.utils.io import Tee | ||||
#io.stdout = Tee(self.cout, channel='stdout') # dbg | ||||
#io.stderr = Tee(self.cout, channel='stderr') # dbg | ||||
Fernando Perez
|
r2439 | |||
# Store a few parts of IPython we'll need. | ||||
Brian Granger
|
r2499 | self.IP = IP | ||
Fernando Perez
|
r2439 | self.user_ns = self.IP.user_ns | ||
self.user_global_ns = self.IP.user_global_ns | ||||
Skipper Seabold
|
r4432 | |||
Fernando Perez
|
r2439 | self.input = '' | ||
self.output = '' | ||||
self.is_verbatim = False | ||||
self.is_doctest = False | ||||
self.is_suppress = False | ||||
# on the first call to the savefig decorator, we'll import | ||||
# pyplot as plt so we can make a call to the plt.gcf().savefig | ||||
self._pyplot_imported = False | ||||
Skipper Seabold
|
r4432 | def clear_cout(self): | ||
Fernando Perez
|
r2439 | self.cout.seek(0) | ||
self.cout.truncate(0) | ||||
Skipper Seabold
|
r4432 | def process_input_line(self, line, store_history=True): | ||
Fernando Perez
|
r2439 | """process the input, capturing stdout""" | ||
#print "input='%s'"%self.input | ||||
stdout = sys.stdout | ||||
Skipper Seabold
|
r4432 | splitter = self.IP.input_splitter | ||
Fernando Perez
|
r2439 | try: | ||
sys.stdout = self.cout | ||||
Skipper Seabold
|
r4432 | splitter.push(line) | ||
more = splitter.push_accepts_more() | ||||
if not more: | ||||
source_raw = splitter.source_raw_reset()[1] | ||||
self.IP.run_cell(source_raw, store_history=store_history) | ||||
Fernando Perez
|
r2439 | finally: | ||
sys.stdout = stdout | ||||
Skipper Seabold
|
r4432 | def process_image(self, decorator): | ||
""" | ||||
# build out an image directive like | ||||
# .. image:: somefile.png | ||||
# :width 4in | ||||
# | ||||
# from an input like | ||||
# savefig somefile.png width=4in | ||||
""" | ||||
savefig_dir = self.savefig_dir | ||||
source_dir = self.source_dir | ||||
saveargs = decorator.split(' ') | ||||
filename = saveargs[1] | ||||
# insert relative path to image file in source | ||||
outfile = os.path.relpath(os.path.join(savefig_dir,filename), | ||||
source_dir) | ||||
imagerows = ['.. image:: %s'%outfile] | ||||
for kwarg in saveargs[2:]: | ||||
arg, val = kwarg.split('=') | ||||
arg = arg.strip() | ||||
val = val.strip() | ||||
imagerows.append(' :%s: %s'%(arg, val)) | ||||
image_file = os.path.basename(outfile) # only return file name | ||||
image_directive = '\n'.join(imagerows) | ||||
return image_file, image_directive | ||||
Fernando Perez
|
r2439 | # Callbacks for each type of token | ||
def process_input(self, data, input_prompt, lineno): | ||||
"""Process data block for INPUT token.""" | ||||
decorator, input, rest = data | ||||
image_file = None | ||||
Skipper Seabold
|
r4432 | image_directive = None | ||
Fernando Perez
|
r2439 | #print 'INPUT:', data # dbg | ||
is_verbatim = decorator=='@verbatim' or self.is_verbatim | ||||
is_doctest = decorator=='@doctest' or self.is_doctest | ||||
is_suppress = decorator=='@suppress' or self.is_suppress | ||||
is_savefig = decorator is not None and \ | ||||
decorator.startswith('@savefig') | ||||
input_lines = input.split('\n') | ||||
Skipper Seabold
|
r6135 | if len(input_lines) > 1: | ||
if input_lines[-1] != "": | ||||
input_lines.append('') # make sure there's a blank line | ||||
# so splitter buffer gets reset | ||||
Fernando Perez
|
r2439 | |||
continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) | ||||
Nc = len(continuation) | ||||
if is_savefig: | ||||
Skipper Seabold
|
r4432 | image_file, image_directive = self.process_image(decorator) | ||
Fernando Perez
|
r2439 | |||
ret = [] | ||||
is_semicolon = False | ||||
for i, line in enumerate(input_lines): | ||||
if line.endswith(';'): | ||||
is_semicolon = True | ||||
if i==0: | ||||
# process the first input line | ||||
if is_verbatim: | ||||
self.process_input_line('') | ||||
Skipper Seabold
|
r4432 | self.IP.execution_count += 1 # increment it anyway | ||
Fernando Perez
|
r2439 | else: | ||
# only submit the line in non-verbatim mode | ||||
Skipper Seabold
|
r6147 | self.process_input_line(line, store_history=True) | ||
Fernando Perez
|
r2439 | formatted_line = '%s %s'%(input_prompt, line) | ||
else: | ||||
# process a continuation line | ||||
if not is_verbatim: | ||||
Skipper Seabold
|
r6147 | self.process_input_line(line, store_history=True) | ||
Fernando Perez
|
r2439 | |||
formatted_line = '%s %s'%(continuation, line) | ||||
if not is_suppress: | ||||
ret.append(formatted_line) | ||||
Skipper Seabold
|
r6135 | if not is_suppress and len(rest.strip()) and is_verbatim: | ||
# the "rest" is the standard output of the | ||||
# input, which needs to be added in | ||||
# verbatim mode | ||||
ret.append(rest) | ||||
Fernando Perez
|
r2439 | |||
self.cout.seek(0) | ||||
output = self.cout.read() | ||||
if not is_suppress and not is_semicolon: | ||||
ret.append(output) | ||||
Skipper Seabold
|
r6135 | elif is_semicolon: # get spacing right | ||
ret.append('') | ||||
Fernando Perez
|
r2439 | |||
self.cout.truncate(0) | ||||
Skipper Seabold
|
r4432 | return (ret, input_lines, output, is_doctest, image_file, | ||
image_directive) | ||||
Fernando Perez
|
r2439 | #print 'OUTPUT', output # dbg | ||
def process_output(self, data, output_prompt, | ||||
input_lines, output, is_doctest, image_file): | ||||
"""Process data block for OUTPUT token.""" | ||||
if is_doctest: | ||||
submitted = data.strip() | ||||
found = output | ||||
if found is not None: | ||||
found = found.strip() | ||||
Skipper Seabold
|
r4432 | |||
Fernando Perez
|
r2439 | # XXX - fperez: in 0.11, 'output' never comes with the prompt | ||
# in it, just the actual output text. So I think all this code | ||||
# can be nuked... | ||||
Skipper Seabold
|
r4432 | |||
# the above comment does not appear to be accurate... (minrk) | ||||
ind = found.find(output_prompt) | ||||
if ind<0: | ||||
e='output prompt="%s" does not match out line=%s' % \ | ||||
(output_prompt, found) | ||||
raise RuntimeError(e) | ||||
found = found[len(output_prompt):].strip() | ||||
Fernando Perez
|
r2439 | |||
if found!=submitted: | ||||
e = ('doctest failure for input_lines="%s" with ' | ||||
'found_output="%s" and submitted output="%s"' % | ||||
(input_lines, found, submitted) ) | ||||
raise RuntimeError(e) | ||||
#print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted) | ||||
def process_comment(self, data): | ||||
Skipper Seabold
|
r4432 | """Process data fPblock for COMMENT token.""" | ||
Fernando Perez
|
r2439 | if not self.is_suppress: | ||
return [data] | ||||
Skipper Seabold
|
r4432 | def save_image(self, image_file): | ||
""" | ||||
Saves the image file to disk. | ||||
""" | ||||
self.ensure_pyplot() | ||||
command = 'plt.gcf().savefig("%s")'%image_file | ||||
#print 'SAVEFIG', command # dbg | ||||
self.process_input_line('bookmark ipy_thisdir', store_history=False) | ||||
self.process_input_line('cd -b ipy_savedir', store_history=False) | ||||
self.process_input_line(command, store_history=False) | ||||
self.process_input_line('cd -b ipy_thisdir', store_history=False) | ||||
self.process_input_line('bookmark -d ipy_thisdir', store_history=False) | ||||
self.clear_cout() | ||||
Fernando Perez
|
r2439 | def process_block(self, block): | ||
""" | ||||
process block from the block_parser and return a list of processed lines | ||||
""" | ||||
ret = [] | ||||
output = None | ||||
input_lines = None | ||||
Skipper Seabold
|
r4432 | lineno = self.IP.execution_count | ||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | input_prompt = self.promptin%lineno | ||
output_prompt = self.promptout%lineno | ||||
Fernando Perez
|
r2439 | image_file = None | ||
image_directive = None | ||||
Skipper Seabold
|
r4432 | |||
Fernando Perez
|
r2439 | for token, data in block: | ||
if token==COMMENT: | ||||
out_data = self.process_comment(data) | ||||
elif token==INPUT: | ||||
Skipper Seabold
|
r4432 | (out_data, input_lines, output, is_doctest, image_file, | ||
image_directive) = \ | ||||
Fernando Perez
|
r2439 | self.process_input(data, input_prompt, lineno) | ||
elif token==OUTPUT: | ||||
out_data = \ | ||||
self.process_output(data, output_prompt, | ||||
input_lines, output, is_doctest, | ||||
image_file) | ||||
if out_data: | ||||
ret.extend(out_data) | ||||
Skipper Seabold
|
r4432 | # save the image files | ||
Fernando Perez
|
r2439 | if image_file is not None: | ||
Skipper Seabold
|
r4432 | self.save_image(image_file) | ||
Fernando Perez
|
r2439 | return ret, image_directive | ||
def ensure_pyplot(self): | ||||
if self._pyplot_imported: | ||||
return | ||||
Skipper Seabold
|
r4432 | self.process_input_line('import matplotlib.pyplot as plt', | ||
store_history=False) | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | def process_pure_python(self, content): | ||
""" | ||||
content is a list of strings. it is unedited directive conent | ||||
This runs it line by line in the InteractiveShell, prepends | ||||
prompts as needed capturing stderr and stdout, then returns | ||||
the content as a list as if it were ipython code | ||||
""" | ||||
output = [] | ||||
savefig = False # keep up with this to clear figure | ||||
multiline = False # to handle line continuation | ||||
Skipper Seabold
|
r6134 | multiline_start = None | ||
Skipper Seabold
|
r4432 | fmtin = self.promptin | ||
Skipper Seabold
|
r6134 | ct = 0 | ||
Skipper Seabold
|
r4432 | for lineno, line in enumerate(content): | ||
line_stripped = line.strip() | ||||
if not len(line): | ||||
Skipper Seabold
|
r6134 | output.append(line) | ||
Skipper Seabold
|
r4432 | continue | ||
# handle decorators | ||||
if line_stripped.startswith('@'): | ||||
output.extend([line]) | ||||
if 'savefig' in line: | ||||
savefig = True # and need to clear figure | ||||
continue | ||||
# handle comments | ||||
if line_stripped.startswith('#'): | ||||
output.extend([line]) | ||||
continue | ||||
Skipper Seabold
|
r6134 | # deal with lines checking for multiline | ||
continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2)) | ||||
if not multiline: | ||||
modified = u"%s %s" % (fmtin % ct, line_stripped) | ||||
output.append(modified) | ||||
ct += 1 | ||||
try: | ||||
ast.parse(line_stripped) | ||||
output.append(u'') | ||||
except Exception: # on a multiline | ||||
Skipper Seabold
|
r4432 | multiline = True | ||
Skipper Seabold
|
r6134 | multiline_start = lineno | ||
else: # still on a multiline | ||||
modified = u'%s %s' % (continuation, line) | ||||
output.append(modified) | ||||
Ahmet Bakan
|
r10597 | |||
# if the next line is indented, it should be part of multiline | ||||
if len(content) > lineno + 1: | ||||
nextline = content[lineno + 1] | ||||
if len(nextline) - len(nextline.lstrip()) > 3: | ||||
continue | ||||
Skipper Seabold
|
r6134 | try: | ||
mod = ast.parse( | ||||
'\n'.join(content[multiline_start:lineno+1])) | ||||
if isinstance(mod.body[0], ast.FunctionDef): | ||||
# check to see if we have the whole function | ||||
for element in mod.body[0].body: | ||||
if isinstance(element, ast.Return): | ||||
multiline = False | ||||
else: | ||||
output.append(u'') | ||||
multiline = False | ||||
except Exception: | ||||
pass | ||||
if savefig: # clear figure if plotted | ||||
Skipper Seabold
|
r4432 | self.ensure_pyplot() | ||
self.process_input_line('plt.clf()', store_history=False) | ||||
self.clear_cout() | ||||
savefig = False | ||||
return output | ||||
MinRK
|
r13150 | class IPythonDirective(Directive): | ||
Skipper Seabold
|
r4432 | |||
has_content = True | ||||
required_arguments = 0 | ||||
optional_arguments = 4 # python, suppress, verbatim, doctest | ||||
final_argumuent_whitespace = True | ||||
option_spec = { 'python': directives.unchanged, | ||||
'suppress' : directives.flag, | ||||
'verbatim' : directives.flag, | ||||
'doctest' : directives.flag, | ||||
} | ||||
Fernando Perez
|
r2439 | |||
MinRK
|
r13150 | shell = None | ||
Fernando Perez
|
r2439 | |||
Ahmet Bakan
|
r10596 | seen_docs = set() | ||
Skipper Seabold
|
r4432 | def get_config_options(self): | ||
# contains sphinx configuration variables | ||||
config = self.state.document.settings.env.config | ||||
# get config variables to set figure output directory | ||||
confdir = self.state.document.settings.env.app.confdir | ||||
savefig_dir = config.ipython_savefig_dir | ||||
source_dir = os.path.dirname(self.state.document.current_source) | ||||
if savefig_dir is None: | ||||
savefig_dir = config.html_static_path | ||||
if isinstance(savefig_dir, list): | ||||
savefig_dir = savefig_dir[0] # safe to assume only one path? | ||||
savefig_dir = os.path.join(confdir, savefig_dir) | ||||
# get regex and prompt stuff | ||||
rgxin = config.ipython_rgxin | ||||
rgxout = config.ipython_rgxout | ||||
promptin = config.ipython_promptin | ||||
promptout = config.ipython_promptout | ||||
return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout | ||||
def setup(self): | ||||
MinRK
|
r13150 | if self.shell is None: | ||
self.shell = EmbeddedSphinxShell() | ||||
Skipper Seabold
|
r6137 | # reset the execution count if we haven't processed this doc | ||
#NOTE: this may be borked if there are multiple seen_doc tmp files | ||||
#check time stamp? | ||||
Ahmet Bakan
|
r10596 | |||
if not self.state.document.current_source in self.seen_docs: | ||||
Skipper Seabold
|
r6137 | self.shell.IP.history_manager.reset() | ||
self.shell.IP.execution_count = 1 | ||||
Ahmet Bakan
|
r10596 | self.seen_docs.add(self.state.document.current_source) | ||
Skipper Seabold
|
r6137 | |||
Skipper Seabold
|
r4432 | # get config values | ||
(savefig_dir, source_dir, rgxin, | ||||
rgxout, promptin, promptout) = self.get_config_options() | ||||
# and attach to shell so we don't have to pass them around | ||||
self.shell.rgxin = rgxin | ||||
self.shell.rgxout = rgxout | ||||
self.shell.promptin = promptin | ||||
self.shell.promptout = promptout | ||||
self.shell.savefig_dir = savefig_dir | ||||
self.shell.source_dir = source_dir | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | # setup bookmark for saving figures directory | ||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir, | ||
store_history=False) | ||||
self.shell.clear_cout() | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | return rgxin, rgxout, promptin, promptout | ||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | def teardown(self): | ||
# delete last bookmark | ||||
self.shell.process_input_line('bookmark -d ipy_savedir', | ||||
store_history=False) | ||||
self.shell.clear_cout() | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | def run(self): | ||
debug = False | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | #TODO, any reason block_parser can't be a method of embeddable shell | ||
# then we wouldn't have to carry these around | ||||
rgxin, rgxout, promptin, promptout = self.setup() | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | options = self.options | ||
self.shell.is_suppress = 'suppress' in options | ||||
self.shell.is_doctest = 'doctest' in options | ||||
self.shell.is_verbatim = 'verbatim' in options | ||||
Fernando Perez
|
r2439 | |||
Skipper Seabold
|
r4432 | # handle pure python code | ||
if 'python' in self.arguments: | ||||
content = self.content | ||||
self.content = self.shell.process_pure_python(content) | ||||
parts = '\n'.join(self.content).split('\n\n') | ||||
lines = ['.. code-block:: ipython',''] | ||||
figures = [] | ||||
for part in parts: | ||||
block = block_parser(part, rgxin, rgxout, promptin, promptout) | ||||
if len(block): | ||||
rows, figure = self.shell.process_block(block) | ||||
for row in rows: | ||||
lines.extend([' %s'%line for line in row.split('\n')]) | ||||
if figure is not None: | ||||
figures.append(figure) | ||||
#text = '\n'.join(lines) | ||||
#figs = '\n'.join(figures) | ||||
for figure in figures: | ||||
lines.append('') | ||||
lines.extend(figure.split('\n')) | ||||
lines.append('') | ||||
#print lines | ||||
if len(lines)>2: | ||||
if debug: | ||||
Thomas Kluyver
|
r13348 | print('\n'.join(lines)) | ||
Skipper Seabold
|
r4432 | else: #NOTE: this raises some errors, what's it for? | ||
#print 'INSERTING %d lines'%len(lines) | ||||
self.state_machine.insert_input( | ||||
lines, self.state_machine.input_lines.source(0)) | ||||
text = '\n'.join(lines) | ||||
txtnode = nodes.literal_block(text, text) | ||||
txtnode['language'] = 'ipython' | ||||
#imgnode = nodes.image(figs) | ||||
# cleanup | ||||
self.teardown() | ||||
return []#, imgnode] | ||||
Fernando Perez
|
r2439 | |||
# Enable as a proper Sphinx directive | ||||
def setup(app): | ||||
setup.app = app | ||||
MinRK
|
r13150 | app.add_directive('ipython', IPythonDirective) | ||
Skipper Seabold
|
r4432 | app.add_config_value('ipython_savefig_dir', None, True) | ||
app.add_config_value('ipython_rgxin', | ||||
re.compile('In \[(\d+)\]:\s?(.*)\s*'), True) | ||||
app.add_config_value('ipython_rgxout', | ||||
re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True) | ||||
app.add_config_value('ipython_promptin', 'In [%d]:', True) | ||||
app.add_config_value('ipython_promptout', 'Out[%d]:', True) | ||||
Fernando Perez
|
r2439 | |||
# Simple smoke test, needs to be converted to a proper automatic test. | ||||
def test(): | ||||
examples = [ | ||||
r""" | ||||
In [9]: pwd | ||||
Out[9]: '/home/jdhunter/py4science/book' | ||||
In [10]: cd bookdata/ | ||||
/home/jdhunter/py4science/book/bookdata | ||||
In [2]: from pylab import * | ||||
In [2]: ion() | ||||
In [3]: im = imread('stinkbug.png') | ||||
@savefig mystinkbug.png width=4in | ||||
In [4]: imshow(im) | ||||
Out[4]: <matplotlib.image.AxesImage object at 0x39ea850> | ||||
Skipper Seabold
|
r4432 | |||
Fernando Perez
|
r2439 | """, | ||
r""" | ||||
In [1]: x = 'hello world' | ||||
# string methods can be | ||||
# used to alter the string | ||||
@doctest | ||||
In [2]: x.upper() | ||||
Out[2]: 'HELLO WORLD' | ||||
@verbatim | ||||
In [3]: x.st<TAB> | ||||
x.startswith x.strip | ||||
""", | ||||
r""" | ||||
In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\ | ||||
.....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv' | ||||
In [131]: print url.split('&') | ||||
['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv'] | ||||
In [60]: import urllib | ||||
""", | ||||
r"""\ | ||||
In [133]: import numpy.random | ||||
@suppress | ||||
In [134]: numpy.random.seed(2358) | ||||
@doctest | ||||
Skipper Seabold
|
r4432 | In [135]: numpy.random.rand(10,2) | ||
Fernando Perez
|
r2439 | Out[135]: | ||
array([[ 0.64524308, 0.59943846], | ||||
[ 0.47102322, 0.8715456 ], | ||||
[ 0.29370834, 0.74776844], | ||||
[ 0.99539577, 0.1313423 ], | ||||
[ 0.16250302, 0.21103583], | ||||
[ 0.81626524, 0.1312433 ], | ||||
[ 0.67338089, 0.72302393], | ||||
[ 0.7566368 , 0.07033696], | ||||
[ 0.22591016, 0.77731835], | ||||
[ 0.0072729 , 0.34273127]]) | ||||
""", | ||||
r""" | ||||
In [106]: print x | ||||
jdh | ||||
In [109]: for i in range(10): | ||||
.....: print i | ||||
.....: | ||||
.....: | ||||
0 | ||||
1 | ||||
2 | ||||
3 | ||||
4 | ||||
5 | ||||
6 | ||||
7 | ||||
8 | ||||
9 | ||||
""", | ||||
r""" | ||||
In [144]: from pylab import * | ||||
In [145]: ion() | ||||
# use a semicolon to suppress the output | ||||
@savefig test_hist.png width=4in | ||||
In [151]: hist(np.random.randn(10000), 100); | ||||
@savefig test_plot.png width=4in | ||||
In [151]: plot(np.random.randn(10000), 'o'); | ||||
""", | ||||
r""" | ||||
# use a semicolon to suppress the output | ||||
In [151]: plt.clf() | ||||
@savefig plot_simple.png width=4in | ||||
In [151]: plot([1,2,3]) | ||||
@savefig hist_simple.png width=4in | ||||
In [151]: hist(np.random.randn(10000), 100); | ||||
""", | ||||
r""" | ||||
# update the current fig | ||||
In [151]: ylabel('number') | ||||
In [152]: title('normal distribution') | ||||
@savefig hist_with_text.png | ||||
In [153]: grid(True) | ||||
""", | ||||
] | ||||
Skipper Seabold
|
r4432 | # skip local-file depending first example: | ||
examples = examples[1:] | ||||
Fernando Perez
|
r2439 | |||
#ipython_directive.DEBUG = True # dbg | ||||
#options = dict(suppress=True) # dbg | ||||
options = dict() | ||||
for example in examples: | ||||
content = example.split('\n') | ||||
MinRK
|
r13150 | IPythonDirective('debug', arguments=None, options=options, | ||
Fernando Perez
|
r2439 | content=content, lineno=0, | ||
content_offset=None, block_text=None, | ||||
state=None, state_machine=None, | ||||
) | ||||
# Run test suite as a script | ||||
if __name__=='__main__': | ||||
if not os.path.isdir('_static'): | ||||
os.mkdir('_static') | ||||
test() | ||||
Thomas Kluyver
|
r13348 | print('All OK? Check figures in _static/') | ||