[IPython-dev] ipython sphinx directive

John Hunter jdh2358 at gmail.com
Fri Nov 6 17:24:05 EST 2009


I have been working on a chapter in sphinx, and make liberal use of
the ipython sourecode lexer and matplotlib plot directive Michael D
wrote, with contributions from others.  I am writing in type-along
fashion, so I do a lot of this::

    .. sourcecode:: ipython

       In [193]: X = imread('moonlanding.png')

       In [195]: imshow(X, cmap='gray')
       Out[195]: <matplotlib.image.AxesImage object at 0x97d1acc>


    .. plot::

       import matplotlib.image as mimage
       import matplotlib.pyplot as plt
       X = mimage.imread('moonlanding.png')

       fig = plt.figure()
       ax = fig.add_subplot(111)
       ax.imshow(X, cmap='gray')
       plt.show()

This works great, but as I continue my example, adding new lines of
ipython, I have to always cut and paste the *full* plot code as the
figure is updated, which is tedious and difficult to maintain.  What I
really want is an ipython sphinx mode, preferably one that is pylab
aware, so all I have to do is

    .. ipython_pylab::

           X = imread('moonlanding.png')
           imshow(X, cmap='gray')

and sphinx will pass this to an embedded ipython session, render the
input and output prompts, and insert any figures that are created.
Then I can update the figure in the next block

    .. ipython_pylab::

           clim(0, 300)
           hot()

and get the updated figure in my docs....

I have taken a crack at this in the "ipython_directive" code below.  I
haven't yet tacked the pylab/figure part, being an ipython newbie, and
have just done the basic ipython part.  We have already cracked the
nut of managing pylab figures in the plot directive, so while this
won't be trivial it's not the hardest part I don't think.  What I want
to focus on here is the ipython part.

Since I suspect this will be of interest to a lot of ipython/sphinx
users, I want to get the interface right.  For example, there is a
good argument to allow the sphinx writer to pass complete ipython
blocks in rather than plain python blocks, so the reader of the plain
unrendered rest document would see both the input and output.  The
ipython directive would in this case just grab the input blocks, strip
off the prompts, execute the code, and (possibly) replace the inputs
and outputs in the rendered docs which would provide auto-renumbering.
 There are a lot of things to balance here -- auto-renumbering is nice
but it makes it hard for the doc-writer to refer to specific line
numbers....

For the simplest case, I just handle plain python inputs, and generate
rendered ipython sessions.  Eg, the following sphinx doc::

  =================
  Ipython Directive
  =================

  The ipython directive is a stateful ipython shell for embedding in
  sphinx documents.

  .. ipython::

     x = 2
     x**3

  The state from previous sessions is stored, and standard error is
  trapped

  .. ipython::

     z = x*3   # x is recalled from previous block
     z
     print z   # this doesn't produce output -- how do I capture this?
     q = z[)   # this is a syntax error -- we trap ipy exceptions

  That's it -- next step is supporting pylab with embedded figs!


The sphinx ipython_directive and rendered html are included below and
attached.  For a working directory with everything ready to build for
testing, you can grab:  http://matplotlib.sf.net/ipymode.zip

Suggestions and improvements welcome --  in addition to input on the
interface,  I could use some help from the ipython devs on on how to
configure my Shell to support magics like 'ls' or 'pwd' and 'cd', how
to fix the "print" problem above, integrating pylab plots...

JDH


import sys, os, shutil, imp, warnings, cStringIO, re
try:
    from hashlib import md5
except ImportError:
    from md5 import md5

from docutils.parsers.rst import directives
try:
    # docutils 0.4
    from docutils.parsers.rst.directives.images import align
except ImportError:
    # docutils 0.5
    from docutils.parsers.rst.directives.images import Image
    align = Image.align
import sphinx


sphinx_version = sphinx.__version__.split(".")
# The split is necessary for sphinx beta versions where the string is
# '6b1'
sphinx_version = tuple([int(re.split('[a-z]', x)[0])
                        for x in sphinx_version[:2]])


import IPython



class EmbeddedSphinxShell:
    def __init__(self):

        self.cout = cStringIO.StringIO()

        IPython.Shell.Term.cout = self.cout
        IPython.Shell.Term.cerr = self.cout
        argv = []
        self.user_ns = {}
        self.user_glocal_ns = {}

        self.IP = IPython.ipmaker.make_IPython(
            argv, self.user_ns, self.user_glocal_ns, embedded=True,
            shell_class=IPython.Shell.InteractiveShell,
            rc_override=dict(colors = 'NoColor'))

    def process(self, line):

        self.cout.write('%s%s\n'%(self.IP.outputcache.prompt1, line))
        self.IP.push(line)
        if (self.IP.SyntaxTB.last_syntax_error and
            self.IP.rc.autoedit_syntax):
            print 'OOPS'
            self.IP.edit_syntax_error()


    def astext(self):
        self.cout.seek(0)
        s = self.cout.read()
        self.cout.truncate(0)
        return s



shell = EmbeddedSphinxShell()


def ipython_directive(name, arguments, options, content, lineno,
                   content_offset, block_text, state, state_machine):

    for line in content:
        shell.process(line)

    lines = shell.astext().split('\n')
    if len(lines):
        ipy_lines = ['.. sourcecode:: ipython', '']
        ipy_lines.extend(['    %s'%line for line in lines])
        ipy_lines.append('')

        state_machine.insert_input(
            ipy_lines, state_machine.input_lines.source(0))

def setup(app):
    setup.app = app
    options = {}
    app.add_directive('ipython', ipython_directive, True, (0, 2, 0), **options)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/ipython-dev/attachments/20091106/b7169893/attachment.html>


More information about the IPython-dev mailing list