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