##// END OF EJS Templates
Use AST parser to find python multilines with function handling
Skipper Seabold -
Show More
@@ -1,813 +1,808 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
303
304 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
304 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
305 Nc = len(continuation)
305 Nc = len(continuation)
306
306
307 if is_savefig:
307 if is_savefig:
308 image_file, image_directive = self.process_image(decorator)
308 image_file, image_directive = self.process_image(decorator)
309
309
310 ret = []
310 ret = []
311 is_semicolon = False
311 is_semicolon = False
312 store_history = True
312 store_history = True
313
313
314 for i, line in enumerate(input_lines):
314 for i, line in enumerate(input_lines):
315 if line.endswith(';'):
315 if line.endswith(';'):
316 is_semicolon = True
316 is_semicolon = True
317 if is_semicolon or is_suppress:
317 if is_semicolon or is_suppress:
318 store_history = False
318 store_history = False
319
319
320 if i==0:
320 if i==0:
321 # process the first input line
321 # process the first input line
322 if is_verbatim:
322 if is_verbatim:
323 self.process_input_line('')
323 self.process_input_line('')
324 self.IP.execution_count += 1 # increment it anyway
324 self.IP.execution_count += 1 # increment it anyway
325 else:
325 else:
326 # only submit the line in non-verbatim mode
326 # only submit the line in non-verbatim mode
327 self.process_input_line(line, store_history=store_history)
327 self.process_input_line(line, store_history=store_history)
328 formatted_line = '%s %s'%(input_prompt, line)
328 formatted_line = '%s %s'%(input_prompt, line)
329 else:
329 else:
330 # process a continuation line
330 # process a continuation line
331 if not is_verbatim:
331 if not is_verbatim:
332 self.process_input_line(line, store_history=store_history)
332 self.process_input_line(line, store_history=store_history)
333
333
334 formatted_line = '%s %s'%(continuation, line)
334 formatted_line = '%s %s'%(continuation, line)
335
335
336 if not is_suppress:
336 if not is_suppress:
337 ret.append(formatted_line)
337 ret.append(formatted_line)
338
338
339 if not is_suppress:
339 if not is_suppress:
340 if len(rest.strip()):
340 if len(rest.strip()):
341 if is_verbatim:
341 if is_verbatim:
342 # the "rest" is the standard output of the
342 # the "rest" is the standard output of the
343 # input, which needs to be added in
343 # input, which needs to be added in
344 # verbatim mode
344 # verbatim mode
345 ret.append(rest)
345 ret.append(rest)
346
346
347 self.cout.seek(0)
347 self.cout.seek(0)
348 output = self.cout.read()
348 output = self.cout.read()
349 if not is_suppress and not is_semicolon:
349 if not is_suppress and not is_semicolon:
350 ret.append(output)
350 ret.append(output)
351
351
352 self.cout.truncate(0)
352 self.cout.truncate(0)
353 return (ret, input_lines, output, is_doctest, image_file,
353 return (ret, input_lines, output, is_doctest, image_file,
354 image_directive)
354 image_directive)
355 #print 'OUTPUT', output # dbg
355 #print 'OUTPUT', output # dbg
356
356
357 def process_output(self, data, output_prompt,
357 def process_output(self, data, output_prompt,
358 input_lines, output, is_doctest, image_file):
358 input_lines, output, is_doctest, image_file):
359 """Process data block for OUTPUT token."""
359 """Process data block for OUTPUT token."""
360 if is_doctest:
360 if is_doctest:
361 submitted = data.strip()
361 submitted = data.strip()
362 found = output
362 found = output
363 if found is not None:
363 if found is not None:
364 found = found.strip()
364 found = found.strip()
365
365
366 # XXX - fperez: in 0.11, 'output' never comes with the prompt
366 # 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
367 # in it, just the actual output text. So I think all this code
368 # can be nuked...
368 # can be nuked...
369
369
370 # the above comment does not appear to be accurate... (minrk)
370 # the above comment does not appear to be accurate... (minrk)
371
371
372 ind = found.find(output_prompt)
372 ind = found.find(output_prompt)
373 if ind<0:
373 if ind<0:
374 e='output prompt="%s" does not match out line=%s' % \
374 e='output prompt="%s" does not match out line=%s' % \
375 (output_prompt, found)
375 (output_prompt, found)
376 raise RuntimeError(e)
376 raise RuntimeError(e)
377 found = found[len(output_prompt):].strip()
377 found = found[len(output_prompt):].strip()
378
378
379 if found!=submitted:
379 if found!=submitted:
380 e = ('doctest failure for input_lines="%s" with '
380 e = ('doctest failure for input_lines="%s" with '
381 'found_output="%s" and submitted output="%s"' %
381 'found_output="%s" and submitted output="%s"' %
382 (input_lines, found, submitted) )
382 (input_lines, found, submitted) )
383 raise RuntimeError(e)
383 raise RuntimeError(e)
384 #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted)
384 #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted)
385
385
386 def process_comment(self, data):
386 def process_comment(self, data):
387 """Process data fPblock for COMMENT token."""
387 """Process data fPblock for COMMENT token."""
388 if not self.is_suppress:
388 if not self.is_suppress:
389 return [data]
389 return [data]
390
390
391 def save_image(self, image_file):
391 def save_image(self, image_file):
392 """
392 """
393 Saves the image file to disk.
393 Saves the image file to disk.
394 """
394 """
395 self.ensure_pyplot()
395 self.ensure_pyplot()
396 command = 'plt.gcf().savefig("%s")'%image_file
396 command = 'plt.gcf().savefig("%s")'%image_file
397 #print 'SAVEFIG', command # dbg
397 #print 'SAVEFIG', command # dbg
398 self.process_input_line('bookmark ipy_thisdir', store_history=False)
398 self.process_input_line('bookmark ipy_thisdir', store_history=False)
399 self.process_input_line('cd -b ipy_savedir', store_history=False)
399 self.process_input_line('cd -b ipy_savedir', store_history=False)
400 self.process_input_line(command, store_history=False)
400 self.process_input_line(command, store_history=False)
401 self.process_input_line('cd -b ipy_thisdir', store_history=False)
401 self.process_input_line('cd -b ipy_thisdir', store_history=False)
402 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
402 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
403 self.clear_cout()
403 self.clear_cout()
404
404
405
405
406 def process_block(self, block):
406 def process_block(self, block):
407 """
407 """
408 process block from the block_parser and return a list of processed lines
408 process block from the block_parser and return a list of processed lines
409 """
409 """
410 ret = []
410 ret = []
411 output = None
411 output = None
412 input_lines = None
412 input_lines = None
413 lineno = self.IP.execution_count
413 lineno = self.IP.execution_count
414
414
415 input_prompt = self.promptin%lineno
415 input_prompt = self.promptin%lineno
416 output_prompt = self.promptout%lineno
416 output_prompt = self.promptout%lineno
417 image_file = None
417 image_file = None
418 image_directive = None
418 image_directive = None
419
419
420 for token, data in block:
420 for token, data in block:
421 if token==COMMENT:
421 if token==COMMENT:
422 out_data = self.process_comment(data)
422 out_data = self.process_comment(data)
423 elif token==INPUT:
423 elif token==INPUT:
424 (out_data, input_lines, output, is_doctest, image_file,
424 (out_data, input_lines, output, is_doctest, image_file,
425 image_directive) = \
425 image_directive) = \
426 self.process_input(data, input_prompt, lineno)
426 self.process_input(data, input_prompt, lineno)
427 elif token==OUTPUT:
427 elif token==OUTPUT:
428 out_data = \
428 out_data = \
429 self.process_output(data, output_prompt,
429 self.process_output(data, output_prompt,
430 input_lines, output, is_doctest,
430 input_lines, output, is_doctest,
431 image_file)
431 image_file)
432 if out_data:
432 if out_data:
433 ret.extend(out_data)
433 ret.extend(out_data)
434
434
435 # save the image files
435 # save the image files
436 if image_file is not None:
436 if image_file is not None:
437 self.save_image(image_file)
437 self.save_image(image_file)
438
438
439 return ret, image_directive
439 return ret, image_directive
440
440
441 def ensure_pyplot(self):
441 def ensure_pyplot(self):
442 if self._pyplot_imported:
442 if self._pyplot_imported:
443 return
443 return
444 self.process_input_line('import matplotlib.pyplot as plt',
444 self.process_input_line('import matplotlib.pyplot as plt',
445 store_history=False)
445 store_history=False)
446
446
447 def process_pure_python(self, content):
447 def process_pure_python(self, content):
448 """
448 """
449 content is a list of strings. it is unedited directive conent
449 content is a list of strings. it is unedited directive conent
450
450
451 This runs it line by line in the InteractiveShell, prepends
451 This runs it line by line in the InteractiveShell, prepends
452 prompts as needed capturing stderr and stdout, then returns
452 prompts as needed capturing stderr and stdout, then returns
453 the content as a list as if it were ipython code
453 the content as a list as if it were ipython code
454 """
454 """
455 output = []
455 output = []
456 savefig = False # keep up with this to clear figure
456 savefig = False # keep up with this to clear figure
457 multiline = False # to handle line continuation
457 multiline = False # to handle line continuation
458 multiline_start = None
458 fmtin = self.promptin
459 fmtin = self.promptin
459
460
461 ct = 0
462
460 for lineno, line in enumerate(content):
463 for lineno, line in enumerate(content):
461
464
462 line_stripped = line.strip()
465 line_stripped = line.strip()
463
464 if not len(line):
466 if not len(line):
465 output.append(line) # preserve empty lines in output
467 output.append(line)
466 continue
468 continue
467
469
468 # handle decorators
470 # handle decorators
469 if line_stripped.startswith('@'):
471 if line_stripped.startswith('@'):
470 output.extend([line])
472 output.extend([line])
471 if 'savefig' in line:
473 if 'savefig' in line:
472 savefig = True # and need to clear figure
474 savefig = True # and need to clear figure
473 continue
475 continue
474
476
475 # handle comments
477 # handle comments
476 if line_stripped.startswith('#'):
478 if line_stripped.startswith('#'):
477 output.extend([line])
479 output.extend([line])
478 continue
480 continue
479
481
480 # deal with multilines
482 # deal with lines checking for multiline
481 if not multiline: # not currently on a multiline
483 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
482
484 if not multiline:
483 if line_stripped.endswith('\\'): # now we are
485 modified = u"%s %s" % (fmtin % ct, line_stripped)
486 output.append(modified)
487 ct += 1
488 try:
489 ast.parse(line_stripped)
490 output.append(u'')
491 except Exception: # on a multiline
484 multiline = True
492 multiline = True
485 cont_len = len(str(lineno)) + 2
493 multiline_start = lineno
486 line_to_process = line.strip('\\')
494 if line_stripped.startswith('def '):
487 output.extend([u"%s %s" % (fmtin%lineno,line)])
495 is_function = True
488 continue
496 else: # still on a multiline
489 else: # no we're still not
497 modified = u'%s %s' % (continuation, line)
490 line_to_process = line.strip('\\')
498 output.append(modified)
491 else: # we are currently on a multiline
499 try:
492 line_to_process += line.strip('\\')
500 mod = ast.parse(
493 if line_stripped.endswith('\\'): # and we still are
501 '\n'.join(content[multiline_start:lineno+1]))
494 continuation = '.' * cont_len
502 if isinstance(mod.body[0], ast.FunctionDef):
495 output.extend([(u' %s: '+line_stripped) % continuation])
503 # check to see if we have the whole function
496 continue
504 for element in mod.body[0].body:
497 # else go ahead and run this multiline then carry on
505 if isinstance(element, ast.Return):
498
506 multiline = False
499 # get output of line
507 else:
500 self.process_input_line(unicode(line_to_process.strip()),
508 output.append(u'')
501 store_history=False)
509 multiline = False
502 out_line = self.cout.getvalue()
510 except Exception:
503 self.clear_cout()
511 pass
504
512
505 # clear current figure if plotted
513 if savefig: # clear figure if plotted
506 if savefig:
507 self.ensure_pyplot()
514 self.ensure_pyplot()
508 self.process_input_line('plt.clf()', store_history=False)
515 self.process_input_line('plt.clf()', store_history=False)
509 self.clear_cout()
516 self.clear_cout()
510 savefig = False
517 savefig = False
511
518
512 # line numbers don't actually matter, they're replaced later
513 if not multiline:
514 in_line = u"%s %s" % (fmtin%lineno,line)
515
516 output.extend([in_line])
517 else:
518 output.extend([(u' %s: '+line_stripped) % continuation])
519 multiline = False
520 if len(out_line):
521 output.extend([out_line])
522 output.extend([u''])
523
524 return output
519 return output
525
520
526 class IpythonDirective(Directive):
521 class IpythonDirective(Directive):
527
522
528 has_content = True
523 has_content = True
529 required_arguments = 0
524 required_arguments = 0
530 optional_arguments = 4 # python, suppress, verbatim, doctest
525 optional_arguments = 4 # python, suppress, verbatim, doctest
531 final_argumuent_whitespace = True
526 final_argumuent_whitespace = True
532 option_spec = { 'python': directives.unchanged,
527 option_spec = { 'python': directives.unchanged,
533 'suppress' : directives.flag,
528 'suppress' : directives.flag,
534 'verbatim' : directives.flag,
529 'verbatim' : directives.flag,
535 'doctest' : directives.flag,
530 'doctest' : directives.flag,
536 }
531 }
537
532
538 shell = EmbeddedSphinxShell()
533 shell = EmbeddedSphinxShell()
539
534
540 def get_config_options(self):
535 def get_config_options(self):
541 # contains sphinx configuration variables
536 # contains sphinx configuration variables
542 config = self.state.document.settings.env.config
537 config = self.state.document.settings.env.config
543
538
544 # get config variables to set figure output directory
539 # get config variables to set figure output directory
545 confdir = self.state.document.settings.env.app.confdir
540 confdir = self.state.document.settings.env.app.confdir
546 savefig_dir = config.ipython_savefig_dir
541 savefig_dir = config.ipython_savefig_dir
547 source_dir = os.path.dirname(self.state.document.current_source)
542 source_dir = os.path.dirname(self.state.document.current_source)
548 if savefig_dir is None:
543 if savefig_dir is None:
549 savefig_dir = config.html_static_path
544 savefig_dir = config.html_static_path
550 if isinstance(savefig_dir, list):
545 if isinstance(savefig_dir, list):
551 savefig_dir = savefig_dir[0] # safe to assume only one path?
546 savefig_dir = savefig_dir[0] # safe to assume only one path?
552 savefig_dir = os.path.join(confdir, savefig_dir)
547 savefig_dir = os.path.join(confdir, savefig_dir)
553
548
554 # get regex and prompt stuff
549 # get regex and prompt stuff
555 rgxin = config.ipython_rgxin
550 rgxin = config.ipython_rgxin
556 rgxout = config.ipython_rgxout
551 rgxout = config.ipython_rgxout
557 promptin = config.ipython_promptin
552 promptin = config.ipython_promptin
558 promptout = config.ipython_promptout
553 promptout = config.ipython_promptout
559
554
560 return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout
555 return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout
561
556
562 def setup(self):
557 def setup(self):
563 # get config values
558 # get config values
564 (savefig_dir, source_dir, rgxin,
559 (savefig_dir, source_dir, rgxin,
565 rgxout, promptin, promptout) = self.get_config_options()
560 rgxout, promptin, promptout) = self.get_config_options()
566
561
567 # and attach to shell so we don't have to pass them around
562 # and attach to shell so we don't have to pass them around
568 self.shell.rgxin = rgxin
563 self.shell.rgxin = rgxin
569 self.shell.rgxout = rgxout
564 self.shell.rgxout = rgxout
570 self.shell.promptin = promptin
565 self.shell.promptin = promptin
571 self.shell.promptout = promptout
566 self.shell.promptout = promptout
572 self.shell.savefig_dir = savefig_dir
567 self.shell.savefig_dir = savefig_dir
573 self.shell.source_dir = source_dir
568 self.shell.source_dir = source_dir
574
569
575 # setup bookmark for saving figures directory
570 # setup bookmark for saving figures directory
576
571
577 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
572 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
578 store_history=False)
573 store_history=False)
579 self.shell.clear_cout()
574 self.shell.clear_cout()
580
575
581 return rgxin, rgxout, promptin, promptout
576 return rgxin, rgxout, promptin, promptout
582
577
583
578
584 def teardown(self):
579 def teardown(self):
585 # delete last bookmark
580 # delete last bookmark
586 self.shell.process_input_line('bookmark -d ipy_savedir',
581 self.shell.process_input_line('bookmark -d ipy_savedir',
587 store_history=False)
582 store_history=False)
588 self.shell.clear_cout()
583 self.shell.clear_cout()
589
584
590 def run(self):
585 def run(self):
591 debug = False
586 debug = False
592
587
593 #TODO, any reason block_parser can't be a method of embeddable shell
588 #TODO, any reason block_parser can't be a method of embeddable shell
594 # then we wouldn't have to carry these around
589 # then we wouldn't have to carry these around
595 rgxin, rgxout, promptin, promptout = self.setup()
590 rgxin, rgxout, promptin, promptout = self.setup()
596
591
597 options = self.options
592 options = self.options
598 self.shell.is_suppress = 'suppress' in options
593 self.shell.is_suppress = 'suppress' in options
599 self.shell.is_doctest = 'doctest' in options
594 self.shell.is_doctest = 'doctest' in options
600 self.shell.is_verbatim = 'verbatim' in options
595 self.shell.is_verbatim = 'verbatim' in options
601
596
602
597
603 # handle pure python code
598 # handle pure python code
604 if 'python' in self.arguments:
599 if 'python' in self.arguments:
605 content = self.content
600 content = self.content
606 self.content = self.shell.process_pure_python(content)
601 self.content = self.shell.process_pure_python(content)
607
602
608 parts = '\n'.join(self.content).split('\n\n')
603 parts = '\n'.join(self.content).split('\n\n')
609
604
610 lines = ['.. code-block:: ipython','']
605 lines = ['.. code-block:: ipython','']
611 figures = []
606 figures = []
612
607
613 for part in parts:
608 for part in parts:
614
609
615 block = block_parser(part, rgxin, rgxout, promptin, promptout)
610 block = block_parser(part, rgxin, rgxout, promptin, promptout)
616
611
617 if len(block):
612 if len(block):
618 rows, figure = self.shell.process_block(block)
613 rows, figure = self.shell.process_block(block)
619 for row in rows:
614 for row in rows:
620 lines.extend([' %s'%line for line in row.split('\n')])
615 lines.extend([' %s'%line for line in row.split('\n')])
621
616
622 if figure is not None:
617 if figure is not None:
623 figures.append(figure)
618 figures.append(figure)
624
619
625 #text = '\n'.join(lines)
620 #text = '\n'.join(lines)
626 #figs = '\n'.join(figures)
621 #figs = '\n'.join(figures)
627
622
628 for figure in figures:
623 for figure in figures:
629 lines.append('')
624 lines.append('')
630 lines.extend(figure.split('\n'))
625 lines.extend(figure.split('\n'))
631 lines.append('')
626 lines.append('')
632
627
633 #print lines
628 #print lines
634 if len(lines)>2:
629 if len(lines)>2:
635 if debug:
630 if debug:
636 print '\n'.join(lines)
631 print '\n'.join(lines)
637 else: #NOTE: this raises some errors, what's it for?
632 else: #NOTE: this raises some errors, what's it for?
638 #print 'INSERTING %d lines'%len(lines)
633 #print 'INSERTING %d lines'%len(lines)
639 self.state_machine.insert_input(
634 self.state_machine.insert_input(
640 lines, self.state_machine.input_lines.source(0))
635 lines, self.state_machine.input_lines.source(0))
641
636
642 text = '\n'.join(lines)
637 text = '\n'.join(lines)
643 txtnode = nodes.literal_block(text, text)
638 txtnode = nodes.literal_block(text, text)
644 txtnode['language'] = 'ipython'
639 txtnode['language'] = 'ipython'
645 #imgnode = nodes.image(figs)
640 #imgnode = nodes.image(figs)
646
641
647 # cleanup
642 # cleanup
648 self.teardown()
643 self.teardown()
649
644
650 return []#, imgnode]
645 return []#, imgnode]
651
646
652 # Enable as a proper Sphinx directive
647 # Enable as a proper Sphinx directive
653 def setup(app):
648 def setup(app):
654 setup.app = app
649 setup.app = app
655
650
656 app.add_directive('ipython', IpythonDirective)
651 app.add_directive('ipython', IpythonDirective)
657 app.add_config_value('ipython_savefig_dir', None, True)
652 app.add_config_value('ipython_savefig_dir', None, True)
658 app.add_config_value('ipython_rgxin',
653 app.add_config_value('ipython_rgxin',
659 re.compile('In \[(\d+)\]:\s?(.*)\s*'), True)
654 re.compile('In \[(\d+)\]:\s?(.*)\s*'), True)
660 app.add_config_value('ipython_rgxout',
655 app.add_config_value('ipython_rgxout',
661 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True)
656 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True)
662 app.add_config_value('ipython_promptin', 'In [%d]:', True)
657 app.add_config_value('ipython_promptin', 'In [%d]:', True)
663 app.add_config_value('ipython_promptout', 'Out[%d]:', True)
658 app.add_config_value('ipython_promptout', 'Out[%d]:', True)
664
659
665
660
666 # Simple smoke test, needs to be converted to a proper automatic test.
661 # Simple smoke test, needs to be converted to a proper automatic test.
667 def test():
662 def test():
668
663
669 examples = [
664 examples = [
670 r"""
665 r"""
671 In [9]: pwd
666 In [9]: pwd
672 Out[9]: '/home/jdhunter/py4science/book'
667 Out[9]: '/home/jdhunter/py4science/book'
673
668
674 In [10]: cd bookdata/
669 In [10]: cd bookdata/
675 /home/jdhunter/py4science/book/bookdata
670 /home/jdhunter/py4science/book/bookdata
676
671
677 In [2]: from pylab import *
672 In [2]: from pylab import *
678
673
679 In [2]: ion()
674 In [2]: ion()
680
675
681 In [3]: im = imread('stinkbug.png')
676 In [3]: im = imread('stinkbug.png')
682
677
683 @savefig mystinkbug.png width=4in
678 @savefig mystinkbug.png width=4in
684 In [4]: imshow(im)
679 In [4]: imshow(im)
685 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
680 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
686
681
687 """,
682 """,
688 r"""
683 r"""
689
684
690 In [1]: x = 'hello world'
685 In [1]: x = 'hello world'
691
686
692 # string methods can be
687 # string methods can be
693 # used to alter the string
688 # used to alter the string
694 @doctest
689 @doctest
695 In [2]: x.upper()
690 In [2]: x.upper()
696 Out[2]: 'HELLO WORLD'
691 Out[2]: 'HELLO WORLD'
697
692
698 @verbatim
693 @verbatim
699 In [3]: x.st<TAB>
694 In [3]: x.st<TAB>
700 x.startswith x.strip
695 x.startswith x.strip
701 """,
696 """,
702 r"""
697 r"""
703
698
704 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
699 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
705 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
700 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
706
701
707 In [131]: print url.split('&')
702 In [131]: print url.split('&')
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']
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']
709
704
710 In [60]: import urllib
705 In [60]: import urllib
711
706
712 """,
707 """,
713 r"""\
708 r"""\
714
709
715 In [133]: import numpy.random
710 In [133]: import numpy.random
716
711
717 @suppress
712 @suppress
718 In [134]: numpy.random.seed(2358)
713 In [134]: numpy.random.seed(2358)
719
714
720 @doctest
715 @doctest
721 In [135]: numpy.random.rand(10,2)
716 In [135]: numpy.random.rand(10,2)
722 Out[135]:
717 Out[135]:
723 array([[ 0.64524308, 0.59943846],
718 array([[ 0.64524308, 0.59943846],
724 [ 0.47102322, 0.8715456 ],
719 [ 0.47102322, 0.8715456 ],
725 [ 0.29370834, 0.74776844],
720 [ 0.29370834, 0.74776844],
726 [ 0.99539577, 0.1313423 ],
721 [ 0.99539577, 0.1313423 ],
727 [ 0.16250302, 0.21103583],
722 [ 0.16250302, 0.21103583],
728 [ 0.81626524, 0.1312433 ],
723 [ 0.81626524, 0.1312433 ],
729 [ 0.67338089, 0.72302393],
724 [ 0.67338089, 0.72302393],
730 [ 0.7566368 , 0.07033696],
725 [ 0.7566368 , 0.07033696],
731 [ 0.22591016, 0.77731835],
726 [ 0.22591016, 0.77731835],
732 [ 0.0072729 , 0.34273127]])
727 [ 0.0072729 , 0.34273127]])
733
728
734 """,
729 """,
735
730
736 r"""
731 r"""
737 In [106]: print x
732 In [106]: print x
738 jdh
733 jdh
739
734
740 In [109]: for i in range(10):
735 In [109]: for i in range(10):
741 .....: print i
736 .....: print i
742 .....:
737 .....:
743 .....:
738 .....:
744 0
739 0
745 1
740 1
746 2
741 2
747 3
742 3
748 4
743 4
749 5
744 5
750 6
745 6
751 7
746 7
752 8
747 8
753 9
748 9
754 """,
749 """,
755
750
756 r"""
751 r"""
757
752
758 In [144]: from pylab import *
753 In [144]: from pylab import *
759
754
760 In [145]: ion()
755 In [145]: ion()
761
756
762 # use a semicolon to suppress the output
757 # use a semicolon to suppress the output
763 @savefig test_hist.png width=4in
758 @savefig test_hist.png width=4in
764 In [151]: hist(np.random.randn(10000), 100);
759 In [151]: hist(np.random.randn(10000), 100);
765
760
766
761
767 @savefig test_plot.png width=4in
762 @savefig test_plot.png width=4in
768 In [151]: plot(np.random.randn(10000), 'o');
763 In [151]: plot(np.random.randn(10000), 'o');
769 """,
764 """,
770
765
771 r"""
766 r"""
772 # use a semicolon to suppress the output
767 # use a semicolon to suppress the output
773 In [151]: plt.clf()
768 In [151]: plt.clf()
774
769
775 @savefig plot_simple.png width=4in
770 @savefig plot_simple.png width=4in
776 In [151]: plot([1,2,3])
771 In [151]: plot([1,2,3])
777
772
778 @savefig hist_simple.png width=4in
773 @savefig hist_simple.png width=4in
779 In [151]: hist(np.random.randn(10000), 100);
774 In [151]: hist(np.random.randn(10000), 100);
780
775
781 """,
776 """,
782 r"""
777 r"""
783 # update the current fig
778 # update the current fig
784 In [151]: ylabel('number')
779 In [151]: ylabel('number')
785
780
786 In [152]: title('normal distribution')
781 In [152]: title('normal distribution')
787
782
788
783
789 @savefig hist_with_text.png
784 @savefig hist_with_text.png
790 In [153]: grid(True)
785 In [153]: grid(True)
791
786
792 """,
787 """,
793 ]
788 ]
794 # skip local-file depending first example:
789 # skip local-file depending first example:
795 examples = examples[1:]
790 examples = examples[1:]
796
791
797 #ipython_directive.DEBUG = True # dbg
792 #ipython_directive.DEBUG = True # dbg
798 #options = dict(suppress=True) # dbg
793 #options = dict(suppress=True) # dbg
799 options = dict()
794 options = dict()
800 for example in examples:
795 for example in examples:
801 content = example.split('\n')
796 content = example.split('\n')
802 ipython_directive('debug', arguments=None, options=options,
797 ipython_directive('debug', arguments=None, options=options,
803 content=content, lineno=0,
798 content=content, lineno=0,
804 content_offset=None, block_text=None,
799 content_offset=None, block_text=None,
805 state=None, state_machine=None,
800 state=None, state_machine=None,
806 )
801 )
807
802
808 # Run test suite as a script
803 # Run test suite as a script
809 if __name__=='__main__':
804 if __name__=='__main__':
810 if not os.path.isdir('_static'):
805 if not os.path.isdir('_static'):
811 os.mkdir('_static')
806 os.mkdir('_static')
812 test()
807 test()
813 print 'All OK? Check figures in _static/'
808 print 'All OK? Check figures in _static/'
General Comments 0
You need to be logged in to leave comments. Login now