##// END OF EJS Templates
Make sure splitter._buffer is reset. Handle suppressed output better
Skipper Seabold -
Show More
@@ -1,808 +1,812 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Sphinx directive to support embedded IPython code.
2 """Sphinx directive to support embedded IPython code.
3
3
4 This directive allows pasting of entire interactive IPython sessions, prompts
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
5 and all, and their code will actually get re-executed at doc build time, with
6 all prompts renumbered sequentially. It also allows you to input code as a pure
6 all prompts renumbered sequentially. It also allows you to input code as a pure
7 python input by giving the argument python to the directive. The output looks
7 python input by giving the argument python to the directive. The output looks
8 like an interactive ipython section.
8 like an interactive ipython section.
9
9
10 To enable this directive, simply list it in your Sphinx ``conf.py`` file
10 To enable this directive, simply list it in your Sphinx ``conf.py`` file
11 (making sure the directory where you placed it is visible to sphinx, as is
11 (making sure the directory where you placed it is visible to sphinx, as is
12 needed for all Sphinx directives).
12 needed for all Sphinx directives).
13
13
14 By default this directive assumes that your prompts are unchanged IPython ones,
14 By default this directive assumes that your prompts are unchanged IPython ones,
15 but this can be customized. The configurable options that can be placed in
15 but this can be customized. The configurable options that can be placed in
16 conf.py are
16 conf.py are
17
17
18 ipython_savefig_dir:
18 ipython_savefig_dir:
19 The directory in which to save the figures. This is relative to the
19 The directory in which to save the figures. This is relative to the
20 Sphinx source directory. The default is `html_static_path`.
20 Sphinx source directory. The default is `html_static_path`.
21 ipython_rgxin:
21 ipython_rgxin:
22 The compiled regular expression to denote the start of IPython input
22 The compiled regular expression to denote the start of IPython input
23 lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You
23 lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You
24 shouldn't need to change this.
24 shouldn't need to change this.
25 ipython_rgxout:
25 ipython_rgxout:
26 The compiled regular expression to denote the start of IPython output
26 The compiled regular expression to denote the start of IPython output
27 lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You
27 lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You
28 shouldn't need to change this.
28 shouldn't need to change this.
29 ipython_promptin:
29 ipython_promptin:
30 The string to represent the IPython input prompt in the generated ReST.
30 The string to represent the IPython input prompt in the generated ReST.
31 The default is 'In [%d]:'. This expects that the line numbers are used
31 The default is 'In [%d]:'. This expects that the line numbers are used
32 in the prompt.
32 in the prompt.
33 ipython_promptout:
33 ipython_promptout:
34
34
35 The string to represent the IPython prompt in the generated ReST. The
35 The string to represent the IPython prompt in the generated ReST. The
36 default is 'Out [%d]:'. This expects that the line numbers are used
36 default is 'Out [%d]:'. This expects that the line numbers are used
37 in the prompt.
37 in the prompt.
38
38
39 ToDo
39 ToDo
40 ----
40 ----
41
41
42 - Turn the ad-hoc test() function into a real test suite.
42 - Turn the ad-hoc test() function into a real test suite.
43 - Break up ipython-specific functionality from matplotlib stuff into better
43 - Break up ipython-specific functionality from matplotlib stuff into better
44 separated code.
44 separated code.
45
45
46 Authors
46 Authors
47 -------
47 -------
48
48
49 - John D Hunter: orignal author.
49 - John D Hunter: orignal author.
50 - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
50 - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
51 - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
51 - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
52 - Skipper Seabold, refactoring, cleanups, pure python addition
52 - Skipper Seabold, refactoring, cleanups, pure python addition
53 """
53 """
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Imports
56 # Imports
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59 # Stdlib
59 # Stdlib
60 import cStringIO
60 import cStringIO
61 import os
61 import os
62 import re
62 import re
63 import sys
63 import sys
64 import tempfile
64 import tempfile
65
65
66 # To keep compatibility with various python versions
66 # To keep compatibility with various python versions
67 try:
67 try:
68 from hashlib import md5
68 from hashlib import md5
69 except ImportError:
69 except ImportError:
70 from md5 import md5
70 from md5 import md5
71
71
72 # Third-party
72 # Third-party
73 import matplotlib
73 import matplotlib
74 import sphinx
74 import sphinx
75 from docutils.parsers.rst import directives
75 from docutils.parsers.rst import directives
76 from docutils import nodes
76 from docutils import nodes
77 from sphinx.util.compat import Directive
77 from sphinx.util.compat import Directive
78
78
79 matplotlib.use('Agg')
79 matplotlib.use('Agg')
80
80
81 # Our own
81 # Our own
82 from IPython import Config, InteractiveShell
82 from IPython import Config, InteractiveShell
83 from IPython.core.profiledir import ProfileDir
83 from IPython.core.profiledir import ProfileDir
84 from IPython.utils import io
84 from IPython.utils import io
85
85
86 #-----------------------------------------------------------------------------
86 #-----------------------------------------------------------------------------
87 # Globals
87 # Globals
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # for tokenizing blocks
89 # for tokenizing blocks
90 COMMENT, INPUT, OUTPUT = range(3)
90 COMMENT, INPUT, OUTPUT = range(3)
91
91
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93 # Functions and class declarations
93 # Functions and class declarations
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
95 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
96 """
96 """
97 part is a string of ipython text, comprised of at most one
97 part is a string of ipython text, comprised of at most one
98 input, one ouput, comments, and blank lines. The block parser
98 input, one ouput, comments, and blank lines. The block parser
99 parses the text into a list of::
99 parses the text into a list of::
100
100
101 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
101 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
102
102
103 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
103 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
104 data is, depending on the type of token::
104 data is, depending on the type of token::
105
105
106 COMMENT : the comment string
106 COMMENT : the comment string
107
107
108 INPUT: the (DECORATOR, INPUT_LINE, REST) where
108 INPUT: the (DECORATOR, INPUT_LINE, REST) where
109 DECORATOR: the input decorator (or None)
109 DECORATOR: the input decorator (or None)
110 INPUT_LINE: the input as string (possibly multi-line)
110 INPUT_LINE: the input as string (possibly multi-line)
111 REST : any stdout generated by the input line (not OUTPUT)
111 REST : any stdout generated by the input line (not OUTPUT)
112
112
113
113
114 OUTPUT: the output string, possibly multi-line
114 OUTPUT: the output string, possibly multi-line
115 """
115 """
116
116
117 block = []
117 block = []
118 lines = part.split('\n')
118 lines = part.split('\n')
119 N = len(lines)
119 N = len(lines)
120 i = 0
120 i = 0
121 decorator = None
121 decorator = None
122 while 1:
122 while 1:
123
123
124 if i==N:
124 if i==N:
125 # nothing left to parse -- the last line
125 # nothing left to parse -- the last line
126 break
126 break
127
127
128 line = lines[i]
128 line = lines[i]
129 i += 1
129 i += 1
130 line_stripped = line.strip()
130 line_stripped = line.strip()
131 if line_stripped.startswith('#'):
131 if line_stripped.startswith('#'):
132 block.append((COMMENT, line))
132 block.append((COMMENT, line))
133 continue
133 continue
134
134
135 if line_stripped.startswith('@'):
135 if line_stripped.startswith('@'):
136 # we're assuming at most one decorator -- may need to
136 # we're assuming at most one decorator -- may need to
137 # rethink
137 # rethink
138 decorator = line_stripped
138 decorator = line_stripped
139 continue
139 continue
140
140
141 # does this look like an input line?
141 # does this look like an input line?
142 matchin = rgxin.match(line)
142 matchin = rgxin.match(line)
143 if matchin:
143 if matchin:
144 lineno, inputline = int(matchin.group(1)), matchin.group(2)
144 lineno, inputline = int(matchin.group(1)), matchin.group(2)
145
145
146 # the ....: continuation string
146 # the ....: continuation string
147 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
147 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
148 Nc = len(continuation)
148 Nc = len(continuation)
149 # input lines can continue on for more than one line, if
149 # input lines can continue on for more than one line, if
150 # we have a '\' line continuation char or a function call
150 # we have a '\' line continuation char or a function call
151 # echo line 'print'. The input line can only be
151 # echo line 'print'. The input line can only be
152 # terminated by the end of the block or an output line, so
152 # terminated by the end of the block or an output line, so
153 # we parse out the rest of the input line if it is
153 # we parse out the rest of the input line if it is
154 # multiline as well as any echo text
154 # multiline as well as any echo text
155
155
156 rest = []
156 rest = []
157 while i<N:
157 while i<N:
158
158
159 # look ahead; if the next line is blank, or a comment, or
159 # look ahead; if the next line is blank, or a comment, or
160 # an output line, we're done
160 # an output line, we're done
161
161
162 nextline = lines[i]
162 nextline = lines[i]
163 matchout = rgxout.match(nextline)
163 matchout = rgxout.match(nextline)
164 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
164 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
165 if matchout or nextline.startswith('#'):
165 if matchout or nextline.startswith('#'):
166 break
166 break
167 elif nextline.startswith(continuation):
167 elif nextline.startswith(continuation):
168 inputline += '\n' + nextline[Nc:]
168 inputline += '\n' + nextline[Nc:]
169 else:
169 else:
170 rest.append(nextline)
170 rest.append(nextline)
171 i+= 1
171 i+= 1
172
172
173 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
173 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
174 continue
174 continue
175
175
176 # if it looks like an output line grab all the text to the end
176 # if it looks like an output line grab all the text to the end
177 # of the block
177 # of the block
178 matchout = rgxout.match(line)
178 matchout = rgxout.match(line)
179 if matchout:
179 if matchout:
180 lineno, output = int(matchout.group(1)), matchout.group(2)
180 lineno, output = int(matchout.group(1)), matchout.group(2)
181 if i<N-1:
181 if i<N-1:
182 output = '\n'.join([output] + lines[i:])
182 output = '\n'.join([output] + lines[i:])
183
183
184 block.append((OUTPUT, output))
184 block.append((OUTPUT, output))
185 break
185 break
186
186
187 return block
187 return block
188
188
189 class EmbeddedSphinxShell(object):
189 class EmbeddedSphinxShell(object):
190 """An embedded IPython instance to run inside Sphinx"""
190 """An embedded IPython instance to run inside Sphinx"""
191
191
192 def __init__(self):
192 def __init__(self):
193
193
194 self.cout = cStringIO.StringIO()
194 self.cout = cStringIO.StringIO()
195
195
196
196
197 # Create config object for IPython
197 # Create config object for IPython
198 config = Config()
198 config = Config()
199 config.Global.display_banner = False
199 config.Global.display_banner = False
200 config.Global.exec_lines = ['import numpy as np',
200 config.Global.exec_lines = ['import numpy as np',
201 'from pylab import *'
201 'from pylab import *'
202 ]
202 ]
203 config.InteractiveShell.autocall = False
203 config.InteractiveShell.autocall = False
204 config.InteractiveShell.autoindent = False
204 config.InteractiveShell.autoindent = False
205 config.InteractiveShell.colors = 'NoColor'
205 config.InteractiveShell.colors = 'NoColor'
206
206
207 # create a profile so instance history isn't saved
207 # create a profile so instance history isn't saved
208 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
208 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
209 profname = 'auto_profile_sphinx_build'
209 profname = 'auto_profile_sphinx_build'
210 pdir = os.path.join(tmp_profile_dir,profname)
210 pdir = os.path.join(tmp_profile_dir,profname)
211 profile = ProfileDir.create_profile_dir(pdir)
211 profile = ProfileDir.create_profile_dir(pdir)
212
212
213 # Create and initialize ipython, but don't start its mainloop
213 # Create and initialize ipython, but don't start its mainloop
214 IP = InteractiveShell.instance(config=config, profile_dir=profile)
214 IP = InteractiveShell.instance(config=config, profile_dir=profile)
215 # io.stdout redirect must be done *after* instantiating InteractiveShell
215 # io.stdout redirect must be done *after* instantiating InteractiveShell
216 io.stdout = self.cout
216 io.stdout = self.cout
217 io.stderr = self.cout
217 io.stderr = self.cout
218
218
219 # For debugging, so we can see normal output, use this:
219 # For debugging, so we can see normal output, use this:
220 #from IPython.utils.io import Tee
220 #from IPython.utils.io import Tee
221 #io.stdout = Tee(self.cout, channel='stdout') # dbg
221 #io.stdout = Tee(self.cout, channel='stdout') # dbg
222 #io.stderr = Tee(self.cout, channel='stderr') # dbg
222 #io.stderr = Tee(self.cout, channel='stderr') # dbg
223
223
224 # Store a few parts of IPython we'll need.
224 # Store a few parts of IPython we'll need.
225 self.IP = IP
225 self.IP = IP
226 self.user_ns = self.IP.user_ns
226 self.user_ns = self.IP.user_ns
227 self.user_global_ns = self.IP.user_global_ns
227 self.user_global_ns = self.IP.user_global_ns
228
228
229 self.input = ''
229 self.input = ''
230 self.output = ''
230 self.output = ''
231
231
232 self.is_verbatim = False
232 self.is_verbatim = False
233 self.is_doctest = False
233 self.is_doctest = False
234 self.is_suppress = False
234 self.is_suppress = False
235
235
236 # on the first call to the savefig decorator, we'll import
236 # on the first call to the savefig decorator, we'll import
237 # pyplot as plt so we can make a call to the plt.gcf().savefig
237 # pyplot as plt so we can make a call to the plt.gcf().savefig
238 self._pyplot_imported = False
238 self._pyplot_imported = False
239
239
240 def clear_cout(self):
240 def clear_cout(self):
241 self.cout.seek(0)
241 self.cout.seek(0)
242 self.cout.truncate(0)
242 self.cout.truncate(0)
243
243
244 def process_input_line(self, line, store_history=True):
244 def process_input_line(self, line, store_history=True):
245 """process the input, capturing stdout"""
245 """process the input, capturing stdout"""
246 #print "input='%s'"%self.input
246 #print "input='%s'"%self.input
247 stdout = sys.stdout
247 stdout = sys.stdout
248 splitter = self.IP.input_splitter
248 splitter = self.IP.input_splitter
249 try:
249 try:
250 sys.stdout = self.cout
250 sys.stdout = self.cout
251 splitter.push(line)
251 splitter.push(line)
252 more = splitter.push_accepts_more()
252 more = splitter.push_accepts_more()
253 if not more:
253 if not more:
254 source_raw = splitter.source_raw_reset()[1]
254 source_raw = splitter.source_raw_reset()[1]
255 self.IP.run_cell(source_raw, store_history=store_history)
255 self.IP.run_cell(source_raw, store_history=store_history)
256 finally:
256 finally:
257 sys.stdout = stdout
257 sys.stdout = stdout
258
258
259 def process_image(self, decorator):
259 def process_image(self, decorator):
260 """
260 """
261 # build out an image directive like
261 # build out an image directive like
262 # .. image:: somefile.png
262 # .. image:: somefile.png
263 # :width 4in
263 # :width 4in
264 #
264 #
265 # from an input like
265 # from an input like
266 # savefig somefile.png width=4in
266 # savefig somefile.png width=4in
267 """
267 """
268 savefig_dir = self.savefig_dir
268 savefig_dir = self.savefig_dir
269 source_dir = self.source_dir
269 source_dir = self.source_dir
270 saveargs = decorator.split(' ')
270 saveargs = decorator.split(' ')
271 filename = saveargs[1]
271 filename = saveargs[1]
272 # insert relative path to image file in source
272 # insert relative path to image file in source
273 outfile = os.path.relpath(os.path.join(savefig_dir,filename),
273 outfile = os.path.relpath(os.path.join(savefig_dir,filename),
274 source_dir)
274 source_dir)
275
275
276 imagerows = ['.. image:: %s'%outfile]
276 imagerows = ['.. image:: %s'%outfile]
277
277
278 for kwarg in saveargs[2:]:
278 for kwarg in saveargs[2:]:
279 arg, val = kwarg.split('=')
279 arg, val = kwarg.split('=')
280 arg = arg.strip()
280 arg = arg.strip()
281 val = val.strip()
281 val = val.strip()
282 imagerows.append(' :%s: %s'%(arg, val))
282 imagerows.append(' :%s: %s'%(arg, val))
283
283
284 image_file = os.path.basename(outfile) # only return file name
284 image_file = os.path.basename(outfile) # only return file name
285 image_directive = '\n'.join(imagerows)
285 image_directive = '\n'.join(imagerows)
286 return image_file, image_directive
286 return image_file, image_directive
287
287
288
288
289 # Callbacks for each type of token
289 # Callbacks for each type of token
290 def process_input(self, data, input_prompt, lineno):
290 def process_input(self, data, input_prompt, lineno):
291 """Process data block for INPUT token."""
291 """Process data block for INPUT token."""
292 decorator, input, rest = data
292 decorator, input, rest = data
293 image_file = None
293 image_file = None
294 image_directive = None
294 image_directive = None
295 #print 'INPUT:', data # dbg
295 #print 'INPUT:', data # dbg
296 is_verbatim = decorator=='@verbatim' or self.is_verbatim
296 is_verbatim = decorator=='@verbatim' or self.is_verbatim
297 is_doctest = decorator=='@doctest' or self.is_doctest
297 is_doctest = decorator=='@doctest' or self.is_doctest
298 is_suppress = decorator=='@suppress' or self.is_suppress
298 is_suppress = decorator=='@suppress' or self.is_suppress
299 is_savefig = decorator is not None and \
299 is_savefig = decorator is not None and \
300 decorator.startswith('@savefig')
300 decorator.startswith('@savefig')
301
301
302 input_lines = input.split('\n')
302 input_lines = input.split('\n')
303 if len(input_lines) > 1:
304 if input_lines[-1] != "":
305 input_lines.append('') # make sure there's a blank line
306 # so splitter buffer gets reset
303
307
304 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
308 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
305 Nc = len(continuation)
309 Nc = len(continuation)
306
310
307 if is_savefig:
311 if is_savefig:
308 image_file, image_directive = self.process_image(decorator)
312 image_file, image_directive = self.process_image(decorator)
309
313
310 ret = []
314 ret = []
311 is_semicolon = False
315 is_semicolon = False
312 store_history = True
316 store_history = True
313
317
314 for i, line in enumerate(input_lines):
318 for i, line in enumerate(input_lines):
315 if line.endswith(';'):
319 if line.endswith(';'):
316 is_semicolon = True
320 is_semicolon = True
317 if is_semicolon or is_suppress:
321 if is_suppress:
318 store_history = False
322 store_history = False
319
323
320 if i==0:
324 if i==0:
321 # process the first input line
325 # process the first input line
322 if is_verbatim:
326 if is_verbatim:
323 self.process_input_line('')
327 self.process_input_line('')
324 self.IP.execution_count += 1 # increment it anyway
328 self.IP.execution_count += 1 # increment it anyway
325 else:
329 else:
326 # only submit the line in non-verbatim mode
330 # only submit the line in non-verbatim mode
327 self.process_input_line(line, store_history=store_history)
331 self.process_input_line(line, store_history=store_history)
328 formatted_line = '%s %s'%(input_prompt, line)
332 formatted_line = '%s %s'%(input_prompt, line)
329 else:
333 else:
330 # process a continuation line
334 # process a continuation line
331 if not is_verbatim:
335 if not is_verbatim:
332 self.process_input_line(line, store_history=store_history)
336 self.process_input_line(line, store_history=store_history)
333
337
334 formatted_line = '%s %s'%(continuation, line)
338 formatted_line = '%s %s'%(continuation, line)
335
339
336 if not is_suppress:
340 if not is_suppress:
337 ret.append(formatted_line)
341 ret.append(formatted_line)
338
342
339 if not is_suppress:
343 if not is_suppress and len(rest.strip()) and is_verbatim:
340 if len(rest.strip()):
344 # the "rest" is the standard output of the
341 if is_verbatim:
345 # input, which needs to be added in
342 # the "rest" is the standard output of the
346 # verbatim mode
343 # input, which needs to be added in
347 ret.append(rest)
344 # verbatim mode
345 ret.append(rest)
346
348
347 self.cout.seek(0)
349 self.cout.seek(0)
348 output = self.cout.read()
350 output = self.cout.read()
349 if not is_suppress and not is_semicolon:
351 if not is_suppress and not is_semicolon:
350 ret.append(output)
352 ret.append(output)
353 elif is_semicolon: # get spacing right
354 ret.append('')
351
355
352 self.cout.truncate(0)
356 self.cout.truncate(0)
353 return (ret, input_lines, output, is_doctest, image_file,
357 return (ret, input_lines, output, is_doctest, image_file,
354 image_directive)
358 image_directive)
355 #print 'OUTPUT', output # dbg
359 #print 'OUTPUT', output # dbg
356
360
357 def process_output(self, data, output_prompt,
361 def process_output(self, data, output_prompt,
358 input_lines, output, is_doctest, image_file):
362 input_lines, output, is_doctest, image_file):
359 """Process data block for OUTPUT token."""
363 """Process data block for OUTPUT token."""
360 if is_doctest:
364 if is_doctest:
361 submitted = data.strip()
365 submitted = data.strip()
362 found = output
366 found = output
363 if found is not None:
367 if found is not None:
364 found = found.strip()
368 found = found.strip()
365
369
366 # XXX - fperez: in 0.11, 'output' never comes with the prompt
370 # XXX - fperez: in 0.11, 'output' never comes with the prompt
367 # in it, just the actual output text. So I think all this code
371 # in it, just the actual output text. So I think all this code
368 # can be nuked...
372 # can be nuked...
369
373
370 # the above comment does not appear to be accurate... (minrk)
374 # the above comment does not appear to be accurate... (minrk)
371
375
372 ind = found.find(output_prompt)
376 ind = found.find(output_prompt)
373 if ind<0:
377 if ind<0:
374 e='output prompt="%s" does not match out line=%s' % \
378 e='output prompt="%s" does not match out line=%s' % \
375 (output_prompt, found)
379 (output_prompt, found)
376 raise RuntimeError(e)
380 raise RuntimeError(e)
377 found = found[len(output_prompt):].strip()
381 found = found[len(output_prompt):].strip()
378
382
379 if found!=submitted:
383 if found!=submitted:
380 e = ('doctest failure for input_lines="%s" with '
384 e = ('doctest failure for input_lines="%s" with '
381 'found_output="%s" and submitted output="%s"' %
385 'found_output="%s" and submitted output="%s"' %
382 (input_lines, found, submitted) )
386 (input_lines, found, submitted) )
383 raise RuntimeError(e)
387 raise RuntimeError(e)
384 #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted)
388 #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted)
385
389
386 def process_comment(self, data):
390 def process_comment(self, data):
387 """Process data fPblock for COMMENT token."""
391 """Process data fPblock for COMMENT token."""
388 if not self.is_suppress:
392 if not self.is_suppress:
389 return [data]
393 return [data]
390
394
391 def save_image(self, image_file):
395 def save_image(self, image_file):
392 """
396 """
393 Saves the image file to disk.
397 Saves the image file to disk.
394 """
398 """
395 self.ensure_pyplot()
399 self.ensure_pyplot()
396 command = 'plt.gcf().savefig("%s")'%image_file
400 command = 'plt.gcf().savefig("%s")'%image_file
397 #print 'SAVEFIG', command # dbg
401 #print 'SAVEFIG', command # dbg
398 self.process_input_line('bookmark ipy_thisdir', store_history=False)
402 self.process_input_line('bookmark ipy_thisdir', store_history=False)
399 self.process_input_line('cd -b ipy_savedir', store_history=False)
403 self.process_input_line('cd -b ipy_savedir', store_history=False)
400 self.process_input_line(command, store_history=False)
404 self.process_input_line(command, store_history=False)
401 self.process_input_line('cd -b ipy_thisdir', store_history=False)
405 self.process_input_line('cd -b ipy_thisdir', store_history=False)
402 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
406 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
403 self.clear_cout()
407 self.clear_cout()
404
408
405
409
406 def process_block(self, block):
410 def process_block(self, block):
407 """
411 """
408 process block from the block_parser and return a list of processed lines
412 process block from the block_parser and return a list of processed lines
409 """
413 """
410 ret = []
414 ret = []
411 output = None
415 output = None
412 input_lines = None
416 input_lines = None
413 lineno = self.IP.execution_count
417 lineno = self.IP.execution_count
414
418
415 input_prompt = self.promptin%lineno
419 input_prompt = self.promptin%lineno
416 output_prompt = self.promptout%lineno
420 output_prompt = self.promptout%lineno
417 image_file = None
421 image_file = None
418 image_directive = None
422 image_directive = None
419
423
420 for token, data in block:
424 for token, data in block:
421 if token==COMMENT:
425 if token==COMMENT:
422 out_data = self.process_comment(data)
426 out_data = self.process_comment(data)
423 elif token==INPUT:
427 elif token==INPUT:
424 (out_data, input_lines, output, is_doctest, image_file,
428 (out_data, input_lines, output, is_doctest, image_file,
425 image_directive) = \
429 image_directive) = \
426 self.process_input(data, input_prompt, lineno)
430 self.process_input(data, input_prompt, lineno)
427 elif token==OUTPUT:
431 elif token==OUTPUT:
428 out_data = \
432 out_data = \
429 self.process_output(data, output_prompt,
433 self.process_output(data, output_prompt,
430 input_lines, output, is_doctest,
434 input_lines, output, is_doctest,
431 image_file)
435 image_file)
432 if out_data:
436 if out_data:
433 ret.extend(out_data)
437 ret.extend(out_data)
434
438
435 # save the image files
439 # save the image files
436 if image_file is not None:
440 if image_file is not None:
437 self.save_image(image_file)
441 self.save_image(image_file)
438
442
439 return ret, image_directive
443 return ret, image_directive
440
444
441 def ensure_pyplot(self):
445 def ensure_pyplot(self):
442 if self._pyplot_imported:
446 if self._pyplot_imported:
443 return
447 return
444 self.process_input_line('import matplotlib.pyplot as plt',
448 self.process_input_line('import matplotlib.pyplot as plt',
445 store_history=False)
449 store_history=False)
446
450
447 def process_pure_python(self, content):
451 def process_pure_python(self, content):
448 """
452 """
449 content is a list of strings. it is unedited directive conent
453 content is a list of strings. it is unedited directive conent
450
454
451 This runs it line by line in the InteractiveShell, prepends
455 This runs it line by line in the InteractiveShell, prepends
452 prompts as needed capturing stderr and stdout, then returns
456 prompts as needed capturing stderr and stdout, then returns
453 the content as a list as if it were ipython code
457 the content as a list as if it were ipython code
454 """
458 """
455 output = []
459 output = []
456 savefig = False # keep up with this to clear figure
460 savefig = False # keep up with this to clear figure
457 multiline = False # to handle line continuation
461 multiline = False # to handle line continuation
458 multiline_start = None
462 multiline_start = None
459 fmtin = self.promptin
463 fmtin = self.promptin
460
464
461 ct = 0
465 ct = 0
462
466
463 for lineno, line in enumerate(content):
467 for lineno, line in enumerate(content):
464
468
465 line_stripped = line.strip()
469 line_stripped = line.strip()
466 if not len(line):
470 if not len(line):
467 output.append(line)
471 output.append(line)
468 continue
472 continue
469
473
470 # handle decorators
474 # handle decorators
471 if line_stripped.startswith('@'):
475 if line_stripped.startswith('@'):
472 output.extend([line])
476 output.extend([line])
473 if 'savefig' in line:
477 if 'savefig' in line:
474 savefig = True # and need to clear figure
478 savefig = True # and need to clear figure
475 continue
479 continue
476
480
477 # handle comments
481 # handle comments
478 if line_stripped.startswith('#'):
482 if line_stripped.startswith('#'):
479 output.extend([line])
483 output.extend([line])
480 continue
484 continue
481
485
482 # deal with lines checking for multiline
486 # deal with lines checking for multiline
483 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
487 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
484 if not multiline:
488 if not multiline:
485 modified = u"%s %s" % (fmtin % ct, line_stripped)
489 modified = u"%s %s" % (fmtin % ct, line_stripped)
486 output.append(modified)
490 output.append(modified)
487 ct += 1
491 ct += 1
488 try:
492 try:
489 ast.parse(line_stripped)
493 ast.parse(line_stripped)
490 output.append(u'')
494 output.append(u'')
491 except Exception: # on a multiline
495 except Exception: # on a multiline
492 multiline = True
496 multiline = True
493 multiline_start = lineno
497 multiline_start = lineno
494 if line_stripped.startswith('def '):
498 if line_stripped.startswith('def '):
495 is_function = True
499 is_function = True
496 else: # still on a multiline
500 else: # still on a multiline
497 modified = u'%s %s' % (continuation, line)
501 modified = u'%s %s' % (continuation, line)
498 output.append(modified)
502 output.append(modified)
499 try:
503 try:
500 mod = ast.parse(
504 mod = ast.parse(
501 '\n'.join(content[multiline_start:lineno+1]))
505 '\n'.join(content[multiline_start:lineno+1]))
502 if isinstance(mod.body[0], ast.FunctionDef):
506 if isinstance(mod.body[0], ast.FunctionDef):
503 # check to see if we have the whole function
507 # check to see if we have the whole function
504 for element in mod.body[0].body:
508 for element in mod.body[0].body:
505 if isinstance(element, ast.Return):
509 if isinstance(element, ast.Return):
506 multiline = False
510 multiline = False
507 else:
511 else:
508 output.append(u'')
512 output.append(u'')
509 multiline = False
513 multiline = False
510 except Exception:
514 except Exception:
511 pass
515 pass
512
516
513 if savefig: # clear figure if plotted
517 if savefig: # clear figure if plotted
514 self.ensure_pyplot()
518 self.ensure_pyplot()
515 self.process_input_line('plt.clf()', store_history=False)
519 self.process_input_line('plt.clf()', store_history=False)
516 self.clear_cout()
520 self.clear_cout()
517 savefig = False
521 savefig = False
518
522
519 return output
523 return output
520
524
521 class IpythonDirective(Directive):
525 class IpythonDirective(Directive):
522
526
523 has_content = True
527 has_content = True
524 required_arguments = 0
528 required_arguments = 0
525 optional_arguments = 4 # python, suppress, verbatim, doctest
529 optional_arguments = 4 # python, suppress, verbatim, doctest
526 final_argumuent_whitespace = True
530 final_argumuent_whitespace = True
527 option_spec = { 'python': directives.unchanged,
531 option_spec = { 'python': directives.unchanged,
528 'suppress' : directives.flag,
532 'suppress' : directives.flag,
529 'verbatim' : directives.flag,
533 'verbatim' : directives.flag,
530 'doctest' : directives.flag,
534 'doctest' : directives.flag,
531 }
535 }
532
536
533 shell = EmbeddedSphinxShell()
537 shell = EmbeddedSphinxShell()
534
538
535 def get_config_options(self):
539 def get_config_options(self):
536 # contains sphinx configuration variables
540 # contains sphinx configuration variables
537 config = self.state.document.settings.env.config
541 config = self.state.document.settings.env.config
538
542
539 # get config variables to set figure output directory
543 # get config variables to set figure output directory
540 confdir = self.state.document.settings.env.app.confdir
544 confdir = self.state.document.settings.env.app.confdir
541 savefig_dir = config.ipython_savefig_dir
545 savefig_dir = config.ipython_savefig_dir
542 source_dir = os.path.dirname(self.state.document.current_source)
546 source_dir = os.path.dirname(self.state.document.current_source)
543 if savefig_dir is None:
547 if savefig_dir is None:
544 savefig_dir = config.html_static_path
548 savefig_dir = config.html_static_path
545 if isinstance(savefig_dir, list):
549 if isinstance(savefig_dir, list):
546 savefig_dir = savefig_dir[0] # safe to assume only one path?
550 savefig_dir = savefig_dir[0] # safe to assume only one path?
547 savefig_dir = os.path.join(confdir, savefig_dir)
551 savefig_dir = os.path.join(confdir, savefig_dir)
548
552
549 # get regex and prompt stuff
553 # get regex and prompt stuff
550 rgxin = config.ipython_rgxin
554 rgxin = config.ipython_rgxin
551 rgxout = config.ipython_rgxout
555 rgxout = config.ipython_rgxout
552 promptin = config.ipython_promptin
556 promptin = config.ipython_promptin
553 promptout = config.ipython_promptout
557 promptout = config.ipython_promptout
554
558
555 return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout
559 return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout
556
560
557 def setup(self):
561 def setup(self):
558 # get config values
562 # get config values
559 (savefig_dir, source_dir, rgxin,
563 (savefig_dir, source_dir, rgxin,
560 rgxout, promptin, promptout) = self.get_config_options()
564 rgxout, promptin, promptout) = self.get_config_options()
561
565
562 # and attach to shell so we don't have to pass them around
566 # and attach to shell so we don't have to pass them around
563 self.shell.rgxin = rgxin
567 self.shell.rgxin = rgxin
564 self.shell.rgxout = rgxout
568 self.shell.rgxout = rgxout
565 self.shell.promptin = promptin
569 self.shell.promptin = promptin
566 self.shell.promptout = promptout
570 self.shell.promptout = promptout
567 self.shell.savefig_dir = savefig_dir
571 self.shell.savefig_dir = savefig_dir
568 self.shell.source_dir = source_dir
572 self.shell.source_dir = source_dir
569
573
570 # setup bookmark for saving figures directory
574 # setup bookmark for saving figures directory
571
575
572 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
576 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
573 store_history=False)
577 store_history=False)
574 self.shell.clear_cout()
578 self.shell.clear_cout()
575
579
576 return rgxin, rgxout, promptin, promptout
580 return rgxin, rgxout, promptin, promptout
577
581
578
582
579 def teardown(self):
583 def teardown(self):
580 # delete last bookmark
584 # delete last bookmark
581 self.shell.process_input_line('bookmark -d ipy_savedir',
585 self.shell.process_input_line('bookmark -d ipy_savedir',
582 store_history=False)
586 store_history=False)
583 self.shell.clear_cout()
587 self.shell.clear_cout()
584
588
585 def run(self):
589 def run(self):
586 debug = False
590 debug = False
587
591
588 #TODO, any reason block_parser can't be a method of embeddable shell
592 #TODO, any reason block_parser can't be a method of embeddable shell
589 # then we wouldn't have to carry these around
593 # then we wouldn't have to carry these around
590 rgxin, rgxout, promptin, promptout = self.setup()
594 rgxin, rgxout, promptin, promptout = self.setup()
591
595
592 options = self.options
596 options = self.options
593 self.shell.is_suppress = 'suppress' in options
597 self.shell.is_suppress = 'suppress' in options
594 self.shell.is_doctest = 'doctest' in options
598 self.shell.is_doctest = 'doctest' in options
595 self.shell.is_verbatim = 'verbatim' in options
599 self.shell.is_verbatim = 'verbatim' in options
596
600
597
601
598 # handle pure python code
602 # handle pure python code
599 if 'python' in self.arguments:
603 if 'python' in self.arguments:
600 content = self.content
604 content = self.content
601 self.content = self.shell.process_pure_python(content)
605 self.content = self.shell.process_pure_python(content)
602
606
603 parts = '\n'.join(self.content).split('\n\n')
607 parts = '\n'.join(self.content).split('\n\n')
604
608
605 lines = ['.. code-block:: ipython','']
609 lines = ['.. code-block:: ipython','']
606 figures = []
610 figures = []
607
611
608 for part in parts:
612 for part in parts:
609
613
610 block = block_parser(part, rgxin, rgxout, promptin, promptout)
614 block = block_parser(part, rgxin, rgxout, promptin, promptout)
611
615
612 if len(block):
616 if len(block):
613 rows, figure = self.shell.process_block(block)
617 rows, figure = self.shell.process_block(block)
614 for row in rows:
618 for row in rows:
615 lines.extend([' %s'%line for line in row.split('\n')])
619 lines.extend([' %s'%line for line in row.split('\n')])
616
620
617 if figure is not None:
621 if figure is not None:
618 figures.append(figure)
622 figures.append(figure)
619
623
620 #text = '\n'.join(lines)
624 #text = '\n'.join(lines)
621 #figs = '\n'.join(figures)
625 #figs = '\n'.join(figures)
622
626
623 for figure in figures:
627 for figure in figures:
624 lines.append('')
628 lines.append('')
625 lines.extend(figure.split('\n'))
629 lines.extend(figure.split('\n'))
626 lines.append('')
630 lines.append('')
627
631
628 #print lines
632 #print lines
629 if len(lines)>2:
633 if len(lines)>2:
630 if debug:
634 if debug:
631 print '\n'.join(lines)
635 print '\n'.join(lines)
632 else: #NOTE: this raises some errors, what's it for?
636 else: #NOTE: this raises some errors, what's it for?
633 #print 'INSERTING %d lines'%len(lines)
637 #print 'INSERTING %d lines'%len(lines)
634 self.state_machine.insert_input(
638 self.state_machine.insert_input(
635 lines, self.state_machine.input_lines.source(0))
639 lines, self.state_machine.input_lines.source(0))
636
640
637 text = '\n'.join(lines)
641 text = '\n'.join(lines)
638 txtnode = nodes.literal_block(text, text)
642 txtnode = nodes.literal_block(text, text)
639 txtnode['language'] = 'ipython'
643 txtnode['language'] = 'ipython'
640 #imgnode = nodes.image(figs)
644 #imgnode = nodes.image(figs)
641
645
642 # cleanup
646 # cleanup
643 self.teardown()
647 self.teardown()
644
648
645 return []#, imgnode]
649 return []#, imgnode]
646
650
647 # Enable as a proper Sphinx directive
651 # Enable as a proper Sphinx directive
648 def setup(app):
652 def setup(app):
649 setup.app = app
653 setup.app = app
650
654
651 app.add_directive('ipython', IpythonDirective)
655 app.add_directive('ipython', IpythonDirective)
652 app.add_config_value('ipython_savefig_dir', None, True)
656 app.add_config_value('ipython_savefig_dir', None, True)
653 app.add_config_value('ipython_rgxin',
657 app.add_config_value('ipython_rgxin',
654 re.compile('In \[(\d+)\]:\s?(.*)\s*'), True)
658 re.compile('In \[(\d+)\]:\s?(.*)\s*'), True)
655 app.add_config_value('ipython_rgxout',
659 app.add_config_value('ipython_rgxout',
656 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True)
660 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True)
657 app.add_config_value('ipython_promptin', 'In [%d]:', True)
661 app.add_config_value('ipython_promptin', 'In [%d]:', True)
658 app.add_config_value('ipython_promptout', 'Out[%d]:', True)
662 app.add_config_value('ipython_promptout', 'Out[%d]:', True)
659
663
660
664
661 # Simple smoke test, needs to be converted to a proper automatic test.
665 # Simple smoke test, needs to be converted to a proper automatic test.
662 def test():
666 def test():
663
667
664 examples = [
668 examples = [
665 r"""
669 r"""
666 In [9]: pwd
670 In [9]: pwd
667 Out[9]: '/home/jdhunter/py4science/book'
671 Out[9]: '/home/jdhunter/py4science/book'
668
672
669 In [10]: cd bookdata/
673 In [10]: cd bookdata/
670 /home/jdhunter/py4science/book/bookdata
674 /home/jdhunter/py4science/book/bookdata
671
675
672 In [2]: from pylab import *
676 In [2]: from pylab import *
673
677
674 In [2]: ion()
678 In [2]: ion()
675
679
676 In [3]: im = imread('stinkbug.png')
680 In [3]: im = imread('stinkbug.png')
677
681
678 @savefig mystinkbug.png width=4in
682 @savefig mystinkbug.png width=4in
679 In [4]: imshow(im)
683 In [4]: imshow(im)
680 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
684 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
681
685
682 """,
686 """,
683 r"""
687 r"""
684
688
685 In [1]: x = 'hello world'
689 In [1]: x = 'hello world'
686
690
687 # string methods can be
691 # string methods can be
688 # used to alter the string
692 # used to alter the string
689 @doctest
693 @doctest
690 In [2]: x.upper()
694 In [2]: x.upper()
691 Out[2]: 'HELLO WORLD'
695 Out[2]: 'HELLO WORLD'
692
696
693 @verbatim
697 @verbatim
694 In [3]: x.st<TAB>
698 In [3]: x.st<TAB>
695 x.startswith x.strip
699 x.startswith x.strip
696 """,
700 """,
697 r"""
701 r"""
698
702
699 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
703 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
700 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
704 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
701
705
702 In [131]: print url.split('&')
706 In [131]: print url.split('&')
703 ['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']
707 ['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']
704
708
705 In [60]: import urllib
709 In [60]: import urllib
706
710
707 """,
711 """,
708 r"""\
712 r"""\
709
713
710 In [133]: import numpy.random
714 In [133]: import numpy.random
711
715
712 @suppress
716 @suppress
713 In [134]: numpy.random.seed(2358)
717 In [134]: numpy.random.seed(2358)
714
718
715 @doctest
719 @doctest
716 In [135]: numpy.random.rand(10,2)
720 In [135]: numpy.random.rand(10,2)
717 Out[135]:
721 Out[135]:
718 array([[ 0.64524308, 0.59943846],
722 array([[ 0.64524308, 0.59943846],
719 [ 0.47102322, 0.8715456 ],
723 [ 0.47102322, 0.8715456 ],
720 [ 0.29370834, 0.74776844],
724 [ 0.29370834, 0.74776844],
721 [ 0.99539577, 0.1313423 ],
725 [ 0.99539577, 0.1313423 ],
722 [ 0.16250302, 0.21103583],
726 [ 0.16250302, 0.21103583],
723 [ 0.81626524, 0.1312433 ],
727 [ 0.81626524, 0.1312433 ],
724 [ 0.67338089, 0.72302393],
728 [ 0.67338089, 0.72302393],
725 [ 0.7566368 , 0.07033696],
729 [ 0.7566368 , 0.07033696],
726 [ 0.22591016, 0.77731835],
730 [ 0.22591016, 0.77731835],
727 [ 0.0072729 , 0.34273127]])
731 [ 0.0072729 , 0.34273127]])
728
732
729 """,
733 """,
730
734
731 r"""
735 r"""
732 In [106]: print x
736 In [106]: print x
733 jdh
737 jdh
734
738
735 In [109]: for i in range(10):
739 In [109]: for i in range(10):
736 .....: print i
740 .....: print i
737 .....:
741 .....:
738 .....:
742 .....:
739 0
743 0
740 1
744 1
741 2
745 2
742 3
746 3
743 4
747 4
744 5
748 5
745 6
749 6
746 7
750 7
747 8
751 8
748 9
752 9
749 """,
753 """,
750
754
751 r"""
755 r"""
752
756
753 In [144]: from pylab import *
757 In [144]: from pylab import *
754
758
755 In [145]: ion()
759 In [145]: ion()
756
760
757 # use a semicolon to suppress the output
761 # use a semicolon to suppress the output
758 @savefig test_hist.png width=4in
762 @savefig test_hist.png width=4in
759 In [151]: hist(np.random.randn(10000), 100);
763 In [151]: hist(np.random.randn(10000), 100);
760
764
761
765
762 @savefig test_plot.png width=4in
766 @savefig test_plot.png width=4in
763 In [151]: plot(np.random.randn(10000), 'o');
767 In [151]: plot(np.random.randn(10000), 'o');
764 """,
768 """,
765
769
766 r"""
770 r"""
767 # use a semicolon to suppress the output
771 # use a semicolon to suppress the output
768 In [151]: plt.clf()
772 In [151]: plt.clf()
769
773
770 @savefig plot_simple.png width=4in
774 @savefig plot_simple.png width=4in
771 In [151]: plot([1,2,3])
775 In [151]: plot([1,2,3])
772
776
773 @savefig hist_simple.png width=4in
777 @savefig hist_simple.png width=4in
774 In [151]: hist(np.random.randn(10000), 100);
778 In [151]: hist(np.random.randn(10000), 100);
775
779
776 """,
780 """,
777 r"""
781 r"""
778 # update the current fig
782 # update the current fig
779 In [151]: ylabel('number')
783 In [151]: ylabel('number')
780
784
781 In [152]: title('normal distribution')
785 In [152]: title('normal distribution')
782
786
783
787
784 @savefig hist_with_text.png
788 @savefig hist_with_text.png
785 In [153]: grid(True)
789 In [153]: grid(True)
786
790
787 """,
791 """,
788 ]
792 ]
789 # skip local-file depending first example:
793 # skip local-file depending first example:
790 examples = examples[1:]
794 examples = examples[1:]
791
795
792 #ipython_directive.DEBUG = True # dbg
796 #ipython_directive.DEBUG = True # dbg
793 #options = dict(suppress=True) # dbg
797 #options = dict(suppress=True) # dbg
794 options = dict()
798 options = dict()
795 for example in examples:
799 for example in examples:
796 content = example.split('\n')
800 content = example.split('\n')
797 ipython_directive('debug', arguments=None, options=options,
801 ipython_directive('debug', arguments=None, options=options,
798 content=content, lineno=0,
802 content=content, lineno=0,
799 content_offset=None, block_text=None,
803 content_offset=None, block_text=None,
800 state=None, state_machine=None,
804 state=None, state_machine=None,
801 )
805 )
802
806
803 # Run test suite as a script
807 # Run test suite as a script
804 if __name__=='__main__':
808 if __name__=='__main__':
805 if not os.path.isdir('_static'):
809 if not os.path.isdir('_static'):
806 os.mkdir('_static')
810 os.mkdir('_static')
807 test()
811 test()
808 print 'All OK? Check figures in _static/'
812 print 'All OK? Check figures in _static/'
General Comments 0
You need to be logged in to leave comments. Login now