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