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