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