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