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