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