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