##// END OF EJS Templates
Merge pull request #4570 from chebee7i/ipydirective...
Thomas Kluyver -
r13909:95829b6e merge
parent child Browse files
Show More
@@ -0,0 +1,155 b''
1 """
2 Handlers for IPythonDirective's @doctest pseudo-decorator.
3
4 The Sphinx extension that provides support for embedded IPython code provides
5 a pseudo-decorator @doctest, which treats the input/output block as a
6 doctest, raising a RuntimeError during doc generation if the actual output
7 (after running the input) does not match the expected output.
8
9 An example usage is:
10
11 .. code-block:: rst
12
13 .. ipython::
14
15 In [1]: x = 1
16
17 @doctest
18 In [2]: x + 2
19 Out[3]: 3
20
21 One can also provide arguments to the decorator. The first argument should be
22 the name of a custom handler. The specification of any other arguments is
23 determined by the handler. For example,
24
25 .. code-block:: rst
26
27 .. ipython::
28
29 @doctest float
30 In [154]: 0.1 + 0.2
31 Out[154]: 0.3
32
33 allows the actual output ``0.30000000000000004`` to match the expected output
34 due to a comparison with `np.allclose`.
35
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
37 should have the following function signature::
38
39 handler(sphinx_shell, args, input_lines, found, submitted)
40
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
43 a list of the lines relevant to the current doctest, `found` is a string
44 containing the output from the IPython shell, and `submitted` is a string
45 containing the expected output from the IPython shell.
46
47 Handlers must be registered in the `doctests` dict at the end of this module.
48
49 """
50
51 def str_to_array(s):
52 """
53 Simplistic converter of strings from repr to float NumPy arrays.
54
55 If the repr representation has ellipsis in it, then this will fail.
56
57 Parameters
58 ----------
59 s : str
60 The repr version of a NumPy array.
61
62 Examples
63 --------
64 >>> s = "array([ 0.3, inf, nan])"
65 >>> a = str_to_array(s)
66
67 """
68 import numpy as np
69
70 # Need to make sure eval() knows about inf and nan.
71 # This also assumes default printoptions for NumPy.
72 from numpy import inf, nan
73
74 if s.startswith(u'array'):
75 # Remove array( and )
76 s = s[6:-1]
77
78 if s.startswith(u'['):
79 a = np.array(eval(s), dtype=float)
80 else:
81 # Assume its a regular float. Force 1D so we can index into it.
82 a = np.atleast_1d(float(s))
83 return a
84
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
86 """
87 Doctest which allow the submitted output to vary slightly from the input.
88
89 Here is how it might appear in an rst file:
90
91 .. code-block:: rst
92
93 .. ipython::
94
95 @doctest float
96 In [1]: 0.1 + 0.2
97 Out[1]: 0.3
98
99 """
100 import numpy as np
101
102 if len(args) == 2:
103 rtol = 1e-05
104 atol = 1e-08
105 else:
106 # Both must be specified if any are specified.
107 try:
108 rtol = float(args[2])
109 atol = float(args[3])
110 except IndexError:
111 e = ("Both `rtol` and `atol` must be specified "
112 "if either are specified: {0}".format(args))
113 raise IndexError(e)
114
115 try:
116 submitted = str_to_array(submitted)
117 found = str_to_array(found)
118 except:
119 # For example, if the array is huge and there are ellipsis in it.
120 error = True
121 else:
122 found_isnan = np.isnan(found)
123 submitted_isnan = np.isnan(submitted)
124 error = not np.allclose(found_isnan, submitted_isnan)
125 error |= not np.allclose(found[~found_isnan],
126 submitted[~submitted_isnan],
127 rtol=rtol, atol=atol)
128
129 TAB = ' ' * 4
130 directive = sphinx_shell.directive
131 if directive is None:
132 source = 'Unavailable'
133 content = 'Unavailable'
134 else:
135 source = directive.state.document.current_source
136 # Add tabs and make into a single string.
137 content = '\n'.join([TAB + line for line in directive.content])
138
139 if error:
140
141 e = ('doctest float comparison failure\n\n'
142 'Document source: {0}\n\n'
143 'Raw content: \n{1}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
146 'instead of the expected:\n{TAB}{4}\n\n')
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
148 repr(submitted), TAB=TAB)
149 raise RuntimeError(e)
150
151 # dict of allowable doctest handlers. The key represents the first argument
152 # that must be given to @doctest in order to activate the handler.
153 doctests = {
154 'float': float_doctest,
155 }
@@ -1,834 +1,999 b''
1 1 # -*- coding: utf-8 -*-
2 """Sphinx directive to support embedded IPython code.
2 """
3 Sphinx directive to support embedded IPython code.
3 4
4 5 This directive allows pasting of entire interactive IPython sessions, prompts
5 6 and all, and their code will actually get re-executed at doc build time, with
6 7 all prompts renumbered sequentially. It also allows you to input code as a pure
7 8 python input by giving the argument python to the directive. The output looks
8 9 like an interactive ipython section.
9 10
10 11 To enable this directive, simply list it in your Sphinx ``conf.py`` file
11 12 (making sure the directory where you placed it is visible to sphinx, as is
12 needed for all Sphinx directives).
13 needed for all Sphinx directives). For example, to enable syntax highlighting
14 and the IPython directive::
15
16 extensions = ['IPython.sphinxext.ipython_console_highlighting',
17 'IPython.sphinxext.ipython_directive']
13 18
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
16 conf.py are
19 The IPython directive outputs code-blocks with the language 'ipython'. So
20 if you do not have the syntax highlighting extension enabled as well, then
21 all rendered code-blocks will be uncolored. By default this directive assumes
22 that your prompts are unchanged IPython ones, but this can be customized.
23 The configurable options that can be placed in conf.py are:
17 24
18 25 ipython_savefig_dir:
19 26 The directory in which to save the figures. This is relative to the
20 27 Sphinx source directory. The default is `html_static_path`.
21 28 ipython_rgxin:
22 29 The compiled regular expression to denote the start of IPython input
23 30 lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You
24 31 shouldn't need to change this.
25 32 ipython_rgxout:
26 33 The compiled regular expression to denote the start of IPython output
27 34 lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You
28 35 shouldn't need to change this.
29 36 ipython_promptin:
30 37 The string to represent the IPython input prompt in the generated ReST.
31 38 The default is 'In [%d]:'. This expects that the line numbers are used
32 39 in the prompt.
33 40 ipython_promptout:
34 41 The string to represent the IPython prompt in the generated ReST. The
35 42 default is 'Out [%d]:'. This expects that the line numbers are used
36 43 in the prompt.
44 ipython_mplbackend:
45 The string which specifies if the embedded Sphinx shell should import
46 Matplotlib and set the backend. The value specifies a backend that is
47 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
48 executed. If not specified in conf.py, then the default value of 'agg' is
49 used. To use the IPython directive without matplotlib as a dependency, set
50 the value to `None`. It may end up that matplotlib is still imported
51 if the user specifies so in `ipython_execlines` or makes use of the
52 @savefig pseudo decorator.
53 ipython_execlines:
54 A list of strings to be exec'd in the embedded Sphinx shell. Typical
55 usage is to make certain packages always available. Set this to an empty
56 list if you wish to have no imports always available. If specified in
57 conf.py as `None`, then it has the effect of making no imports available.
58 If omitted from conf.py altogether, then the default value of
59 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
60 ipython_holdcount
61 When the @suppress pseudo-decorator is used, the execution count can be
62 incremented or not. The default behavior is to hold the execution count,
63 corresponding to a value of `True`. Set this to `False` to increment
64 the execution count after each suppressed command.
65
66 As an example, to use the IPython directive when `matplotlib` is not available,
67 one sets the backend to `None`::
68
69 ipython_mplbackend = None
70
71 An example usage of the directive is:
72
73 .. code-block:: rst
74
75 .. ipython::
76
77 In [1]: x = 1
78
79 In [2]: y = x**2
80
81 In [3]: print(y)
82
83 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
84 documentation.
37 85
38 86 ToDo
39 87 ----
40 88
41 89 - Turn the ad-hoc test() function into a real test suite.
42 90 - Break up ipython-specific functionality from matplotlib stuff into better
43 91 separated code.
44 92
45 93 Authors
46 94 -------
47 95
48 96 - John D Hunter: orignal author.
49 97 - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
50 98 - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
51 99 - Skipper Seabold, refactoring, cleanups, pure python addition
52 100 """
53 101 from __future__ import print_function
54 102
55 103 #-----------------------------------------------------------------------------
56 104 # Imports
57 105 #-----------------------------------------------------------------------------
58 106
59 107 # Stdlib
60 108 import os
61 109 import re
62 110 import sys
63 111 import tempfile
64 112 import ast
65 113
66 114 # To keep compatibility with various python versions
67 115 try:
68 116 from hashlib import md5
69 117 except ImportError:
70 118 from md5 import md5
71 119
72 120 # Third-party
73 import matplotlib
74 121 import sphinx
75 122 from docutils.parsers.rst import directives
76 123 from docutils import nodes
77 124 from sphinx.util.compat import Directive
78 125
79 matplotlib.use('Agg')
80
81 126 # Our own
82 127 from IPython import Config, InteractiveShell
83 128 from IPython.core.profiledir import ProfileDir
84 129 from IPython.utils import io
85 130 from IPython.utils.py3compat import PY3
86 131
87 132 if PY3:
88 133 from io import StringIO
89 134 else:
90 135 from StringIO import StringIO
91 136
92 137 #-----------------------------------------------------------------------------
93 138 # Globals
94 139 #-----------------------------------------------------------------------------
95 140 # for tokenizing blocks
96 141 COMMENT, INPUT, OUTPUT = range(3)
97 142
98 143 #-----------------------------------------------------------------------------
99 144 # Functions and class declarations
100 145 #-----------------------------------------------------------------------------
146
101 147 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
102 148 """
103 149 part is a string of ipython text, comprised of at most one
104 150 input, one ouput, comments, and blank lines. The block parser
105 151 parses the text into a list of::
106 152
107 153 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
108 154
109 155 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
110 156 data is, depending on the type of token::
111 157
112 158 COMMENT : the comment string
113 159
114 160 INPUT: the (DECORATOR, INPUT_LINE, REST) where
115 161 DECORATOR: the input decorator (or None)
116 162 INPUT_LINE: the input as string (possibly multi-line)
117 163 REST : any stdout generated by the input line (not OUTPUT)
118 164
119
120 165 OUTPUT: the output string, possibly multi-line
121 """
122 166
167 """
123 168 block = []
124 169 lines = part.split('\n')
125 170 N = len(lines)
126 171 i = 0
127 172 decorator = None
128 173 while 1:
129 174
130 175 if i==N:
131 176 # nothing left to parse -- the last line
132 177 break
133 178
134 179 line = lines[i]
135 180 i += 1
136 181 line_stripped = line.strip()
137 182 if line_stripped.startswith('#'):
138 183 block.append((COMMENT, line))
139 184 continue
140 185
141 186 if line_stripped.startswith('@'):
142 187 # we're assuming at most one decorator -- may need to
143 188 # rethink
144 189 decorator = line_stripped
145 190 continue
146 191
147 192 # does this look like an input line?
148 193 matchin = rgxin.match(line)
149 194 if matchin:
150 195 lineno, inputline = int(matchin.group(1)), matchin.group(2)
151 196
152 197 # the ....: continuation string
153 198 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
154 199 Nc = len(continuation)
155 200 # input lines can continue on for more than one line, if
156 201 # we have a '\' line continuation char or a function call
157 202 # echo line 'print'. The input line can only be
158 203 # terminated by the end of the block or an output line, so
159 204 # we parse out the rest of the input line if it is
160 205 # multiline as well as any echo text
161 206
162 207 rest = []
163 208 while i<N:
164 209
165 210 # look ahead; if the next line is blank, or a comment, or
166 211 # an output line, we're done
167 212
168 213 nextline = lines[i]
169 214 matchout = rgxout.match(nextline)
170 215 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
171 216 if matchout or nextline.startswith('#'):
172 217 break
173 218 elif nextline.startswith(continuation):
174 219 inputline += '\n' + nextline[Nc:]
175 220 else:
176 221 rest.append(nextline)
177 222 i+= 1
178 223
179 224 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
180 225 continue
181 226
182 227 # if it looks like an output line grab all the text to the end
183 228 # of the block
184 229 matchout = rgxout.match(line)
185 230 if matchout:
186 231 lineno, output = int(matchout.group(1)), matchout.group(2)
187 232 if i<N-1:
188 233 output = '\n'.join([output] + lines[i:])
189 234
190 235 block.append((OUTPUT, output))
191 236 break
192 237
193 238 return block
194 239
240
195 241 class EmbeddedSphinxShell(object):
196 242 """An embedded IPython instance to run inside Sphinx"""
197 243
198 def __init__(self):
244 def __init__(self, exec_lines=None):
199 245
200 246 self.cout = StringIO()
201 247
248 if exec_lines is None:
249 exec_lines = []
202 250
203 251 # Create config object for IPython
204 252 config = Config()
205 config.Global.display_banner = False
206 config.Global.exec_lines = ['import numpy as np',
207 'from pylab import *'
208 ]
209 253 config.InteractiveShell.autocall = False
210 254 config.InteractiveShell.autoindent = False
211 255 config.InteractiveShell.colors = 'NoColor'
212 256
213 257 # create a profile so instance history isn't saved
214 258 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
215 259 profname = 'auto_profile_sphinx_build'
216 260 pdir = os.path.join(tmp_profile_dir,profname)
217 261 profile = ProfileDir.create_profile_dir(pdir)
218 262
219 # Create and initialize ipython, but don't start its mainloop
263 # Create and initialize global ipython, but don't start its mainloop.
264 # This will persist across different EmbededSphinxShell instances.
220 265 IP = InteractiveShell.instance(config=config, profile_dir=profile)
221 # io.stdout redirect must be done *after* instantiating InteractiveShell
266
267 # io.stdout redirect must be done after instantiating InteractiveShell
222 268 io.stdout = self.cout
223 269 io.stderr = self.cout
224 270
225 271 # For debugging, so we can see normal output, use this:
226 272 #from IPython.utils.io import Tee
227 273 #io.stdout = Tee(self.cout, channel='stdout') # dbg
228 274 #io.stderr = Tee(self.cout, channel='stderr') # dbg
229 275
230 276 # Store a few parts of IPython we'll need.
231 277 self.IP = IP
232 278 self.user_ns = self.IP.user_ns
233 279 self.user_global_ns = self.IP.user_global_ns
234 280
235 281 self.input = ''
236 282 self.output = ''
237 283
238 284 self.is_verbatim = False
239 285 self.is_doctest = False
240 286 self.is_suppress = False
241 287
288 # Optionally, provide more detailed information to shell.
289 self.directive = None
290
242 291 # on the first call to the savefig decorator, we'll import
243 292 # pyplot as plt so we can make a call to the plt.gcf().savefig
244 293 self._pyplot_imported = False
245 294
295 # Prepopulate the namespace.
296 for line in exec_lines:
297 self.process_input_line(line, store_history=False)
298
246 299 def clear_cout(self):
247 300 self.cout.seek(0)
248 301 self.cout.truncate(0)
249 302
250 303 def process_input_line(self, line, store_history=True):
251 304 """process the input, capturing stdout"""
252 #print "input='%s'"%self.input
305
253 306 stdout = sys.stdout
254 307 splitter = self.IP.input_splitter
255 308 try:
256 309 sys.stdout = self.cout
257 310 splitter.push(line)
258 311 more = splitter.push_accepts_more()
259 312 if not more:
260 313 source_raw = splitter.source_raw_reset()[1]
261 314 self.IP.run_cell(source_raw, store_history=store_history)
262 315 finally:
263 316 sys.stdout = stdout
264 317
265 318 def process_image(self, decorator):
266 319 """
267 320 # build out an image directive like
268 321 # .. image:: somefile.png
269 322 # :width 4in
270 323 #
271 324 # from an input like
272 325 # savefig somefile.png width=4in
273 326 """
274 327 savefig_dir = self.savefig_dir
275 328 source_dir = self.source_dir
276 329 saveargs = decorator.split(' ')
277 330 filename = saveargs[1]
278 331 # insert relative path to image file in source
279 332 outfile = os.path.relpath(os.path.join(savefig_dir,filename),
280 333 source_dir)
281 334
282 335 imagerows = ['.. image:: %s'%outfile]
283 336
284 337 for kwarg in saveargs[2:]:
285 338 arg, val = kwarg.split('=')
286 339 arg = arg.strip()
287 340 val = val.strip()
288 341 imagerows.append(' :%s: %s'%(arg, val))
289 342
290 343 image_file = os.path.basename(outfile) # only return file name
291 344 image_directive = '\n'.join(imagerows)
292 345 return image_file, image_directive
293 346
294
295 347 # Callbacks for each type of token
296 348 def process_input(self, data, input_prompt, lineno):
297 """Process data block for INPUT token."""
349 """
350 Process data block for INPUT token.
351
352 """
298 353 decorator, input, rest = data
299 354 image_file = None
300 355 image_directive = None
301 #print 'INPUT:', data # dbg
356
302 357 is_verbatim = decorator=='@verbatim' or self.is_verbatim
303 is_doctest = decorator=='@doctest' or self.is_doctest
358 is_doctest = (decorator is not None and \
359 decorator.startswith('@doctest')) or self.is_doctest
304 360 is_suppress = decorator=='@suppress' or self.is_suppress
305 361 is_savefig = decorator is not None and \
306 362 decorator.startswith('@savefig')
307 363
308 364 input_lines = input.split('\n')
309 365 if len(input_lines) > 1:
310 366 if input_lines[-1] != "":
311 367 input_lines.append('') # make sure there's a blank line
312 368 # so splitter buffer gets reset
313 369
314 370 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
315 Nc = len(continuation)
316 371
317 372 if is_savefig:
318 373 image_file, image_directive = self.process_image(decorator)
319 374
320 375 ret = []
321 376 is_semicolon = False
322 377
378 # Hold the execution count, if requested to do so.
379 if is_suppress and self.hold_count:
380 store_history = False
381 else:
382 store_history = True
383
323 384 for i, line in enumerate(input_lines):
324 385 if line.endswith(';'):
325 386 is_semicolon = True
326 387
327 388 if i==0:
328 389 # process the first input line
329 390 if is_verbatim:
330 391 self.process_input_line('')
331 392 self.IP.execution_count += 1 # increment it anyway
332 393 else:
333 394 # only submit the line in non-verbatim mode
334 self.process_input_line(line, store_history=True)
395 self.process_input_line(line, store_history=store_history)
335 396 formatted_line = '%s %s'%(input_prompt, line)
336 397 else:
337 398 # process a continuation line
338 399 if not is_verbatim:
339 self.process_input_line(line, store_history=True)
400 self.process_input_line(line, store_history=store_history)
340 401
341 402 formatted_line = '%s %s'%(continuation, line)
342 403
343 404 if not is_suppress:
344 405 ret.append(formatted_line)
345 406
346 407 if not is_suppress and len(rest.strip()) and is_verbatim:
347 408 # the "rest" is the standard output of the
348 409 # input, which needs to be added in
349 410 # verbatim mode
350 411 ret.append(rest)
351 412
352 413 self.cout.seek(0)
353 414 output = self.cout.read()
354 415 if not is_suppress and not is_semicolon:
355 416 ret.append(output)
356 417 elif is_semicolon: # get spacing right
357 418 ret.append('')
358 419
359 420 self.cout.truncate(0)
360 return (ret, input_lines, output, is_doctest, image_file,
421 return (ret, input_lines, output, is_doctest, decorator, image_file,
361 422 image_directive)
362 #print 'OUTPUT', output # dbg
423
363 424
364 425 def process_output(self, data, output_prompt,
365 input_lines, output, is_doctest, image_file):
366 """Process data block for OUTPUT token."""
367 if is_doctest:
368 submitted = data.strip()
426 input_lines, output, is_doctest, decorator, image_file):
427 """
428 Process data block for OUTPUT token.
429
430 """
431 TAB = ' ' * 4
432
433 if is_doctest and output is not None:
434
369 435 found = output
370 if found is not None:
371 436 found = found.strip()
437 submitted = data.strip()
372 438
373 # XXX - fperez: in 0.11, 'output' never comes with the prompt
374 # in it, just the actual output text. So I think all this code
375 # can be nuked...
376
377 # the above comment does not appear to be accurate... (minrk)
439 if self.directive is None:
440 source = 'Unavailable'
441 content = 'Unavailable'
442 else:
443 source = self.directive.state.document.current_source
444 content = self.directive.content
445 # Add tabs and join into a single string.
446 content = '\n'.join([TAB + line for line in content])
378 447
448 # Make sure the output contains the output prompt.
379 449 ind = found.find(output_prompt)
380 450 if ind<0:
381 e='output prompt="%s" does not match out line=%s' % \
382 (output_prompt, found)
451 e = ('output does not contain output prompt\n\n'
452 'Document source: {0}\n\n'
453 'Raw content: \n{1}\n\n'
454 'Input line(s):\n{TAB}{2}\n\n'
455 'Output line(s):\n{TAB}{3}\n\n')
456 e = e.format(source, content, '\n'.join(input_lines),
457 repr(found), TAB=TAB)
383 458 raise RuntimeError(e)
384 459 found = found[len(output_prompt):].strip()
385 460
461 # Handle the actual doctest comparison.
462 if decorator.strip() == '@doctest':
463 # Standard doctest
386 464 if found!=submitted:
387 e = ('doctest failure for input_lines="%s" with '
388 'found_output="%s" and submitted output="%s"' %
389 (input_lines, found, submitted) )
465 e = ('doctest failure\n\n'
466 'Document source: {0}\n\n'
467 'Raw content: \n{1}\n\n'
468 'On input line(s):\n{TAB}{2}\n\n'
469 'we found output:\n{TAB}{3}\n\n'
470 'instead of the expected:\n{TAB}{4}\n\n')
471 e = e.format(source, content, '\n'.join(input_lines),
472 repr(found), repr(submitted), TAB=TAB)
390 473 raise RuntimeError(e)
391 #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted)
474 else:
475 self.custom_doctest(decorator, input_lines, found, submitted)
392 476
393 477 def process_comment(self, data):
394 478 """Process data fPblock for COMMENT token."""
395 479 if not self.is_suppress:
396 480 return [data]
397 481
398 482 def save_image(self, image_file):
399 483 """
400 484 Saves the image file to disk.
401 485 """
402 486 self.ensure_pyplot()
403 487 command = 'plt.gcf().savefig("%s")'%image_file
404 488 #print 'SAVEFIG', command # dbg
405 489 self.process_input_line('bookmark ipy_thisdir', store_history=False)
406 490 self.process_input_line('cd -b ipy_savedir', store_history=False)
407 491 self.process_input_line(command, store_history=False)
408 492 self.process_input_line('cd -b ipy_thisdir', store_history=False)
409 493 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
410 494 self.clear_cout()
411 495
412
413 496 def process_block(self, block):
414 497 """
415 498 process block from the block_parser and return a list of processed lines
416 499 """
417 500 ret = []
418 501 output = None
419 502 input_lines = None
420 503 lineno = self.IP.execution_count
421 504
422 505 input_prompt = self.promptin%lineno
423 506 output_prompt = self.promptout%lineno
424 507 image_file = None
425 508 image_directive = None
426 509
427 510 for token, data in block:
428 511 if token==COMMENT:
429 512 out_data = self.process_comment(data)
430 513 elif token==INPUT:
431 (out_data, input_lines, output, is_doctest, image_file,
432 image_directive) = \
514 (out_data, input_lines, output, is_doctest, decorator,
515 image_file, image_directive) = \
433 516 self.process_input(data, input_prompt, lineno)
434 517 elif token==OUTPUT:
435 518 out_data = \
436 519 self.process_output(data, output_prompt,
437 520 input_lines, output, is_doctest,
438 image_file)
521 decorator, image_file)
439 522 if out_data:
440 523 ret.extend(out_data)
441 524
442 525 # save the image files
443 526 if image_file is not None:
444 527 self.save_image(image_file)
445 528
446 529 return ret, image_directive
447 530
448 531 def ensure_pyplot(self):
449 if self._pyplot_imported:
450 return
532 """
533 Ensures that pyplot has been imported into the embedded IPython shell.
534
535 Also, makes sure to set the backend appropriately if not set already.
536
537 """
538 # We are here if the @figure pseudo decorator was used. Thus, it's
539 # possible that we could be here even if python_mplbackend were set to
540 # `None`. That's also strange and perhaps worthy of raising an
541 # exception, but for now, we just set the backend to 'agg'.
542
543 if not self._pyplot_imported:
544 if 'matplotlib.backends' not in sys.modules:
545 # Then ipython_matplotlib was set to None but there was a
546 # call to the @figure decorator (and ipython_execlines did
547 # not set a backend).
548 #raise Exception("No backend was set, but @figure was used!")
549 import matplotlib
550 matplotlib.use('agg')
551
552 # Always import pyplot into embedded shell.
451 553 self.process_input_line('import matplotlib.pyplot as plt',
452 554 store_history=False)
555 self._pyplot_imported = True
453 556
454 557 def process_pure_python(self, content):
455 558 """
456 content is a list of strings. it is unedited directive conent
559 content is a list of strings. it is unedited directive content
457 560
458 561 This runs it line by line in the InteractiveShell, prepends
459 562 prompts as needed capturing stderr and stdout, then returns
460 563 the content as a list as if it were ipython code
461 564 """
462 565 output = []
463 566 savefig = False # keep up with this to clear figure
464 567 multiline = False # to handle line continuation
465 568 multiline_start = None
466 569 fmtin = self.promptin
467 570
468 571 ct = 0
469 572
470 573 for lineno, line in enumerate(content):
471 574
472 575 line_stripped = line.strip()
473 576 if not len(line):
474 577 output.append(line)
475 578 continue
476 579
477 580 # handle decorators
478 581 if line_stripped.startswith('@'):
479 582 output.extend([line])
480 583 if 'savefig' in line:
481 584 savefig = True # and need to clear figure
482 585 continue
483 586
484 587 # handle comments
485 588 if line_stripped.startswith('#'):
486 589 output.extend([line])
487 590 continue
488 591
489 592 # deal with lines checking for multiline
490 593 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
491 594 if not multiline:
492 595 modified = u"%s %s" % (fmtin % ct, line_stripped)
493 596 output.append(modified)
494 597 ct += 1
495 598 try:
496 599 ast.parse(line_stripped)
497 600 output.append(u'')
498 601 except Exception: # on a multiline
499 602 multiline = True
500 603 multiline_start = lineno
501 604 else: # still on a multiline
502 605 modified = u'%s %s' % (continuation, line)
503 606 output.append(modified)
504 607
505 608 # if the next line is indented, it should be part of multiline
506 609 if len(content) > lineno + 1:
507 610 nextline = content[lineno + 1]
508 611 if len(nextline) - len(nextline.lstrip()) > 3:
509 612 continue
510 613 try:
511 614 mod = ast.parse(
512 615 '\n'.join(content[multiline_start:lineno+1]))
513 616 if isinstance(mod.body[0], ast.FunctionDef):
514 617 # check to see if we have the whole function
515 618 for element in mod.body[0].body:
516 619 if isinstance(element, ast.Return):
517 620 multiline = False
518 621 else:
519 622 output.append(u'')
520 623 multiline = False
521 624 except Exception:
522 625 pass
523 626
524 627 if savefig: # clear figure if plotted
525 628 self.ensure_pyplot()
526 629 self.process_input_line('plt.clf()', store_history=False)
527 630 self.clear_cout()
528 631 savefig = False
529 632
530 633 return output
531 634
635 def custom_doctest(self, decorator, input_lines, found, submitted):
636 """
637 Perform a specialized doctest.
638
639 """
640 from .custom_doctests import doctests
641
642 args = decorator.split()
643 doctest_type = args[1]
644 if doctest_type in doctests:
645 doctests[doctest_type](self, args, input_lines, found, submitted)
646 else:
647 e = "Invalid option to @doctest: {0}".format(doctest_type)
648 raise Exception(e)
649
650
532 651 class IPythonDirective(Directive):
533 652
534 653 has_content = True
535 654 required_arguments = 0
536 655 optional_arguments = 4 # python, suppress, verbatim, doctest
537 656 final_argumuent_whitespace = True
538 657 option_spec = { 'python': directives.unchanged,
539 658 'suppress' : directives.flag,
540 659 'verbatim' : directives.flag,
541 660 'doctest' : directives.flag,
542 661 }
543 662
544 663 shell = None
545 664
546 665 seen_docs = set()
547 666
548 667 def get_config_options(self):
549 668 # contains sphinx configuration variables
550 669 config = self.state.document.settings.env.config
551 670
552 671 # get config variables to set figure output directory
553 672 confdir = self.state.document.settings.env.app.confdir
554 673 savefig_dir = config.ipython_savefig_dir
555 674 source_dir = os.path.dirname(self.state.document.current_source)
556 675 if savefig_dir is None:
557 676 savefig_dir = config.html_static_path
558 677 if isinstance(savefig_dir, list):
559 678 savefig_dir = savefig_dir[0] # safe to assume only one path?
560 679 savefig_dir = os.path.join(confdir, savefig_dir)
561 680
562 681 # get regex and prompt stuff
563 682 rgxin = config.ipython_rgxin
564 683 rgxout = config.ipython_rgxout
565 684 promptin = config.ipython_promptin
566 685 promptout = config.ipython_promptout
686 mplbackend = config.ipython_mplbackend
687 exec_lines = config.ipython_execlines
688 hold_count = config.ipython_holdcount
567 689
568 return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout
690 return (savefig_dir, source_dir, rgxin, rgxout,
691 promptin, promptout, mplbackend, exec_lines, hold_count)
569 692
570 693 def setup(self):
694 # Get configuration values.
695 (savefig_dir, source_dir, rgxin, rgxout, promptin, promptout,
696 mplbackend, exec_lines, hold_count) = self.get_config_options()
697
571 698 if self.shell is None:
572 self.shell = EmbeddedSphinxShell()
699 # We will be here many times. However, when the
700 # EmbeddedSphinxShell is created, its interactive shell member
701 # is the same for each instance.
702
703 if mplbackend:
704 import matplotlib
705 # Repeated calls to use() will not hurt us since `mplbackend`
706 # is the same each time.
707 matplotlib.use(mplbackend)
708
709 # Must be called after (potentially) importing matplotlib and
710 # setting its backend since exec_lines might import pylab.
711 self.shell = EmbeddedSphinxShell(exec_lines)
712
713 # Store IPython directive to enable better error messages
714 self.shell.directive = self
715
573 716 # reset the execution count if we haven't processed this doc
574 717 #NOTE: this may be borked if there are multiple seen_doc tmp files
575 718 #check time stamp?
576
577 719 if not self.state.document.current_source in self.seen_docs:
578 720 self.shell.IP.history_manager.reset()
579 721 self.shell.IP.execution_count = 1
722 self.shell.IP.prompt_manager.width = 0
580 723 self.seen_docs.add(self.state.document.current_source)
581 724
582
583
584 # get config values
585 (savefig_dir, source_dir, rgxin,
586 rgxout, promptin, promptout) = self.get_config_options()
587
588 725 # and attach to shell so we don't have to pass them around
589 726 self.shell.rgxin = rgxin
590 727 self.shell.rgxout = rgxout
591 728 self.shell.promptin = promptin
592 729 self.shell.promptout = promptout
593 730 self.shell.savefig_dir = savefig_dir
594 731 self.shell.source_dir = source_dir
732 self.shell.hold_count = hold_count
595 733
596 734 # setup bookmark for saving figures directory
597
598 735 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
599 736 store_history=False)
600 737 self.shell.clear_cout()
601 738
602 739 return rgxin, rgxout, promptin, promptout
603 740
604
605 741 def teardown(self):
606 742 # delete last bookmark
607 743 self.shell.process_input_line('bookmark -d ipy_savedir',
608 744 store_history=False)
609 745 self.shell.clear_cout()
610 746
611 747 def run(self):
612 748 debug = False
613 749
614 750 #TODO, any reason block_parser can't be a method of embeddable shell
615 751 # then we wouldn't have to carry these around
616 752 rgxin, rgxout, promptin, promptout = self.setup()
617 753
618 754 options = self.options
619 755 self.shell.is_suppress = 'suppress' in options
620 756 self.shell.is_doctest = 'doctest' in options
621 757 self.shell.is_verbatim = 'verbatim' in options
622 758
623
624 759 # handle pure python code
625 760 if 'python' in self.arguments:
626 761 content = self.content
627 762 self.content = self.shell.process_pure_python(content)
628 763
629 764 parts = '\n'.join(self.content).split('\n\n')
630 765
631 766 lines = ['.. code-block:: ipython','']
632 767 figures = []
633 768
634 769 for part in parts:
635
636 770 block = block_parser(part, rgxin, rgxout, promptin, promptout)
637
638 771 if len(block):
639 772 rows, figure = self.shell.process_block(block)
640 773 for row in rows:
641 774 lines.extend([' %s'%line for line in row.split('\n')])
642 775
643 776 if figure is not None:
644 777 figures.append(figure)
645 778
646 #text = '\n'.join(lines)
647 #figs = '\n'.join(figures)
648
649 779 for figure in figures:
650 780 lines.append('')
651 781 lines.extend(figure.split('\n'))
652 782 lines.append('')
653 783
654 #print lines
655 784 if len(lines)>2:
656 785 if debug:
657 786 print('\n'.join(lines))
658 else: #NOTE: this raises some errors, what's it for?
659 #print 'INSERTING %d lines'%len(lines)
787 else:
788 # This has to do with input, not output. But if we comment
789 # these lines out, then no IPython code will appear in the
790 # final output.
660 791 self.state_machine.insert_input(
661 792 lines, self.state_machine.input_lines.source(0))
662 793
663 text = '\n'.join(lines)
664 txtnode = nodes.literal_block(text, text)
665 txtnode['language'] = 'ipython'
666 #imgnode = nodes.image(figs)
667
668 794 # cleanup
669 795 self.teardown()
670 796
671 return []#, imgnode]
797 return []
672 798
673 799 # Enable as a proper Sphinx directive
674 800 def setup(app):
675 801 setup.app = app
676 802
677 803 app.add_directive('ipython', IPythonDirective)
678 app.add_config_value('ipython_savefig_dir', None, True)
804 app.add_config_value('ipython_savefig_dir', None, 'env')
679 805 app.add_config_value('ipython_rgxin',
680 re.compile('In \[(\d+)\]:\s?(.*)\s*'), True)
806 re.compile('In \[(\d+)\]:\s?(.*)\s*'), 'env')
681 807 app.add_config_value('ipython_rgxout',
682 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True)
683 app.add_config_value('ipython_promptin', 'In [%d]:', True)
684 app.add_config_value('ipython_promptout', 'Out[%d]:', True)
808 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), 'env')
809 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
810 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
811
812 # We could just let matplotlib pick whatever is specified as the default
813 # backend in the matplotlibrc file, but this would cause issues if the
814 # backend didn't work in headless environments. For this reason, 'agg'
815 # is a good default backend choice.
816 app.add_config_value('ipython_mplbackend', 'agg', 'env')
817
818 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
819 # __init__ method will treat it as [].
820 execlines = ['import numpy as np', 'import matplotlib.pyplot as plt']
821 app.add_config_value('ipython_execlines', execlines, 'env')
685 822
823 app.add_config_value('ipython_holdcount', True, 'env')
686 824
687 825 # Simple smoke test, needs to be converted to a proper automatic test.
688 826 def test():
689 827
690 828 examples = [
691 829 r"""
692 830 In [9]: pwd
693 831 Out[9]: '/home/jdhunter/py4science/book'
694 832
695 833 In [10]: cd bookdata/
696 834 /home/jdhunter/py4science/book/bookdata
697 835
698 836 In [2]: from pylab import *
699 837
700 838 In [2]: ion()
701 839
702 840 In [3]: im = imread('stinkbug.png')
703 841
704 842 @savefig mystinkbug.png width=4in
705 843 In [4]: imshow(im)
706 844 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
707 845
708 846 """,
709 847 r"""
710 848
711 849 In [1]: x = 'hello world'
712 850
713 851 # string methods can be
714 852 # used to alter the string
715 853 @doctest
716 854 In [2]: x.upper()
717 855 Out[2]: 'HELLO WORLD'
718 856
719 857 @verbatim
720 858 In [3]: x.st<TAB>
721 859 x.startswith x.strip
722 860 """,
723 861 r"""
724 862
725 863 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
726 864 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
727 865
728 866 In [131]: print url.split('&')
729 867 ['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']
730 868
731 869 In [60]: import urllib
732 870
733 871 """,
734 872 r"""\
735 873
736 874 In [133]: import numpy.random
737 875
738 876 @suppress
739 877 In [134]: numpy.random.seed(2358)
740 878
741 879 @doctest
742 880 In [135]: numpy.random.rand(10,2)
743 881 Out[135]:
744 882 array([[ 0.64524308, 0.59943846],
745 883 [ 0.47102322, 0.8715456 ],
746 884 [ 0.29370834, 0.74776844],
747 885 [ 0.99539577, 0.1313423 ],
748 886 [ 0.16250302, 0.21103583],
749 887 [ 0.81626524, 0.1312433 ],
750 888 [ 0.67338089, 0.72302393],
751 889 [ 0.7566368 , 0.07033696],
752 890 [ 0.22591016, 0.77731835],
753 891 [ 0.0072729 , 0.34273127]])
754 892
755 893 """,
756 894
757 895 r"""
758 896 In [106]: print x
759 897 jdh
760 898
761 899 In [109]: for i in range(10):
762 900 .....: print i
763 901 .....:
764 902 .....:
765 903 0
766 904 1
767 905 2
768 906 3
769 907 4
770 908 5
771 909 6
772 910 7
773 911 8
774 912 9
775 913 """,
776 914
777 915 r"""
778 916
779 917 In [144]: from pylab import *
780 918
781 919 In [145]: ion()
782 920
783 921 # use a semicolon to suppress the output
784 922 @savefig test_hist.png width=4in
785 923 In [151]: hist(np.random.randn(10000), 100);
786 924
787 925
788 926 @savefig test_plot.png width=4in
789 927 In [151]: plot(np.random.randn(10000), 'o');
790 928 """,
791 929
792 930 r"""
793 931 # use a semicolon to suppress the output
794 932 In [151]: plt.clf()
795 933
796 934 @savefig plot_simple.png width=4in
797 935 In [151]: plot([1,2,3])
798 936
799 937 @savefig hist_simple.png width=4in
800 938 In [151]: hist(np.random.randn(10000), 100);
801 939
802 940 """,
803 941 r"""
804 942 # update the current fig
805 943 In [151]: ylabel('number')
806 944
807 945 In [152]: title('normal distribution')
808 946
809 947
810 948 @savefig hist_with_text.png
811 949 In [153]: grid(True)
812 950
951 @doctest float
952 In [154]: 0.1 + 0.2
953 Out[154]: 0.3
954
955 @doctest float
956 In [155]: np.arange(16).reshape(4,4)
957 Out[155]:
958 array([[ 0, 1, 2, 3],
959 [ 4, 5, 6, 7],
960 [ 8, 9, 10, 11],
961 [12, 13, 14, 15]])
962
963 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
964
965 In [2]: x[0,0] = np.inf
966
967 In [3]: x[0,1] = np.nan
968
969 @doctest float
970 In [4]: x
971 Out[4]:
972 array([[ inf, nan, 2., 3.],
973 [ 4., 5., 6., 7.],
974 [ 8., 9., 10., 11.],
975 [ 12., 13., 14., 15.]])
976
977
813 978 """,
814 979 ]
815 980 # skip local-file depending first example:
816 981 examples = examples[1:]
817 982
818 983 #ipython_directive.DEBUG = True # dbg
819 984 #options = dict(suppress=True) # dbg
820 985 options = dict()
821 986 for example in examples:
822 987 content = example.split('\n')
823 988 IPythonDirective('debug', arguments=None, options=options,
824 989 content=content, lineno=0,
825 990 content_offset=None, block_text=None,
826 991 state=None, state_machine=None,
827 992 )
828 993
829 994 # Run test suite as a script
830 995 if __name__=='__main__':
831 996 if not os.path.isdir('_static'):
832 997 os.mkdir('_static')
833 998 test()
834 999 print('All OK? Check figures in _static/')
General Comments 0
You need to be logged in to leave comments. Login now