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