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