##// END OF EJS Templates
Merge pull request #12342 from blakejgriffin/sphinximage...
Matthias Bussonnier -
r25772:a2f85e9b merge
parent child Browse files
Show More
@@ -1,1251 +1,1251 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 Sphinx directive to support embedded IPython code.
4 4
5 5 IPython provides an extension for `Sphinx <http://www.sphinx-doc.org/>`_ to
6 6 highlight and run code.
7 7
8 8 This directive allows pasting of entire interactive IPython sessions, prompts
9 9 and all, and their code will actually get re-executed at doc build time, with
10 10 all prompts renumbered sequentially. It also allows you to input code as a pure
11 11 python input by giving the argument python to the directive. The output looks
12 12 like an interactive ipython section.
13 13
14 14 Here is an example of how the IPython directive can
15 15 **run** python code, at build time.
16 16
17 17 .. ipython::
18 18
19 19 In [1]: 1+1
20 20
21 21 In [1]: import datetime
22 22 ...: datetime.datetime.now()
23 23
24 24 It supports IPython construct that plain
25 25 Python does not understand (like magics):
26 26
27 27 .. ipython::
28 28
29 29 In [0]: import time
30 30
31 31 In [0]: %timeit time.sleep(0.05)
32 32
33 33 This will also support top-level async when using IPython 7.0+
34 34
35 35 .. ipython::
36 36
37 37 In [2]: import asyncio
38 38 ...: print('before')
39 39 ...: await asyncio.sleep(1)
40 40 ...: print('after')
41 41
42 42
43 43 The namespace will persist across multiple code chucks, Let's define a variable:
44 44
45 45 .. ipython::
46 46
47 47 In [0]: who = "World"
48 48
49 49 And now say hello:
50 50
51 51 .. ipython::
52 52
53 53 In [0]: print('Hello,', who)
54 54
55 55 If the current section raises an exception, you can add the ``:okexcept:`` flag
56 56 to the current block, otherwise the build will fail.
57 57
58 58 .. ipython::
59 59 :okexcept:
60 60
61 61 In [1]: 1/0
62 62
63 63 IPython Sphinx directive module
64 64 ===============================
65 65
66 66 To enable this directive, simply list it in your Sphinx ``conf.py`` file
67 67 (making sure the directory where you placed it is visible to sphinx, as is
68 68 needed for all Sphinx directives). For example, to enable syntax highlighting
69 69 and the IPython directive::
70 70
71 71 extensions = ['IPython.sphinxext.ipython_console_highlighting',
72 72 'IPython.sphinxext.ipython_directive']
73 73
74 74 The IPython directive outputs code-blocks with the language 'ipython'. So
75 75 if you do not have the syntax highlighting extension enabled as well, then
76 76 all rendered code-blocks will be uncolored. By default this directive assumes
77 77 that your prompts are unchanged IPython ones, but this can be customized.
78 78 The configurable options that can be placed in conf.py are:
79 79
80 80 ipython_savefig_dir:
81 81 The directory in which to save the figures. This is relative to the
82 82 Sphinx source directory. The default is `html_static_path`.
83 83 ipython_rgxin:
84 84 The compiled regular expression to denote the start of IPython input
85 85 lines. The default is ``re.compile('In \\[(\\d+)\\]:\\s?(.*)\\s*')``. You
86 86 shouldn't need to change this.
87 87 ipython_warning_is_error: [default to True]
88 88 Fail the build if something unexpected happen, for example if a block raise
89 89 an exception but does not have the `:okexcept:` flag. The exact behavior of
90 90 what is considered strict, may change between the sphinx directive version.
91 91 ipython_rgxout:
92 92 The compiled regular expression to denote the start of IPython output
93 93 lines. The default is ``re.compile('Out\\[(\\d+)\\]:\\s?(.*)\\s*')``. You
94 94 shouldn't need to change this.
95 95 ipython_promptin:
96 96 The string to represent the IPython input prompt in the generated ReST.
97 97 The default is ``'In [%d]:'``. This expects that the line numbers are used
98 98 in the prompt.
99 99 ipython_promptout:
100 100 The string to represent the IPython prompt in the generated ReST. The
101 101 default is ``'Out [%d]:'``. This expects that the line numbers are used
102 102 in the prompt.
103 103 ipython_mplbackend:
104 104 The string which specifies if the embedded Sphinx shell should import
105 105 Matplotlib and set the backend. The value specifies a backend that is
106 106 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
107 107 executed. If not specified in conf.py, then the default value of 'agg' is
108 108 used. To use the IPython directive without matplotlib as a dependency, set
109 109 the value to `None`. It may end up that matplotlib is still imported
110 110 if the user specifies so in `ipython_execlines` or makes use of the
111 111 @savefig pseudo decorator.
112 112 ipython_execlines:
113 113 A list of strings to be exec'd in the embedded Sphinx shell. Typical
114 114 usage is to make certain packages always available. Set this to an empty
115 115 list if you wish to have no imports always available. If specified in
116 116 ``conf.py`` as `None`, then it has the effect of making no imports available.
117 117 If omitted from conf.py altogether, then the default value of
118 118 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
119 119 ipython_holdcount
120 120 When the @suppress pseudo-decorator is used, the execution count can be
121 121 incremented or not. The default behavior is to hold the execution count,
122 122 corresponding to a value of `True`. Set this to `False` to increment
123 123 the execution count after each suppressed command.
124 124
125 125 As an example, to use the IPython directive when `matplotlib` is not available,
126 126 one sets the backend to `None`::
127 127
128 128 ipython_mplbackend = None
129 129
130 130 An example usage of the directive is:
131 131
132 132 .. code-block:: rst
133 133
134 134 .. ipython::
135 135
136 136 In [1]: x = 1
137 137
138 138 In [2]: y = x**2
139 139
140 140 In [3]: print(y)
141 141
142 142 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
143 143 documentation.
144 144
145 145 Pseudo-Decorators
146 146 =================
147 147
148 148 Note: Only one decorator is supported per input. If more than one decorator
149 149 is specified, then only the last one is used.
150 150
151 151 In addition to the Pseudo-Decorators/options described at the above link,
152 152 several enhancements have been made. The directive will emit a message to the
153 153 console at build-time if code-execution resulted in an exception or warning.
154 154 You can suppress these on a per-block basis by specifying the :okexcept:
155 155 or :okwarning: options:
156 156
157 157 .. code-block:: rst
158 158
159 159 .. ipython::
160 160 :okexcept:
161 161 :okwarning:
162 162
163 163 In [1]: 1/0
164 164 In [2]: # raise warning.
165 165
166 166 To Do
167 167 =====
168 168
169 169 - Turn the ad-hoc test() function into a real test suite.
170 170 - Break up ipython-specific functionality from matplotlib stuff into better
171 171 separated code.
172 172
173 173 """
174 174
175 175 # Authors
176 176 # =======
177 177 #
178 178 # - John D Hunter: original author.
179 179 # - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
180 180 # - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
181 181 # - Skipper Seabold, refactoring, cleanups, pure python addition
182 182
183 183 #-----------------------------------------------------------------------------
184 184 # Imports
185 185 #-----------------------------------------------------------------------------
186 186
187 187 # Stdlib
188 188 import atexit
189 189 import errno
190 190 import os
191 191 import pathlib
192 192 import re
193 193 import sys
194 194 import tempfile
195 195 import ast
196 196 import warnings
197 197 import shutil
198 198 from io import StringIO
199 199
200 200 # Third-party
201 201 from docutils.parsers.rst import directives
202 202 from docutils.parsers.rst import Directive
203 203
204 204 # Our own
205 205 from traitlets.config import Config
206 206 from IPython import InteractiveShell
207 207 from IPython.core.profiledir import ProfileDir
208 208
209 209 use_matplotlib = False
210 210 try:
211 211 import matplotlib
212 212 use_matplotlib = True
213 213 except Exception:
214 214 pass
215 215
216 216 #-----------------------------------------------------------------------------
217 217 # Globals
218 218 #-----------------------------------------------------------------------------
219 219 # for tokenizing blocks
220 220 COMMENT, INPUT, OUTPUT = range(3)
221 221
222 222 #-----------------------------------------------------------------------------
223 223 # Functions and class declarations
224 224 #-----------------------------------------------------------------------------
225 225
226 226 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
227 227 """
228 228 part is a string of ipython text, comprised of at most one
229 229 input, one output, comments, and blank lines. The block parser
230 230 parses the text into a list of::
231 231
232 232 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
233 233
234 234 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
235 235 data is, depending on the type of token::
236 236
237 237 COMMENT : the comment string
238 238
239 239 INPUT: the (DECORATOR, INPUT_LINE, REST) where
240 240 DECORATOR: the input decorator (or None)
241 241 INPUT_LINE: the input as string (possibly multi-line)
242 242 REST : any stdout generated by the input line (not OUTPUT)
243 243
244 244 OUTPUT: the output string, possibly multi-line
245 245
246 246 """
247 247 block = []
248 248 lines = part.split('\n')
249 249 N = len(lines)
250 250 i = 0
251 251 decorator = None
252 252 while 1:
253 253
254 254 if i==N:
255 255 # nothing left to parse -- the last line
256 256 break
257 257
258 258 line = lines[i]
259 259 i += 1
260 260 line_stripped = line.strip()
261 261 if line_stripped.startswith('#'):
262 262 block.append((COMMENT, line))
263 263 continue
264 264
265 265 if line_stripped.startswith('@'):
266 266 # Here is where we assume there is, at most, one decorator.
267 267 # Might need to rethink this.
268 268 decorator = line_stripped
269 269 continue
270 270
271 271 # does this look like an input line?
272 272 matchin = rgxin.match(line)
273 273 if matchin:
274 274 lineno, inputline = int(matchin.group(1)), matchin.group(2)
275 275
276 276 # the ....: continuation string
277 277 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
278 278 Nc = len(continuation)
279 279 # input lines can continue on for more than one line, if
280 280 # we have a '\' line continuation char or a function call
281 281 # echo line 'print'. The input line can only be
282 282 # terminated by the end of the block or an output line, so
283 283 # we parse out the rest of the input line if it is
284 284 # multiline as well as any echo text
285 285
286 286 rest = []
287 287 while i<N:
288 288
289 289 # look ahead; if the next line is blank, or a comment, or
290 290 # an output line, we're done
291 291
292 292 nextline = lines[i]
293 293 matchout = rgxout.match(nextline)
294 294 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
295 295 if matchout or nextline.startswith('#'):
296 296 break
297 297 elif nextline.startswith(continuation):
298 298 # The default ipython_rgx* treat the space following the colon as optional.
299 299 # However, If the space is there we must consume it or code
300 300 # employing the cython_magic extension will fail to execute.
301 301 #
302 302 # This works with the default ipython_rgx* patterns,
303 303 # If you modify them, YMMV.
304 304 nextline = nextline[Nc:]
305 305 if nextline and nextline[0] == ' ':
306 306 nextline = nextline[1:]
307 307
308 308 inputline += '\n' + nextline
309 309 else:
310 310 rest.append(nextline)
311 311 i+= 1
312 312
313 313 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
314 314 continue
315 315
316 316 # if it looks like an output line grab all the text to the end
317 317 # of the block
318 318 matchout = rgxout.match(line)
319 319 if matchout:
320 320 lineno, output = int(matchout.group(1)), matchout.group(2)
321 321 if i<N-1:
322 322 output = '\n'.join([output] + lines[i:])
323 323
324 324 block.append((OUTPUT, output))
325 325 break
326 326
327 327 return block
328 328
329 329
330 330 class EmbeddedSphinxShell(object):
331 331 """An embedded IPython instance to run inside Sphinx"""
332 332
333 333 def __init__(self, exec_lines=None):
334 334
335 335 self.cout = StringIO()
336 336
337 337 if exec_lines is None:
338 338 exec_lines = []
339 339
340 340 # Create config object for IPython
341 341 config = Config()
342 342 config.HistoryManager.hist_file = ':memory:'
343 343 config.InteractiveShell.autocall = False
344 344 config.InteractiveShell.autoindent = False
345 345 config.InteractiveShell.colors = 'NoColor'
346 346
347 347 # create a profile so instance history isn't saved
348 348 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
349 349 profname = 'auto_profile_sphinx_build'
350 350 pdir = os.path.join(tmp_profile_dir,profname)
351 351 profile = ProfileDir.create_profile_dir(pdir)
352 352
353 353 # Create and initialize global ipython, but don't start its mainloop.
354 354 # This will persist across different EmbeddedSphinxShell instances.
355 355 IP = InteractiveShell.instance(config=config, profile_dir=profile)
356 356 atexit.register(self.cleanup)
357 357
358 358 # Store a few parts of IPython we'll need.
359 359 self.IP = IP
360 360 self.user_ns = self.IP.user_ns
361 361 self.user_global_ns = self.IP.user_global_ns
362 362
363 363 self.input = ''
364 364 self.output = ''
365 365 self.tmp_profile_dir = tmp_profile_dir
366 366
367 367 self.is_verbatim = False
368 368 self.is_doctest = False
369 369 self.is_suppress = False
370 370
371 371 # Optionally, provide more detailed information to shell.
372 372 # this is assigned by the SetUp method of IPythonDirective
373 373 # to point at itself.
374 374 #
375 375 # So, you can access handy things at self.directive.state
376 376 self.directive = None
377 377
378 378 # on the first call to the savefig decorator, we'll import
379 379 # pyplot as plt so we can make a call to the plt.gcf().savefig
380 380 self._pyplot_imported = False
381 381
382 382 # Prepopulate the namespace.
383 383 for line in exec_lines:
384 384 self.process_input_line(line, store_history=False)
385 385
386 386 def cleanup(self):
387 387 shutil.rmtree(self.tmp_profile_dir, ignore_errors=True)
388 388
389 389 def clear_cout(self):
390 390 self.cout.seek(0)
391 391 self.cout.truncate(0)
392 392
393 393 def process_input_line(self, line, store_history):
394 394 return self.process_input_lines([line], store_history=store_history)
395 395
396 396 def process_input_lines(self, lines, store_history=True):
397 397 """process the input, capturing stdout"""
398 398 stdout = sys.stdout
399 399 source_raw = '\n'.join(lines)
400 400 try:
401 401 sys.stdout = self.cout
402 402 self.IP.run_cell(source_raw, store_history=store_history)
403 403 finally:
404 404 sys.stdout = stdout
405 405
406 406 def process_image(self, decorator):
407 407 """
408 408 # build out an image directive like
409 409 # .. image:: somefile.png
410 410 # :width 4in
411 411 #
412 412 # from an input like
413 413 # savefig somefile.png width=4in
414 414 """
415 415 savefig_dir = self.savefig_dir
416 416 source_dir = self.source_dir
417 417 saveargs = decorator.split(' ')
418 418 filename = saveargs[1]
419 419 # insert relative path to image file in source
420 420 # as absolute path for Sphinx
421 421 # sphinx expects a posix path, even on Windows
422 posix_path = pathlib.Path(savefig_dir,filename).as_posix()
423 outfile = '/' + os.path.relpath(posix_path, source_dir)
422 path = pathlib.Path(savefig_dir, filename)
423 outfile = '/' + path.relative_to(source_dir).as_posix()
424 424
425 425 imagerows = ['.. image:: %s' % outfile]
426 426
427 427 for kwarg in saveargs[2:]:
428 428 arg, val = kwarg.split('=')
429 429 arg = arg.strip()
430 430 val = val.strip()
431 431 imagerows.append(' :%s: %s'%(arg, val))
432 432
433 433 image_file = os.path.basename(outfile) # only return file name
434 434 image_directive = '\n'.join(imagerows)
435 435 return image_file, image_directive
436 436
437 437 # Callbacks for each type of token
438 438 def process_input(self, data, input_prompt, lineno):
439 439 """
440 440 Process data block for INPUT token.
441 441
442 442 """
443 443 decorator, input, rest = data
444 444 image_file = None
445 445 image_directive = None
446 446
447 447 is_verbatim = decorator=='@verbatim' or self.is_verbatim
448 448 is_doctest = (decorator is not None and \
449 449 decorator.startswith('@doctest')) or self.is_doctest
450 450 is_suppress = decorator=='@suppress' or self.is_suppress
451 451 is_okexcept = decorator=='@okexcept' or self.is_okexcept
452 452 is_okwarning = decorator=='@okwarning' or self.is_okwarning
453 453 is_savefig = decorator is not None and \
454 454 decorator.startswith('@savefig')
455 455
456 456 input_lines = input.split('\n')
457 457 if len(input_lines) > 1:
458 458 if input_lines[-1] != "":
459 459 input_lines.append('') # make sure there's a blank line
460 460 # so splitter buffer gets reset
461 461
462 462 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
463 463
464 464 if is_savefig:
465 465 image_file, image_directive = self.process_image(decorator)
466 466
467 467 ret = []
468 468 is_semicolon = False
469 469
470 470 # Hold the execution count, if requested to do so.
471 471 if is_suppress and self.hold_count:
472 472 store_history = False
473 473 else:
474 474 store_history = True
475 475
476 476 # Note: catch_warnings is not thread safe
477 477 with warnings.catch_warnings(record=True) as ws:
478 478 if input_lines[0].endswith(';'):
479 479 is_semicolon = True
480 480 #for i, line in enumerate(input_lines):
481 481
482 482 # process the first input line
483 483 if is_verbatim:
484 484 self.process_input_lines([''])
485 485 self.IP.execution_count += 1 # increment it anyway
486 486 else:
487 487 # only submit the line in non-verbatim mode
488 488 self.process_input_lines(input_lines, store_history=store_history)
489 489
490 490 if not is_suppress:
491 491 for i, line in enumerate(input_lines):
492 492 if i == 0:
493 493 formatted_line = '%s %s'%(input_prompt, line)
494 494 else:
495 495 formatted_line = '%s %s'%(continuation, line)
496 496 ret.append(formatted_line)
497 497
498 498 if not is_suppress and len(rest.strip()) and is_verbatim:
499 499 # The "rest" is the standard output of the input. This needs to be
500 500 # added when in verbatim mode. If there is no "rest", then we don't
501 501 # add it, as the new line will be added by the processed output.
502 502 ret.append(rest)
503 503
504 504 # Fetch the processed output. (This is not the submitted output.)
505 505 self.cout.seek(0)
506 506 processed_output = self.cout.read()
507 507 if not is_suppress and not is_semicolon:
508 508 #
509 509 # In IPythonDirective.run, the elements of `ret` are eventually
510 510 # combined such that '' entries correspond to newlines. So if
511 511 # `processed_output` is equal to '', then the adding it to `ret`
512 512 # ensures that there is a blank line between consecutive inputs
513 513 # that have no outputs, as in:
514 514 #
515 515 # In [1]: x = 4
516 516 #
517 517 # In [2]: x = 5
518 518 #
519 519 # When there is processed output, it has a '\n' at the tail end. So
520 520 # adding the output to `ret` will provide the necessary spacing
521 521 # between consecutive input/output blocks, as in:
522 522 #
523 523 # In [1]: x
524 524 # Out[1]: 5
525 525 #
526 526 # In [2]: x
527 527 # Out[2]: 5
528 528 #
529 529 # When there is stdout from the input, it also has a '\n' at the
530 530 # tail end, and so this ensures proper spacing as well. E.g.:
531 531 #
532 532 # In [1]: print x
533 533 # 5
534 534 #
535 535 # In [2]: x = 5
536 536 #
537 537 # When in verbatim mode, `processed_output` is empty (because
538 538 # nothing was passed to IP. Sometimes the submitted code block has
539 539 # an Out[] portion and sometimes it does not. When it does not, we
540 540 # need to ensure proper spacing, so we have to add '' to `ret`.
541 541 # However, if there is an Out[] in the submitted code, then we do
542 542 # not want to add a newline as `process_output` has stuff to add.
543 543 # The difficulty is that `process_input` doesn't know if
544 544 # `process_output` will be called---so it doesn't know if there is
545 545 # Out[] in the code block. The requires that we include a hack in
546 546 # `process_block`. See the comments there.
547 547 #
548 548 ret.append(processed_output)
549 549 elif is_semicolon:
550 550 # Make sure there is a newline after the semicolon.
551 551 ret.append('')
552 552
553 553 # context information
554 554 filename = "Unknown"
555 555 lineno = 0
556 556 if self.directive.state:
557 557 filename = self.directive.state.document.current_source
558 558 lineno = self.directive.state.document.current_line
559 559
560 560 # output any exceptions raised during execution to stdout
561 561 # unless :okexcept: has been specified.
562 562 if not is_okexcept and (("Traceback" in processed_output) or ("SyntaxError" in processed_output)):
563 563 s = "\nException in %s at block ending on line %s\n" % (filename, lineno)
564 564 s += "Specify :okexcept: as an option in the ipython:: block to suppress this message\n"
565 565 sys.stdout.write('\n\n>>>' + ('-' * 73))
566 566 sys.stdout.write(s)
567 567 sys.stdout.write(processed_output)
568 568 sys.stdout.write('<<<' + ('-' * 73) + '\n\n')
569 569 if self.warning_is_error:
570 570 raise RuntimeError('Non Expected exception in `{}` line {}'.format(filename, lineno))
571 571
572 572 # output any warning raised during execution to stdout
573 573 # unless :okwarning: has been specified.
574 574 if not is_okwarning:
575 575 for w in ws:
576 576 s = "\nWarning in %s at block ending on line %s\n" % (filename, lineno)
577 577 s += "Specify :okwarning: as an option in the ipython:: block to suppress this message\n"
578 578 sys.stdout.write('\n\n>>>' + ('-' * 73))
579 579 sys.stdout.write(s)
580 580 sys.stdout.write(('-' * 76) + '\n')
581 581 s=warnings.formatwarning(w.message, w.category,
582 582 w.filename, w.lineno, w.line)
583 583 sys.stdout.write(s)
584 584 sys.stdout.write('<<<' + ('-' * 73) + '\n')
585 585 if self.warning_is_error:
586 586 raise RuntimeError('Non Expected warning in `{}` line {}'.format(filename, lineno))
587 587
588 588 self.clear_cout()
589 589 return (ret, input_lines, processed_output,
590 590 is_doctest, decorator, image_file, image_directive)
591 591
592 592
593 593 def process_output(self, data, output_prompt, input_lines, output,
594 594 is_doctest, decorator, image_file):
595 595 """
596 596 Process data block for OUTPUT token.
597 597
598 598 """
599 599 # Recall: `data` is the submitted output, and `output` is the processed
600 600 # output from `input_lines`.
601 601
602 602 TAB = ' ' * 4
603 603
604 604 if is_doctest and output is not None:
605 605
606 606 found = output # This is the processed output
607 607 found = found.strip()
608 608 submitted = data.strip()
609 609
610 610 if self.directive is None:
611 611 source = 'Unavailable'
612 612 content = 'Unavailable'
613 613 else:
614 614 source = self.directive.state.document.current_source
615 615 content = self.directive.content
616 616 # Add tabs and join into a single string.
617 617 content = '\n'.join([TAB + line for line in content])
618 618
619 619 # Make sure the output contains the output prompt.
620 620 ind = found.find(output_prompt)
621 621 if ind < 0:
622 622 e = ('output does not contain output prompt\n\n'
623 623 'Document source: {0}\n\n'
624 624 'Raw content: \n{1}\n\n'
625 625 'Input line(s):\n{TAB}{2}\n\n'
626 626 'Output line(s):\n{TAB}{3}\n\n')
627 627 e = e.format(source, content, '\n'.join(input_lines),
628 628 repr(found), TAB=TAB)
629 629 raise RuntimeError(e)
630 630 found = found[len(output_prompt):].strip()
631 631
632 632 # Handle the actual doctest comparison.
633 633 if decorator.strip() == '@doctest':
634 634 # Standard doctest
635 635 if found != submitted:
636 636 e = ('doctest failure\n\n'
637 637 'Document source: {0}\n\n'
638 638 'Raw content: \n{1}\n\n'
639 639 'On input line(s):\n{TAB}{2}\n\n'
640 640 'we found output:\n{TAB}{3}\n\n'
641 641 'instead of the expected:\n{TAB}{4}\n\n')
642 642 e = e.format(source, content, '\n'.join(input_lines),
643 643 repr(found), repr(submitted), TAB=TAB)
644 644 raise RuntimeError(e)
645 645 else:
646 646 self.custom_doctest(decorator, input_lines, found, submitted)
647 647
648 648 # When in verbatim mode, this holds additional submitted output
649 649 # to be written in the final Sphinx output.
650 650 # https://github.com/ipython/ipython/issues/5776
651 651 out_data = []
652 652
653 653 is_verbatim = decorator=='@verbatim' or self.is_verbatim
654 654 if is_verbatim and data.strip():
655 655 # Note that `ret` in `process_block` has '' as its last element if
656 656 # the code block was in verbatim mode. So if there is no submitted
657 657 # output, then we will have proper spacing only if we do not add
658 658 # an additional '' to `out_data`. This is why we condition on
659 659 # `and data.strip()`.
660 660
661 661 # The submitted output has no output prompt. If we want the
662 662 # prompt and the code to appear, we need to join them now
663 663 # instead of adding them separately---as this would create an
664 664 # undesired newline. How we do this ultimately depends on the
665 665 # format of the output regex. I'll do what works for the default
666 666 # prompt for now, and we might have to adjust if it doesn't work
667 667 # in other cases. Finally, the submitted output does not have
668 668 # a trailing newline, so we must add it manually.
669 669 out_data.append("{0} {1}\n".format(output_prompt, data))
670 670
671 671 return out_data
672 672
673 673 def process_comment(self, data):
674 674 """Process data fPblock for COMMENT token."""
675 675 if not self.is_suppress:
676 676 return [data]
677 677
678 678 def save_image(self, image_file):
679 679 """
680 680 Saves the image file to disk.
681 681 """
682 682 self.ensure_pyplot()
683 683 command = 'plt.gcf().savefig("%s")'%image_file
684 684 #print 'SAVEFIG', command # dbg
685 685 self.process_input_line('bookmark ipy_thisdir', store_history=False)
686 686 self.process_input_line('cd -b ipy_savedir', store_history=False)
687 687 self.process_input_line(command, store_history=False)
688 688 self.process_input_line('cd -b ipy_thisdir', store_history=False)
689 689 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
690 690 self.clear_cout()
691 691
692 692 def process_block(self, block):
693 693 """
694 694 process block from the block_parser and return a list of processed lines
695 695 """
696 696 ret = []
697 697 output = None
698 698 input_lines = None
699 699 lineno = self.IP.execution_count
700 700
701 701 input_prompt = self.promptin % lineno
702 702 output_prompt = self.promptout % lineno
703 703 image_file = None
704 704 image_directive = None
705 705
706 706 found_input = False
707 707 for token, data in block:
708 708 if token == COMMENT:
709 709 out_data = self.process_comment(data)
710 710 elif token == INPUT:
711 711 found_input = True
712 712 (out_data, input_lines, output, is_doctest,
713 713 decorator, image_file, image_directive) = \
714 714 self.process_input(data, input_prompt, lineno)
715 715 elif token == OUTPUT:
716 716 if not found_input:
717 717
718 718 TAB = ' ' * 4
719 719 linenumber = 0
720 720 source = 'Unavailable'
721 721 content = 'Unavailable'
722 722 if self.directive:
723 723 linenumber = self.directive.state.document.current_line
724 724 source = self.directive.state.document.current_source
725 725 content = self.directive.content
726 726 # Add tabs and join into a single string.
727 727 content = '\n'.join([TAB + line for line in content])
728 728
729 729 e = ('\n\nInvalid block: Block contains an output prompt '
730 730 'without an input prompt.\n\n'
731 731 'Document source: {0}\n\n'
732 732 'Content begins at line {1}: \n\n{2}\n\n'
733 733 'Problematic block within content: \n\n{TAB}{3}\n\n')
734 734 e = e.format(source, linenumber, content, block, TAB=TAB)
735 735
736 736 # Write, rather than include in exception, since Sphinx
737 737 # will truncate tracebacks.
738 738 sys.stdout.write(e)
739 739 raise RuntimeError('An invalid block was detected.')
740 740 out_data = \
741 741 self.process_output(data, output_prompt, input_lines,
742 742 output, is_doctest, decorator,
743 743 image_file)
744 744 if out_data:
745 745 # Then there was user submitted output in verbatim mode.
746 746 # We need to remove the last element of `ret` that was
747 747 # added in `process_input`, as it is '' and would introduce
748 748 # an undesirable newline.
749 749 assert(ret[-1] == '')
750 750 del ret[-1]
751 751
752 752 if out_data:
753 753 ret.extend(out_data)
754 754
755 755 # save the image files
756 756 if image_file is not None:
757 757 self.save_image(image_file)
758 758
759 759 return ret, image_directive
760 760
761 761 def ensure_pyplot(self):
762 762 """
763 763 Ensures that pyplot has been imported into the embedded IPython shell.
764 764
765 765 Also, makes sure to set the backend appropriately if not set already.
766 766
767 767 """
768 768 # We are here if the @figure pseudo decorator was used. Thus, it's
769 769 # possible that we could be here even if python_mplbackend were set to
770 770 # `None`. That's also strange and perhaps worthy of raising an
771 771 # exception, but for now, we just set the backend to 'agg'.
772 772
773 773 if not self._pyplot_imported:
774 774 if 'matplotlib.backends' not in sys.modules:
775 775 # Then ipython_matplotlib was set to None but there was a
776 776 # call to the @figure decorator (and ipython_execlines did
777 777 # not set a backend).
778 778 #raise Exception("No backend was set, but @figure was used!")
779 779 import matplotlib
780 780 matplotlib.use('agg')
781 781
782 782 # Always import pyplot into embedded shell.
783 783 self.process_input_line('import matplotlib.pyplot as plt',
784 784 store_history=False)
785 785 self._pyplot_imported = True
786 786
787 787 def process_pure_python(self, content):
788 788 """
789 789 content is a list of strings. it is unedited directive content
790 790
791 791 This runs it line by line in the InteractiveShell, prepends
792 792 prompts as needed capturing stderr and stdout, then returns
793 793 the content as a list as if it were ipython code
794 794 """
795 795 output = []
796 796 savefig = False # keep up with this to clear figure
797 797 multiline = False # to handle line continuation
798 798 multiline_start = None
799 799 fmtin = self.promptin
800 800
801 801 ct = 0
802 802
803 803 for lineno, line in enumerate(content):
804 804
805 805 line_stripped = line.strip()
806 806 if not len(line):
807 807 output.append(line)
808 808 continue
809 809
810 810 # handle decorators
811 811 if line_stripped.startswith('@'):
812 812 output.extend([line])
813 813 if 'savefig' in line:
814 814 savefig = True # and need to clear figure
815 815 continue
816 816
817 817 # handle comments
818 818 if line_stripped.startswith('#'):
819 819 output.extend([line])
820 820 continue
821 821
822 822 # deal with lines checking for multiline
823 823 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
824 824 if not multiline:
825 825 modified = u"%s %s" % (fmtin % ct, line_stripped)
826 826 output.append(modified)
827 827 ct += 1
828 828 try:
829 829 ast.parse(line_stripped)
830 830 output.append(u'')
831 831 except Exception: # on a multiline
832 832 multiline = True
833 833 multiline_start = lineno
834 834 else: # still on a multiline
835 835 modified = u'%s %s' % (continuation, line)
836 836 output.append(modified)
837 837
838 838 # if the next line is indented, it should be part of multiline
839 839 if len(content) > lineno + 1:
840 840 nextline = content[lineno + 1]
841 841 if len(nextline) - len(nextline.lstrip()) > 3:
842 842 continue
843 843 try:
844 844 mod = ast.parse(
845 845 '\n'.join(content[multiline_start:lineno+1]))
846 846 if isinstance(mod.body[0], ast.FunctionDef):
847 847 # check to see if we have the whole function
848 848 for element in mod.body[0].body:
849 849 if isinstance(element, ast.Return):
850 850 multiline = False
851 851 else:
852 852 output.append(u'')
853 853 multiline = False
854 854 except Exception:
855 855 pass
856 856
857 857 if savefig: # clear figure if plotted
858 858 self.ensure_pyplot()
859 859 self.process_input_line('plt.clf()', store_history=False)
860 860 self.clear_cout()
861 861 savefig = False
862 862
863 863 return output
864 864
865 865 def custom_doctest(self, decorator, input_lines, found, submitted):
866 866 """
867 867 Perform a specialized doctest.
868 868
869 869 """
870 870 from .custom_doctests import doctests
871 871
872 872 args = decorator.split()
873 873 doctest_type = args[1]
874 874 if doctest_type in doctests:
875 875 doctests[doctest_type](self, args, input_lines, found, submitted)
876 876 else:
877 877 e = "Invalid option to @doctest: {0}".format(doctest_type)
878 878 raise Exception(e)
879 879
880 880
881 881 class IPythonDirective(Directive):
882 882
883 883 has_content = True
884 884 required_arguments = 0
885 885 optional_arguments = 4 # python, suppress, verbatim, doctest
886 886 final_argumuent_whitespace = True
887 887 option_spec = { 'python': directives.unchanged,
888 888 'suppress' : directives.flag,
889 889 'verbatim' : directives.flag,
890 890 'doctest' : directives.flag,
891 891 'okexcept': directives.flag,
892 892 'okwarning': directives.flag
893 893 }
894 894
895 895 shell = None
896 896
897 897 seen_docs = set()
898 898
899 899 def get_config_options(self):
900 900 # contains sphinx configuration variables
901 901 config = self.state.document.settings.env.config
902 902
903 903 # get config variables to set figure output directory
904 904 savefig_dir = config.ipython_savefig_dir
905 905 source_dir = self.state.document.settings.env.srcdir
906 906 savefig_dir = os.path.join(source_dir, savefig_dir)
907 907
908 908 # get regex and prompt stuff
909 909 rgxin = config.ipython_rgxin
910 910 rgxout = config.ipython_rgxout
911 911 warning_is_error= config.ipython_warning_is_error
912 912 promptin = config.ipython_promptin
913 913 promptout = config.ipython_promptout
914 914 mplbackend = config.ipython_mplbackend
915 915 exec_lines = config.ipython_execlines
916 916 hold_count = config.ipython_holdcount
917 917
918 918 return (savefig_dir, source_dir, rgxin, rgxout,
919 919 promptin, promptout, mplbackend, exec_lines, hold_count, warning_is_error)
920 920
921 921 def setup(self):
922 922 # Get configuration values.
923 923 (savefig_dir, source_dir, rgxin, rgxout, promptin, promptout,
924 924 mplbackend, exec_lines, hold_count, warning_is_error) = self.get_config_options()
925 925
926 926 try:
927 927 os.makedirs(savefig_dir)
928 928 except OSError as e:
929 929 if e.errno != errno.EEXIST:
930 930 raise
931 931
932 932 if self.shell is None:
933 933 # We will be here many times. However, when the
934 934 # EmbeddedSphinxShell is created, its interactive shell member
935 935 # is the same for each instance.
936 936
937 937 if mplbackend and 'matplotlib.backends' not in sys.modules and use_matplotlib:
938 938 import matplotlib
939 939 matplotlib.use(mplbackend)
940 940
941 941 # Must be called after (potentially) importing matplotlib and
942 942 # setting its backend since exec_lines might import pylab.
943 943 self.shell = EmbeddedSphinxShell(exec_lines)
944 944
945 945 # Store IPython directive to enable better error messages
946 946 self.shell.directive = self
947 947
948 948 # reset the execution count if we haven't processed this doc
949 949 #NOTE: this may be borked if there are multiple seen_doc tmp files
950 950 #check time stamp?
951 951 if not self.state.document.current_source in self.seen_docs:
952 952 self.shell.IP.history_manager.reset()
953 953 self.shell.IP.execution_count = 1
954 954 self.seen_docs.add(self.state.document.current_source)
955 955
956 956 # and attach to shell so we don't have to pass them around
957 957 self.shell.rgxin = rgxin
958 958 self.shell.rgxout = rgxout
959 959 self.shell.promptin = promptin
960 960 self.shell.promptout = promptout
961 961 self.shell.savefig_dir = savefig_dir
962 962 self.shell.source_dir = source_dir
963 963 self.shell.hold_count = hold_count
964 964 self.shell.warning_is_error = warning_is_error
965 965
966 966 # setup bookmark for saving figures directory
967 967 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
968 968 store_history=False)
969 969 self.shell.clear_cout()
970 970
971 971 return rgxin, rgxout, promptin, promptout
972 972
973 973 def teardown(self):
974 974 # delete last bookmark
975 975 self.shell.process_input_line('bookmark -d ipy_savedir',
976 976 store_history=False)
977 977 self.shell.clear_cout()
978 978
979 979 def run(self):
980 980 debug = False
981 981
982 982 #TODO, any reason block_parser can't be a method of embeddable shell
983 983 # then we wouldn't have to carry these around
984 984 rgxin, rgxout, promptin, promptout = self.setup()
985 985
986 986 options = self.options
987 987 self.shell.is_suppress = 'suppress' in options
988 988 self.shell.is_doctest = 'doctest' in options
989 989 self.shell.is_verbatim = 'verbatim' in options
990 990 self.shell.is_okexcept = 'okexcept' in options
991 991 self.shell.is_okwarning = 'okwarning' in options
992 992
993 993 # handle pure python code
994 994 if 'python' in self.arguments:
995 995 content = self.content
996 996 self.content = self.shell.process_pure_python(content)
997 997
998 998 # parts consists of all text within the ipython-block.
999 999 # Each part is an input/output block.
1000 1000 parts = '\n'.join(self.content).split('\n\n')
1001 1001
1002 1002 lines = ['.. code-block:: ipython', '']
1003 1003 figures = []
1004 1004
1005 1005 for part in parts:
1006 1006 block = block_parser(part, rgxin, rgxout, promptin, promptout)
1007 1007 if len(block):
1008 1008 rows, figure = self.shell.process_block(block)
1009 1009 for row in rows:
1010 1010 lines.extend([' {0}'.format(line)
1011 1011 for line in row.split('\n')])
1012 1012
1013 1013 if figure is not None:
1014 1014 figures.append(figure)
1015 1015 else:
1016 1016 message = 'Code input with no code at {}, line {}'\
1017 1017 .format(
1018 1018 self.state.document.current_source,
1019 1019 self.state.document.current_line)
1020 1020 if self.shell.warning_is_error:
1021 1021 raise RuntimeError(message)
1022 1022 else:
1023 1023 warnings.warn(message)
1024 1024
1025 1025 for figure in figures:
1026 1026 lines.append('')
1027 1027 lines.extend(figure.split('\n'))
1028 1028 lines.append('')
1029 1029
1030 1030 if len(lines) > 2:
1031 1031 if debug:
1032 1032 print('\n'.join(lines))
1033 1033 else:
1034 1034 # This has to do with input, not output. But if we comment
1035 1035 # these lines out, then no IPython code will appear in the
1036 1036 # final output.
1037 1037 self.state_machine.insert_input(
1038 1038 lines, self.state_machine.input_lines.source(0))
1039 1039
1040 1040 # cleanup
1041 1041 self.teardown()
1042 1042
1043 1043 return []
1044 1044
1045 1045 # Enable as a proper Sphinx directive
1046 1046 def setup(app):
1047 1047 setup.app = app
1048 1048
1049 1049 app.add_directive('ipython', IPythonDirective)
1050 1050 app.add_config_value('ipython_savefig_dir', 'savefig', 'env')
1051 1051 app.add_config_value('ipython_warning_is_error', True, 'env')
1052 1052 app.add_config_value('ipython_rgxin',
1053 1053 re.compile(r'In \[(\d+)\]:\s?(.*)\s*'), 'env')
1054 1054 app.add_config_value('ipython_rgxout',
1055 1055 re.compile(r'Out\[(\d+)\]:\s?(.*)\s*'), 'env')
1056 1056 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
1057 1057 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
1058 1058
1059 1059 # We could just let matplotlib pick whatever is specified as the default
1060 1060 # backend in the matplotlibrc file, but this would cause issues if the
1061 1061 # backend didn't work in headless environments. For this reason, 'agg'
1062 1062 # is a good default backend choice.
1063 1063 app.add_config_value('ipython_mplbackend', 'agg', 'env')
1064 1064
1065 1065 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
1066 1066 # __init__ method will treat it as [].
1067 1067 execlines = ['import numpy as np']
1068 1068 if use_matplotlib:
1069 1069 execlines.append('import matplotlib.pyplot as plt')
1070 1070 app.add_config_value('ipython_execlines', execlines, 'env')
1071 1071
1072 1072 app.add_config_value('ipython_holdcount', True, 'env')
1073 1073
1074 1074 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
1075 1075 return metadata
1076 1076
1077 1077 # Simple smoke test, needs to be converted to a proper automatic test.
1078 1078 def test():
1079 1079
1080 1080 examples = [
1081 1081 r"""
1082 1082 In [9]: pwd
1083 1083 Out[9]: '/home/jdhunter/py4science/book'
1084 1084
1085 1085 In [10]: cd bookdata/
1086 1086 /home/jdhunter/py4science/book/bookdata
1087 1087
1088 1088 In [2]: from pylab import *
1089 1089
1090 1090 In [2]: ion()
1091 1091
1092 1092 In [3]: im = imread('stinkbug.png')
1093 1093
1094 1094 @savefig mystinkbug.png width=4in
1095 1095 In [4]: imshow(im)
1096 1096 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
1097 1097
1098 1098 """,
1099 1099 r"""
1100 1100
1101 1101 In [1]: x = 'hello world'
1102 1102
1103 1103 # string methods can be
1104 1104 # used to alter the string
1105 1105 @doctest
1106 1106 In [2]: x.upper()
1107 1107 Out[2]: 'HELLO WORLD'
1108 1108
1109 1109 @verbatim
1110 1110 In [3]: x.st<TAB>
1111 1111 x.startswith x.strip
1112 1112 """,
1113 1113 r"""
1114 1114
1115 1115 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
1116 1116 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
1117 1117
1118 1118 In [131]: print url.split('&')
1119 1119 ['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']
1120 1120
1121 1121 In [60]: import urllib
1122 1122
1123 1123 """,
1124 1124 r"""\
1125 1125
1126 1126 In [133]: import numpy.random
1127 1127
1128 1128 @suppress
1129 1129 In [134]: numpy.random.seed(2358)
1130 1130
1131 1131 @doctest
1132 1132 In [135]: numpy.random.rand(10,2)
1133 1133 Out[135]:
1134 1134 array([[ 0.64524308, 0.59943846],
1135 1135 [ 0.47102322, 0.8715456 ],
1136 1136 [ 0.29370834, 0.74776844],
1137 1137 [ 0.99539577, 0.1313423 ],
1138 1138 [ 0.16250302, 0.21103583],
1139 1139 [ 0.81626524, 0.1312433 ],
1140 1140 [ 0.67338089, 0.72302393],
1141 1141 [ 0.7566368 , 0.07033696],
1142 1142 [ 0.22591016, 0.77731835],
1143 1143 [ 0.0072729 , 0.34273127]])
1144 1144
1145 1145 """,
1146 1146
1147 1147 r"""
1148 1148 In [106]: print x
1149 1149 jdh
1150 1150
1151 1151 In [109]: for i in range(10):
1152 1152 .....: print i
1153 1153 .....:
1154 1154 .....:
1155 1155 0
1156 1156 1
1157 1157 2
1158 1158 3
1159 1159 4
1160 1160 5
1161 1161 6
1162 1162 7
1163 1163 8
1164 1164 9
1165 1165 """,
1166 1166
1167 1167 r"""
1168 1168
1169 1169 In [144]: from pylab import *
1170 1170
1171 1171 In [145]: ion()
1172 1172
1173 1173 # use a semicolon to suppress the output
1174 1174 @savefig test_hist.png width=4in
1175 1175 In [151]: hist(np.random.randn(10000), 100);
1176 1176
1177 1177
1178 1178 @savefig test_plot.png width=4in
1179 1179 In [151]: plot(np.random.randn(10000), 'o');
1180 1180 """,
1181 1181
1182 1182 r"""
1183 1183 # use a semicolon to suppress the output
1184 1184 In [151]: plt.clf()
1185 1185
1186 1186 @savefig plot_simple.png width=4in
1187 1187 In [151]: plot([1,2,3])
1188 1188
1189 1189 @savefig hist_simple.png width=4in
1190 1190 In [151]: hist(np.random.randn(10000), 100);
1191 1191
1192 1192 """,
1193 1193 r"""
1194 1194 # update the current fig
1195 1195 In [151]: ylabel('number')
1196 1196
1197 1197 In [152]: title('normal distribution')
1198 1198
1199 1199
1200 1200 @savefig hist_with_text.png
1201 1201 In [153]: grid(True)
1202 1202
1203 1203 @doctest float
1204 1204 In [154]: 0.1 + 0.2
1205 1205 Out[154]: 0.3
1206 1206
1207 1207 @doctest float
1208 1208 In [155]: np.arange(16).reshape(4,4)
1209 1209 Out[155]:
1210 1210 array([[ 0, 1, 2, 3],
1211 1211 [ 4, 5, 6, 7],
1212 1212 [ 8, 9, 10, 11],
1213 1213 [12, 13, 14, 15]])
1214 1214
1215 1215 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
1216 1216
1217 1217 In [2]: x[0,0] = np.inf
1218 1218
1219 1219 In [3]: x[0,1] = np.nan
1220 1220
1221 1221 @doctest float
1222 1222 In [4]: x
1223 1223 Out[4]:
1224 1224 array([[ inf, nan, 2., 3.],
1225 1225 [ 4., 5., 6., 7.],
1226 1226 [ 8., 9., 10., 11.],
1227 1227 [ 12., 13., 14., 15.]])
1228 1228
1229 1229
1230 1230 """,
1231 1231 ]
1232 1232 # skip local-file depending first example:
1233 1233 examples = examples[1:]
1234 1234
1235 1235 #ipython_directive.DEBUG = True # dbg
1236 1236 #options = dict(suppress=True) # dbg
1237 1237 options = {}
1238 1238 for example in examples:
1239 1239 content = example.split('\n')
1240 1240 IPythonDirective('debug', arguments=None, options=options,
1241 1241 content=content, lineno=0,
1242 1242 content_offset=None, block_text=None,
1243 1243 state=None, state_machine=None,
1244 1244 )
1245 1245
1246 1246 # Run test suite as a script
1247 1247 if __name__=='__main__':
1248 1248 if not os.path.isdir('_static'):
1249 1249 os.mkdir('_static')
1250 1250 test()
1251 1251 print('All OK? Check figures in _static/')
General Comments 0
You need to be logged in to leave comments. Login now