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