##// END OF EJS Templates
Add Sphinx IPython directive....
Fernando Perez -
Show More
This diff has been collapsed as it changes many lines, (605 lines changed) Show them Hide them
@@ -0,0 +1,605 b''
1 # -*- coding: utf-8 -*-
2 """Sphinx directive to support embedded IPython code.
3
4 This directive allows pasting of entire interactive IPython sessions, prompts
5 and all, and their code will actually get re-executed at doc build time, with
6 all prompts renumbered sequentially.
7
8 To enable this directive, simply list it in your Sphinx ``conf.py`` file
9 (making sure the directory where you placed it is visible to sphinx, as is
10 needed for all Sphinx directives).
11
12 By default this directive assumes that your prompts are unchanged IPython ones,
13 but this can be customized. For example, the following code in your Sphinx
14 config file will configure this directive for the following input/output
15 prompts ``Yade [1]:`` and ``-> [1]:``::
16
17 import ipython_directive as id
18 id.rgxin =re.compile(r'(?:In |Yade )\[(\d+)\]:\s?(.*)\s*')
19 id.rgxout=re.compile(r'(?:Out| -> )\[(\d+)\]:\s?(.*)\s*')
20 id.fmtin ='Yade [%d]:'
21 id.fmtout=' -> [%d]:'
22
23 id.rc_override=dict(
24 prompt_in1="Yade [\#]:",
25 prompt_in2=" .\D..",
26 prompt_out=" -> [\#]:"
27 )
28
29 import ipython_console_highlighting as ich
30 ich.IPythonConsoleLexer.input_prompt=
31 re.compile("(Yade \[[0-9]+\]: )|( \.\.\.+:)")
32 ich.IPythonConsoleLexer.output_prompt=
33 re.compile("(( -> )|(Out)\[[0-9]+\]: )|( \.\.\.+:)")
34 ich.IPythonConsoleLexer.continue_prompt=re.compile(" \.\.\.+:")
35
36
37 ToDo
38 ----
39
40 - Turn the ad-hoc test() function into a real test suite.
41 - Break up ipython-specific functionality from matplotlib stuff into better
42 separated code.
43 - Make sure %bookmarks used internally are removed on exit.
44
45
46 Authors
47 -------
48
49 - John D Hunter: orignal author.
50 - Fernando Perez: refactoring, documentation, cleanups.
51 - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generatlizations.
52 """
53
54 #-----------------------------------------------------------------------------
55 # Imports
56 #-----------------------------------------------------------------------------
57
58 # Stdlib
59 import cStringIO
60 import imp
61 import os
62 import re
63 import shutil
64 import sys
65 import warnings
66
67 # To keep compatibility with various python versions
68 try:
69 from hashlib import md5
70 except ImportError:
71 from md5 import md5
72
73 # Third-party
74 import matplotlib
75 import sphinx
76 from docutils.parsers.rst import directives
77
78 matplotlib.use('Agg')
79
80 # Our own
81 import IPython
82 from IPython.Shell import MatplotlibShell
83
84 #-----------------------------------------------------------------------------
85 # Globals
86 #-----------------------------------------------------------------------------
87
88 sphinx_version = sphinx.__version__.split(".")
89 # The split is necessary for sphinx beta versions where the string is
90 # '6b1'
91 sphinx_version = tuple([int(re.split('[a-z]', x)[0])
92 for x in sphinx_version[:2]])
93
94 COMMENT, INPUT, OUTPUT = range(3)
95 rc_override = {}
96 rgxin = re.compile('In \[(\d+)\]:\s?(.*)\s*')
97 rgxout = re.compile('Out\[(\d+)\]:\s?(.*)\s*')
98 fmtin = 'In [%d]:'
99 fmtout = 'Out[%d]:'
100
101 #-----------------------------------------------------------------------------
102 # Functions and class declarations
103 #-----------------------------------------------------------------------------
104 def block_parser(part):
105 """
106 part is a string of ipython text, comprised of at most one
107 input, one ouput, comments, and blank lines. The block parser
108 parses the text into a list of::
109
110 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
111
112 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
113 data is, depending on the type of token::
114
115 COMMENT : the comment string
116
117 INPUT: the (DECORATOR, INPUT_LINE, REST) where
118 DECORATOR: the input decorator (or None)
119 INPUT_LINE: the input as string (possibly multi-line)
120 REST : any stdout generated by the input line (not OUTPUT)
121
122
123 OUTPUT: the output string, possibly multi-line
124 """
125
126 block = []
127 lines = part.split('\n')
128 N = len(lines)
129 i = 0
130 decorator = None
131 while 1:
132
133 if i==N:
134 # nothing left to parse -- the last line
135 break
136
137 line = lines[i]
138 i += 1
139 line_stripped = line.strip()
140 if line_stripped.startswith('#'):
141 block.append((COMMENT, line))
142 continue
143
144 if line_stripped.startswith('@'):
145 # we're assuming at most one decorator -- may need to
146 # rethink
147 decorator = line_stripped
148 continue
149
150 # does this look like an input line?
151 matchin = rgxin.match(line)
152 if matchin:
153 lineno, inputline = int(matchin.group(1)), matchin.group(2)
154
155 # the ....: continuation string
156 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
157 Nc = len(continuation)
158 # input lines can continue on for more than one line, if
159 # we have a '\' line continuation char or a function call
160 # echo line 'print'. The input line can only be
161 # terminated by the end of the block or an output line, so
162 # we parse out the rest of the input line if it is
163 # multiline as well as any echo text
164
165 rest = []
166 while i<N:
167
168 # look ahead; if the next line is blank, or a comment, or
169 # an output line, we're done
170
171 nextline = lines[i]
172 matchout = rgxout.match(nextline)
173 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
174 if matchout or nextline.startswith('#'):
175 break
176 elif nextline.startswith(continuation):
177 inputline += '\n' + nextline[Nc:]
178 else:
179 rest.append(nextline)
180 i+= 1
181
182 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
183 continue
184
185 # if it looks like an output line grab all the text to the end
186 # of the block
187 matchout = rgxout.match(line)
188 if matchout:
189 lineno, output = int(matchout.group(1)), matchout.group(2)
190 if i<N-1:
191 output = '\n'.join([output] + lines[i:])
192
193 block.append((OUTPUT, output))
194 break
195
196 return block
197
198
199 class EmbeddedSphinxShell(object):
200 """An embedded IPython instance to run inside Sphinx"""
201
202 def __init__(self):
203
204 self.cout = cStringIO.StringIO()
205
206 IPython.Shell.Term.cout = self.cout
207 IPython.Shell.Term.cerr = self.cout
208 argv = ['-autocall', '0']
209 self.user_ns = {}
210 self.user_glocal_ns = {}
211
212 self.IP = IPython.ipmaker.make_IPython(
213 argv, self.user_ns, self.user_glocal_ns, embedded=True,
214 #shell_class=IPython.Shell.InteractiveShell,
215 shell_class=MatplotlibShell,
216 rc_override = dict(colors = 'NoColor'), **rc_override)
217
218 self.input = ''
219 self.output = ''
220
221 self.is_verbatim = False
222 self.is_doctest = False
223 self.is_suppress = False
224
225 # on the first call to the savefig decorator, we'll import
226 # pyplot as plt so we can make a call to the plt.gcf().savefig
227 self._pyplot_imported = False
228
229 # we need bookmark the current dir first so we can save
230 # relative to it
231 self.process_input_line('bookmark ipy_basedir')
232 self.cout.seek(0)
233 self.cout.truncate(0)
234
235 def process_input_line(self, line):
236 """process the input, capturing stdout"""
237 #print "input='%s'"%self.input
238 stdout = sys.stdout
239 sys.stdout = self.cout
240 #self.IP.resetbuffer()
241 self.IP.push(self.IP.prefilter(line, 0))
242 #self.IP.runlines(line)
243 sys.stdout = stdout
244
245 # Callbacks for each type of token
246 def process_input(self, data, input_prompt, lineno):
247 """Process data block for INPUT token."""
248 decorator, input, rest = data
249 image_file = None
250 #print 'INPUT:', data
251 is_verbatim = decorator=='@verbatim' or self.is_verbatim
252 is_doctest = decorator=='@doctest' or self.is_doctest
253 is_suppress = decorator=='@suppress' or self.is_suppress
254 is_savefig = decorator is not None and \
255 decorator.startswith('@savefig')
256
257 input_lines = input.split('\n')
258
259 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
260 Nc = len(continuation)
261
262 if is_savefig:
263 saveargs = decorator.split(' ')
264 filename = saveargs[1]
265 outfile = os.path.join('_static/%s'%filename)
266 # build out an image directive like
267 # .. image:: somefile.png
268 # :width 4in
269 #
270 # from an input like
271 # savefig somefile.png width=4in
272 imagerows = ['.. image:: %s'%outfile]
273
274 for kwarg in saveargs[2:]:
275 arg, val = kwarg.split('=')
276 arg = arg.strip()
277 val = val.strip()
278 imagerows.append(' :%s: %s'%(arg, val))
279
280 image_file = outfile
281 image_directive = '\n'.join(imagerows)
282
283 # TODO: can we get "rest" from ipython
284 #self.process_input_line('\n'.join(input_lines))
285
286 ret = []
287 is_semicolon = False
288
289 for i, line in enumerate(input_lines):
290 if line.endswith(';'):
291 is_semicolon = True
292
293 if i==0:
294 # process the first input line
295 if is_verbatim:
296 self.process_input_line('')
297 else:
298 # only submit the line in non-verbatim mode
299 self.process_input_line(line)
300 formatted_line = '%s %s'%(input_prompt, line)
301 else:
302 # process a continuation line
303 if not is_verbatim:
304 self.process_input_line(line)
305
306 formatted_line = '%s %s'%(continuation, line)
307
308 if not is_suppress:
309 ret.append(formatted_line)
310
311 if not is_suppress:
312 if len(rest.strip()):
313 if is_verbatim:
314 # the "rest" is the standard output of the
315 # input, which needs to be added in
316 # verbatim mode
317 ret.append(rest)
318
319 self.cout.seek(0)
320 output = self.cout.read()
321 if not is_suppress and not is_semicolon:
322 ret.append(output)
323
324 self.cout.truncate(0)
325 return ret, input_lines, output, is_doctest, image_file
326 #print 'OUTPUT', output # dbg
327
328 def process_output(self, data, output_prompt,
329 input_lines, output, is_doctest, image_file):
330 """Process data block for OUTPUT token."""
331 if is_doctest:
332 submitted = data.strip()
333 found = output
334 if found is not None:
335 ind = found.find(output_prompt)
336 if ind<0:
337 raise RuntimeError('output prompt="%s" does not match out line=%s'%(output_prompt, found))
338 found = found[len(output_prompt):].strip()
339
340 if found!=submitted:
341 raise RuntimeError('doctest failure for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted))
342 #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted)
343
344 def process_comment(self, data):
345 """Process data block for COMMENT token."""
346 if not self.is_suppress:
347 return [data]
348
349 def process_block(self, block):
350 """
351 process block from the block_parser and return a list of processed lines
352 """
353
354 ret = []
355 output = None
356 input_lines = None
357
358 m = rgxin.match(str(self.IP.outputcache.prompt1).strip())
359 lineno = int(m.group(1))
360
361 input_prompt = fmtin%lineno
362 output_prompt = fmtout%lineno
363 image_file = None
364 image_directive = None
365 # XXX - This needs a second refactor. There's too much state being
366 # held globally, which makes for a very awkward interface and large,
367 # hard to test functions. I've already broken this up at least into
368 # three separate processors to isolate the logic better, but this only
369 # serves to highlight the coupling. Next we need to clean it up...
370 for token, data in block:
371 if token==COMMENT:
372 out_data = self.process_comment(data)
373 elif token==INPUT:
374 out_data, input_lines, output, is_doctest, image_file= \
375 self.process_input(data, input_prompt, lineno)
376 elif token==OUTPUT:
377 out_data = \
378 self.process_output(data, output_prompt,
379 input_lines, output, is_doctest,
380 image_file)
381 if out_data:
382 ret.extend(out_data)
383
384 if image_file is not None:
385 self.ensure_pyplot()
386 command = 'plt.gcf().savefig("%s")'%image_file
387 #print 'SAVEFIG', command # dbg
388 self.process_input_line('bookmark ipy_thisdir')
389 self.process_input_line('cd -b ipy_basedir')
390 self.process_input_line(command)
391 self.process_input_line('cd -b ipy_thisdir')
392 self.cout.seek(0)
393 self.cout.truncate(0)
394 return ret, image_directive
395
396 def ensure_pyplot(self):
397 if self._pyplot_imported:
398 return
399 self.process_input_line('import matplotlib.pyplot as plt')
400
401 # A global instance used below. XXX: not sure why this can't be created inside
402 # ipython_directive itself.
403 shell = EmbeddedSphinxShell()
404
405
406 def ipython_directive(name, arguments, options, content, lineno,
407 content_offset, block_text, state, state_machine,
408 ):
409
410 debug = ipython_directive.DEBUG
411 shell.is_suppress = options.has_key('suppress')
412 shell.is_doctest = options.has_key('doctest')
413 shell.is_verbatim = options.has_key('verbatim')
414
415 #print 'ipy', shell.is_suppress, options
416 parts = '\n'.join(content).split('\n\n')
417 lines = ['.. sourcecode:: ipython', '']
418
419 figures = []
420 for part in parts:
421 block = block_parser(part)
422
423 if len(block):
424 rows, figure = shell.process_block(block)
425 for row in rows:
426 lines.extend([' %s'%line for line in row.split('\n')])
427
428 if figure is not None:
429 figures.append(figure)
430
431 for figure in figures:
432 lines.append('')
433 lines.extend(figure.split('\n'))
434 lines.append('')
435
436 #print lines
437 if len(lines)>2:
438 if debug:
439 print '\n'.join(lines)
440 else:
441 #print 'INSERTING %d lines'%len(lines)
442 state_machine.insert_input(
443 lines, state_machine.input_lines.source(0))
444
445 return []
446
447 ipython_directive.DEBUG = False
448
449 # Enable as a proper Sphinx directive
450 def setup(app):
451 setup.app = app
452 options = {'suppress': directives.flag,
453 'doctest': directives.flag,
454 'verbatim': directives.flag,
455 }
456
457 app.add_directive('ipython', ipython_directive, True, (0, 2, 0), **options)
458
459
460 # Simple smoke test, needs to be converted to a proper automatic test.
461 def test():
462
463 examples = [
464 r"""
465 In [9]: pwd
466 Out[9]: '/home/jdhunter/py4science/book'
467
468 In [10]: cd bookdata/
469 /home/jdhunter/py4science/book/bookdata
470
471 In [2]: from pylab import *
472
473 In [2]: ion()
474
475 In [3]: im = imread('stinkbug.png')
476
477 @savefig mystinkbug.png width=4in
478 In [4]: imshow(im)
479 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
480
481 """,
482 r"""
483
484 In [1]: x = 'hello world'
485
486 # string methods can be
487 # used to alter the string
488 @doctest
489 In [2]: x.upper()
490 Out[2]: 'HELLO WORLD'
491
492 @verbatim
493 In [3]: x.st<TAB>
494 x.startswith x.strip
495 """,
496 r"""
497
498 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
499 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
500
501 In [131]: print url.split('&')
502 ['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']
503
504 In [60]: import urllib
505
506 """,
507 r"""\
508
509 In [133]: import numpy.random
510
511 @suppress
512 In [134]: numpy.random.seed(2358)
513
514 @doctest
515 In [135]: np.random.rand(10,2)
516 Out[135]:
517 array([[ 0.64524308, 0.59943846],
518 [ 0.47102322, 0.8715456 ],
519 [ 0.29370834, 0.74776844],
520 [ 0.99539577, 0.1313423 ],
521 [ 0.16250302, 0.21103583],
522 [ 0.81626524, 0.1312433 ],
523 [ 0.67338089, 0.72302393],
524 [ 0.7566368 , 0.07033696],
525 [ 0.22591016, 0.77731835],
526 [ 0.0072729 , 0.34273127]])
527
528 """,
529
530 r"""
531 In [106]: print x
532 jdh
533
534 In [109]: for i in range(10):
535 .....: print i
536 .....:
537 .....:
538 0
539 1
540 2
541 3
542 4
543 5
544 6
545 7
546 8
547 9
548
549
550 """,
551
552 r"""
553
554 In [144]: from pylab import *
555
556 In [145]: ion()
557
558 # use a semicolon to suppress the output
559 @savefig test_hist.png width=4in
560 In [151]: hist(np.random.randn(10000), 100);
561
562
563 @savefig test_plot.png width=4in
564 In [151]: plot(np.random.randn(10000), 'o');
565 """,
566
567 r"""
568 # use a semicolon to suppress the output
569 In [151]: plt.clf()
570
571 @savefig plot_simple.png width=4in
572 In [151]: plot([1,2,3])
573
574 @savefig hist_simple.png width=4in
575 In [151]: hist(np.random.randn(10000), 100);
576
577 """,
578 r"""
579 # update the current fig
580 In [151]: ylabel('number')
581
582 In [152]: title('normal distribution')
583
584
585 @savefig hist_with_text.png
586 In [153]: grid(True)
587
588 """,
589 ]
590
591
592 ipython_directive.DEBUG = True
593 #options = dict(suppress=True)
594 options = dict()
595 for example in examples:
596 content = example.split('\n')
597 ipython_directive('debug', arguments=None, options=options,
598 content=content, lineno=0,
599 content_offset=None, block_text=None,
600 state=None, state_machine=None,
601 )
602
603 # Run test suite as a script
604 if __name__=='__main__':
605 test()
General Comments 0
You need to be logged in to leave comments. Login now