##// END OF EJS Templates
Docstring updates.
chebee7i -
Show More
@@ -1,155 +1,155 b''
1 """
1 """
2 Module containing additional handlers for the IPython directive's @doctest.
2 Handlers for IPythonDirective's @doctest pseudo-decorator.
3
3
4 The Sphinx extension that provides support for embedded IPython code provides
4 The Sphinx extension that provides support for embedded IPython code provides
5 a pseudo-decorator @doctest, which treats the input/output block as a
5 a pseudo-decorator @doctest, which treats the input/output block as a
6 doctest, raising a RuntimeError during doc generation if the actual output
6 doctest, raising a RuntimeError during doc generation if the actual output
7 (after running the input) does not match the expected output.
7 (after running the input) does not match the expected output.
8
8
9 An example usage is:
9 An example usage is:
10
10
11 .. code-block:: rst
11 .. code-block:: rst
12
12
13 .. ipython::
13 .. ipython::
14
14
15 In [1]: x = 1
15 In [1]: x = 1
16
16
17 @doctest
17 @doctest
18 In [2]: x + 2
18 In [2]: x + 2
19 Out[3]: 3
19 Out[3]: 3
20
20
21 One can also provide arguments to the decorator. The first argument should be
21 One can also provide arguments to the decorator. The first argument should be
22 the name of a custom handler. The specification of any other arguments is
22 the name of a custom handler. The specification of any other arguments is
23 determined by the handler. For example,
23 determined by the handler. For example,
24
24
25 .. code-block:: rst
25 .. code-block:: rst
26
26
27 .. ipython::
27 .. ipython::
28
28
29 @doctest float
29 @doctest float
30 In [154]: 0.1 + 0.2
30 In [154]: 0.1 + 0.2
31 Out[154]: 0.3
31 Out[154]: 0.3
32
32
33 allows the actual output ``0.30000000000000004`` to match the expected output
33 allows the actual output ``0.30000000000000004`` to match the expected output
34 due to a comparison with `np.allclose`.
34 due to a comparison with `np.allclose`.
35
35
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
37 should have the following function signature::
37 should have the following function signature::
38
38
39 handler(sphinx_shell, args, input_lines, found, submitted)
39 handler(sphinx_shell, args, input_lines, found, submitted)
40
40
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
43 a list of the lines relevant to the current doctest, `found` is a string
43 a list of the lines relevant to the current doctest, `found` is a string
44 containing the output from the IPython shell, and `submitted` is a string
44 containing the output from the IPython shell, and `submitted` is a string
45 containing the expected output from the IPython shell.
45 containing the expected output from the IPython shell.
46
46
47 Handlers must be registered in the `doctests` dict at the end of this module.
47 Handlers must be registered in the `doctests` dict at the end of this module.
48
48
49 """
49 """
50
50
51 def str_to_array(s):
51 def str_to_array(s):
52 """
52 """
53 Simplistic converter of strings from repr to float NumPy arrays.
53 Simplistic converter of strings from repr to float NumPy arrays.
54
54
55 If the repr representation has ellipsis in it, then this will fail.
55 If the repr representation has ellipsis in it, then this will fail.
56
56
57 Parameters
57 Parameters
58 ----------
58 ----------
59 s : str
59 s : str
60 The repr version of a NumPy array.
60 The repr version of a NumPy array.
61
61
62 Examples
62 Examples
63 --------
63 --------
64 >>> s = "array([ 0.3, inf, nan])"
64 >>> s = "array([ 0.3, inf, nan])"
65 >>> a = str_to_array(s)
65 >>> a = str_to_array(s)
66
66
67 """
67 """
68 import numpy as np
68 import numpy as np
69
69
70 # Need to make sure eval() knows about inf and nan.
70 # Need to make sure eval() knows about inf and nan.
71 # This also assumes default printoptions for NumPy.
71 # This also assumes default printoptions for NumPy.
72 from numpy import inf, nan
72 from numpy import inf, nan
73
73
74 if s.startswith(u'array'):
74 if s.startswith(u'array'):
75 # Remove array( and )
75 # Remove array( and )
76 s = s[6:-1]
76 s = s[6:-1]
77
77
78 if s.startswith(u'['):
78 if s.startswith(u'['):
79 a = np.array(eval(s), dtype=float)
79 a = np.array(eval(s), dtype=float)
80 else:
80 else:
81 # Assume its a regular float. Force 1D so we can index into it.
81 # Assume its a regular float. Force 1D so we can index into it.
82 a = np.atleast_1d(float(s))
82 a = np.atleast_1d(float(s))
83 return a
83 return a
84
84
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
86 """
86 """
87 Doctest which allow the submitted output to vary slightly from the input.
87 Doctest which allow the submitted output to vary slightly from the input.
88
88
89 Here is how it might appear in an rst file:
89 Here is how it might appear in an rst file:
90
90
91 .. code-block:: rst
91 .. code-block:: rst
92
92
93 .. ipython::
93 .. ipython::
94
94
95 @doctest float
95 @doctest float
96 In [1]: 0.1 + 0.2
96 In [1]: 0.1 + 0.2
97 Out[1]: 0.3
97 Out[1]: 0.3
98
98
99 """
99 """
100 import numpy as np
100 import numpy as np
101
101
102 if len(args) == 2:
102 if len(args) == 2:
103 rtol = 1e-05
103 rtol = 1e-05
104 atol = 1e-08
104 atol = 1e-08
105 else:
105 else:
106 # Both must be specified if any are specified.
106 # Both must be specified if any are specified.
107 try:
107 try:
108 rtol = float(args[2])
108 rtol = float(args[2])
109 atol = float(args[3])
109 atol = float(args[3])
110 except IndexError:
110 except IndexError:
111 e = ("Both `rtol` and `atol` must be specified "
111 e = ("Both `rtol` and `atol` must be specified "
112 "if either are specified: {0}".format(args))
112 "if either are specified: {0}".format(args))
113 raise IndexError(e)
113 raise IndexError(e)
114
114
115 try:
115 try:
116 submitted = str_to_array(submitted)
116 submitted = str_to_array(submitted)
117 found = str_to_array(found)
117 found = str_to_array(found)
118 except:
118 except:
119 # For example, if the array is huge and there are ellipsis in it.
119 # For example, if the array is huge and there are ellipsis in it.
120 error = True
120 error = True
121 else:
121 else:
122 found_isnan = np.isnan(found)
122 found_isnan = np.isnan(found)
123 submitted_isnan = np.isnan(submitted)
123 submitted_isnan = np.isnan(submitted)
124 error = not np.allclose(found_isnan, submitted_isnan)
124 error = not np.allclose(found_isnan, submitted_isnan)
125 error |= not np.allclose(found[~found_isnan],
125 error |= not np.allclose(found[~found_isnan],
126 submitted[~submitted_isnan],
126 submitted[~submitted_isnan],
127 rtol=rtol, atol=atol)
127 rtol=rtol, atol=atol)
128
128
129 TAB = ' ' * 4
129 TAB = ' ' * 4
130 directive = sphinx_shell.directive
130 directive = sphinx_shell.directive
131 if directive is None:
131 if directive is None:
132 source = 'Unavailable'
132 source = 'Unavailable'
133 content = 'Unavailable'
133 content = 'Unavailable'
134 else:
134 else:
135 source = directive.state.document.current_source
135 source = directive.state.document.current_source
136 # Add tabs and make into a single string.
136 # Add tabs and make into a single string.
137 content = '\n'.join([TAB + line for line in directive.content])
137 content = '\n'.join([TAB + line for line in directive.content])
138
138
139 if error:
139 if error:
140
140
141 e = ('doctest float comparison failure\n\n'
141 e = ('doctest float comparison failure\n\n'
142 'Document source: {0}\n\n'
142 'Document source: {0}\n\n'
143 'Raw content: \n{1}\n\n'
143 'Raw content: \n{1}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
146 'instead of the expected:\n{TAB}{4}\n\n')
146 'instead of the expected:\n{TAB}{4}\n\n')
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
148 repr(submitted), TAB=TAB)
148 repr(submitted), TAB=TAB)
149 raise RuntimeError(e)
149 raise RuntimeError(e)
150
150
151 # dict of allowable doctest handlers. The key represents the first argument
151 # dict of allowable doctest handlers. The key represents the first argument
152 # that must be given to @doctest in order to activate the handler.
152 # that must be given to @doctest in order to activate the handler.
153 doctests = {
153 doctests = {
154 'float': float_doctest,
154 'float': float_doctest,
155 }
155 }
@@ -1,982 +1,983 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Sphinx directive to support embedded IPython code.
2 """
3 Sphinx directive to support embedded IPython code.
3
4
4 This directive allows pasting of entire interactive IPython sessions, prompts
5 This directive allows pasting of entire interactive IPython sessions, prompts
5 and all, and their code will actually get re-executed at doc build time, with
6 and all, and their code will actually get re-executed at doc build time, with
6 all prompts renumbered sequentially. It also allows you to input code as a pure
7 all prompts renumbered sequentially. It also allows you to input code as a pure
7 python input by giving the argument python to the directive. The output looks
8 python input by giving the argument python to the directive. The output looks
8 like an interactive ipython section.
9 like an interactive ipython section.
9
10
10 To enable this directive, simply list it in your Sphinx ``conf.py`` file
11 To enable this directive, simply list it in your Sphinx ``conf.py`` file
11 (making sure the directory where you placed it is visible to sphinx, as is
12 (making sure the directory where you placed it is visible to sphinx, as is
12 needed for all Sphinx directives). For example, to enable syntax highlighting
13 needed for all Sphinx directives). For example, to enable syntax highlighting
13 and the IPython directive::
14 and the IPython directive::
14
15
15 extensions = ['IPython.sphinxext.ipython_console_highlighting',
16 extensions = ['IPython.sphinxext.ipython_console_highlighting',
16 'IPython.sphinxext.ipython_directive']
17 'IPython.sphinxext.ipython_directive']
17
18
18 The IPython directive outputs code-blocks with the language 'ipython'. So
19 The IPython directive outputs code-blocks with the language 'ipython'. So
19 if you do not have the syntax highlighting extension enabled as well, then
20 if you do not have the syntax highlighting extension enabled as well, then
20 all rendered code-blocks will be uncolored. By default this directive assumes
21 all rendered code-blocks will be uncolored. By default this directive assumes
21 that your prompts are unchanged IPython ones, but this can be customized.
22 that your prompts are unchanged IPython ones, but this can be customized.
22 The configurable options that can be placed in conf.py are:
23 The configurable options that can be placed in conf.py are:
23
24
24 ipython_savefig_dir:
25 ipython_savefig_dir:
25 The directory in which to save the figures. This is relative to the
26 The directory in which to save the figures. This is relative to the
26 Sphinx source directory. The default is `html_static_path`.
27 Sphinx source directory. The default is `html_static_path`.
27 ipython_rgxin:
28 ipython_rgxin:
28 The compiled regular expression to denote the start of IPython input
29 The compiled regular expression to denote the start of IPython input
29 lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You
30 lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You
30 shouldn't need to change this.
31 shouldn't need to change this.
31 ipython_rgxout:
32 ipython_rgxout:
32 The compiled regular expression to denote the start of IPython output
33 The compiled regular expression to denote the start of IPython output
33 lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You
34 lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You
34 shouldn't need to change this.
35 shouldn't need to change this.
35 ipython_promptin:
36 ipython_promptin:
36 The string to represent the IPython input prompt in the generated ReST.
37 The string to represent the IPython input prompt in the generated ReST.
37 The default is 'In [%d]:'. This expects that the line numbers are used
38 The default is 'In [%d]:'. This expects that the line numbers are used
38 in the prompt.
39 in the prompt.
39 ipython_promptout:
40 ipython_promptout:
40 The string to represent the IPython prompt in the generated ReST. The
41 The string to represent the IPython prompt in the generated ReST. The
41 default is 'Out [%d]:'. This expects that the line numbers are used
42 default is 'Out [%d]:'. This expects that the line numbers are used
42 in the prompt.
43 in the prompt.
43 ipython_mplbackend:
44 ipython_mplbackend:
44 The string which specifies if the embedded Sphinx shell should import
45 The string which specifies if the embedded Sphinx shell should import
45 Matplotlib and set the backend. The value specifies a backend that is
46 Matplotlib and set the backend. The value specifies a backend that is
46 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
47 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
47 executed. If not specified in conf.py, then the default value of 'agg' is
48 executed. If not specified in conf.py, then the default value of 'agg' is
48 used. To use the IPython directive without matplotlib as a dependency, set
49 used. To use the IPython directive without matplotlib as a dependency, set
49 the value to `None`. It may end up that matplotlib is still imported
50 the value to `None`. It may end up that matplotlib is still imported
50 if the user specifies so in `ipython_execlines` or makes use of the
51 if the user specifies so in `ipython_execlines` or makes use of the
51 @savefig pseudo decorator.
52 @savefig pseudo decorator.
52 ipython_execlines:
53 ipython_execlines:
53 A list of strings to be exec'd in the embedded Sphinx shell. Typical
54 A list of strings to be exec'd in the embedded Sphinx shell. Typical
54 usage is to make certain packages always available. Set this to an empty
55 usage is to make certain packages always available. Set this to an empty
55 list if you wish to have no imports always available. If specified in
56 list if you wish to have no imports always available. If specified in
56 conf.py as `None`, then it has the effect of making no imports available.
57 conf.py as `None`, then it has the effect of making no imports available.
57 If omitted from conf.py altogether, then the default value of
58 If omitted from conf.py altogether, then the default value of
58 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
59 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
59
60
60 As an example, to use the IPython directive when `matplotlib` is not available,
61 As an example, to use the IPython directive when `matplotlib` is not available,
61 one sets the backend to `None`::
62 one sets the backend to `None`::
62
63
63 ipython_mplbackend = None
64 ipython_mplbackend = None
64
65
65 An example usage of the directive is:
66 An example usage of the directive is:
66
67
67 .. code-block:: rst
68 .. code-block:: rst
68
69
69 .. ipython::
70 .. ipython::
70
71
71 In [1]: x = 1
72 In [1]: x = 1
72
73
73 In [2]: y = x**2
74 In [2]: y = x**2
74
75
75 In [3]: print(y)
76 In [3]: print(y)
76
77
77 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
78 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
78 documentation.
79 documentation.
79
80
80 ToDo
81 ToDo
81 ----
82 ----
82
83
83 - Turn the ad-hoc test() function into a real test suite.
84 - Turn the ad-hoc test() function into a real test suite.
84 - Break up ipython-specific functionality from matplotlib stuff into better
85 - Break up ipython-specific functionality from matplotlib stuff into better
85 separated code.
86 separated code.
86
87
87 Authors
88 Authors
88 -------
89 -------
89
90
90 - John D Hunter: orignal author.
91 - John D Hunter: orignal author.
91 - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
92 - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
92 - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
93 - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
93 - Skipper Seabold, refactoring, cleanups, pure python addition
94 - Skipper Seabold, refactoring, cleanups, pure python addition
94 """
95 """
95 from __future__ import print_function
96 from __future__ import print_function
96
97
97 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
98 # Imports
99 # Imports
99 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
100
101
101 # Stdlib
102 # Stdlib
102 import os
103 import os
103 import re
104 import re
104 import sys
105 import sys
105 import tempfile
106 import tempfile
106 import ast
107 import ast
107
108
108 # To keep compatibility with various python versions
109 # To keep compatibility with various python versions
109 try:
110 try:
110 from hashlib import md5
111 from hashlib import md5
111 except ImportError:
112 except ImportError:
112 from md5 import md5
113 from md5 import md5
113
114
114 # Third-party
115 # Third-party
115 import sphinx
116 import sphinx
116 from docutils.parsers.rst import directives
117 from docutils.parsers.rst import directives
117 from docutils import nodes
118 from docutils import nodes
118 from sphinx.util.compat import Directive
119 from sphinx.util.compat import Directive
119
120
120 # Our own
121 # Our own
121 from IPython import Config, InteractiveShell
122 from IPython import Config, InteractiveShell
122 from IPython.core.profiledir import ProfileDir
123 from IPython.core.profiledir import ProfileDir
123 from IPython.utils import io
124 from IPython.utils import io
124 from IPython.utils.py3compat import PY3
125 from IPython.utils.py3compat import PY3
125
126
126 if PY3:
127 if PY3:
127 from io import StringIO
128 from io import StringIO
128 else:
129 else:
129 from StringIO import StringIO
130 from StringIO import StringIO
130
131
131 #-----------------------------------------------------------------------------
132 #-----------------------------------------------------------------------------
132 # Globals
133 # Globals
133 #-----------------------------------------------------------------------------
134 #-----------------------------------------------------------------------------
134 # for tokenizing blocks
135 # for tokenizing blocks
135 COMMENT, INPUT, OUTPUT = range(3)
136 COMMENT, INPUT, OUTPUT = range(3)
136
137
137 #-----------------------------------------------------------------------------
138 #-----------------------------------------------------------------------------
138 # Functions and class declarations
139 # Functions and class declarations
139 #-----------------------------------------------------------------------------
140 #-----------------------------------------------------------------------------
140
141
141 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
142 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
142 """
143 """
143 part is a string of ipython text, comprised of at most one
144 part is a string of ipython text, comprised of at most one
144 input, one ouput, comments, and blank lines. The block parser
145 input, one ouput, comments, and blank lines. The block parser
145 parses the text into a list of::
146 parses the text into a list of::
146
147
147 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
148 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
148
149
149 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
150 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
150 data is, depending on the type of token::
151 data is, depending on the type of token::
151
152
152 COMMENT : the comment string
153 COMMENT : the comment string
153
154
154 INPUT: the (DECORATOR, INPUT_LINE, REST) where
155 INPUT: the (DECORATOR, INPUT_LINE, REST) where
155 DECORATOR: the input decorator (or None)
156 DECORATOR: the input decorator (or None)
156 INPUT_LINE: the input as string (possibly multi-line)
157 INPUT_LINE: the input as string (possibly multi-line)
157 REST : any stdout generated by the input line (not OUTPUT)
158 REST : any stdout generated by the input line (not OUTPUT)
158
159
159 OUTPUT: the output string, possibly multi-line
160 OUTPUT: the output string, possibly multi-line
160
161
161 """
162 """
162 block = []
163 block = []
163 lines = part.split('\n')
164 lines = part.split('\n')
164 N = len(lines)
165 N = len(lines)
165 i = 0
166 i = 0
166 decorator = None
167 decorator = None
167 while 1:
168 while 1:
168
169
169 if i==N:
170 if i==N:
170 # nothing left to parse -- the last line
171 # nothing left to parse -- the last line
171 break
172 break
172
173
173 line = lines[i]
174 line = lines[i]
174 i += 1
175 i += 1
175 line_stripped = line.strip()
176 line_stripped = line.strip()
176 if line_stripped.startswith('#'):
177 if line_stripped.startswith('#'):
177 block.append((COMMENT, line))
178 block.append((COMMENT, line))
178 continue
179 continue
179
180
180 if line_stripped.startswith('@'):
181 if line_stripped.startswith('@'):
181 # we're assuming at most one decorator -- may need to
182 # we're assuming at most one decorator -- may need to
182 # rethink
183 # rethink
183 decorator = line_stripped
184 decorator = line_stripped
184 continue
185 continue
185
186
186 # does this look like an input line?
187 # does this look like an input line?
187 matchin = rgxin.match(line)
188 matchin = rgxin.match(line)
188 if matchin:
189 if matchin:
189 lineno, inputline = int(matchin.group(1)), matchin.group(2)
190 lineno, inputline = int(matchin.group(1)), matchin.group(2)
190
191
191 # the ....: continuation string
192 # the ....: continuation string
192 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
193 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
193 Nc = len(continuation)
194 Nc = len(continuation)
194 # input lines can continue on for more than one line, if
195 # input lines can continue on for more than one line, if
195 # we have a '\' line continuation char or a function call
196 # we have a '\' line continuation char or a function call
196 # echo line 'print'. The input line can only be
197 # echo line 'print'. The input line can only be
197 # terminated by the end of the block or an output line, so
198 # terminated by the end of the block or an output line, so
198 # we parse out the rest of the input line if it is
199 # we parse out the rest of the input line if it is
199 # multiline as well as any echo text
200 # multiline as well as any echo text
200
201
201 rest = []
202 rest = []
202 while i<N:
203 while i<N:
203
204
204 # look ahead; if the next line is blank, or a comment, or
205 # look ahead; if the next line is blank, or a comment, or
205 # an output line, we're done
206 # an output line, we're done
206
207
207 nextline = lines[i]
208 nextline = lines[i]
208 matchout = rgxout.match(nextline)
209 matchout = rgxout.match(nextline)
209 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
210 #print "nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation))
210 if matchout or nextline.startswith('#'):
211 if matchout or nextline.startswith('#'):
211 break
212 break
212 elif nextline.startswith(continuation):
213 elif nextline.startswith(continuation):
213 inputline += '\n' + nextline[Nc:]
214 inputline += '\n' + nextline[Nc:]
214 else:
215 else:
215 rest.append(nextline)
216 rest.append(nextline)
216 i+= 1
217 i+= 1
217
218
218 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
219 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
219 continue
220 continue
220
221
221 # if it looks like an output line grab all the text to the end
222 # if it looks like an output line grab all the text to the end
222 # of the block
223 # of the block
223 matchout = rgxout.match(line)
224 matchout = rgxout.match(line)
224 if matchout:
225 if matchout:
225 lineno, output = int(matchout.group(1)), matchout.group(2)
226 lineno, output = int(matchout.group(1)), matchout.group(2)
226 if i<N-1:
227 if i<N-1:
227 output = '\n'.join([output] + lines[i:])
228 output = '\n'.join([output] + lines[i:])
228
229
229 block.append((OUTPUT, output))
230 block.append((OUTPUT, output))
230 break
231 break
231
232
232 return block
233 return block
233
234
234
235
235 class EmbeddedSphinxShell(object):
236 class EmbeddedSphinxShell(object):
236 """An embedded IPython instance to run inside Sphinx"""
237 """An embedded IPython instance to run inside Sphinx"""
237
238
238 def __init__(self, exec_lines=None):
239 def __init__(self, exec_lines=None):
239
240
240 self.cout = StringIO()
241 self.cout = StringIO()
241
242
242 if exec_lines is None:
243 if exec_lines is None:
243 exec_lines = []
244 exec_lines = []
244
245
245 # Create config object for IPython
246 # Create config object for IPython
246 config = Config()
247 config = Config()
247 config.InteractiveShell.autocall = False
248 config.InteractiveShell.autocall = False
248 config.InteractiveShell.autoindent = False
249 config.InteractiveShell.autoindent = False
249 config.InteractiveShell.colors = 'NoColor'
250 config.InteractiveShell.colors = 'NoColor'
250
251
251 # create a profile so instance history isn't saved
252 # create a profile so instance history isn't saved
252 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
253 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
253 profname = 'auto_profile_sphinx_build'
254 profname = 'auto_profile_sphinx_build'
254 pdir = os.path.join(tmp_profile_dir,profname)
255 pdir = os.path.join(tmp_profile_dir,profname)
255 profile = ProfileDir.create_profile_dir(pdir)
256 profile = ProfileDir.create_profile_dir(pdir)
256
257
257 # Create and initialize global ipython, but don't start its mainloop.
258 # Create and initialize global ipython, but don't start its mainloop.
258 # This will persist across different EmbededSphinxShell instances.
259 # This will persist across different EmbededSphinxShell instances.
259 IP = InteractiveShell.instance(config=config, profile_dir=profile)
260 IP = InteractiveShell.instance(config=config, profile_dir=profile)
260
261
261 # io.stdout redirect must be done after instantiating InteractiveShell
262 # io.stdout redirect must be done after instantiating InteractiveShell
262 io.stdout = self.cout
263 io.stdout = self.cout
263 io.stderr = self.cout
264 io.stderr = self.cout
264
265
265 # For debugging, so we can see normal output, use this:
266 # For debugging, so we can see normal output, use this:
266 #from IPython.utils.io import Tee
267 #from IPython.utils.io import Tee
267 #io.stdout = Tee(self.cout, channel='stdout') # dbg
268 #io.stdout = Tee(self.cout, channel='stdout') # dbg
268 #io.stderr = Tee(self.cout, channel='stderr') # dbg
269 #io.stderr = Tee(self.cout, channel='stderr') # dbg
269
270
270 # Store a few parts of IPython we'll need.
271 # Store a few parts of IPython we'll need.
271 self.IP = IP
272 self.IP = IP
272 self.user_ns = self.IP.user_ns
273 self.user_ns = self.IP.user_ns
273 self.user_global_ns = self.IP.user_global_ns
274 self.user_global_ns = self.IP.user_global_ns
274
275
275 self.input = ''
276 self.input = ''
276 self.output = ''
277 self.output = ''
277
278
278 self.is_verbatim = False
279 self.is_verbatim = False
279 self.is_doctest = False
280 self.is_doctest = False
280 self.is_suppress = False
281 self.is_suppress = False
281
282
282 # Optionally, provide more detailed information to shell.
283 # Optionally, provide more detailed information to shell.
283 self.directive = None
284 self.directive = None
284
285
285 # on the first call to the savefig decorator, we'll import
286 # on the first call to the savefig decorator, we'll import
286 # pyplot as plt so we can make a call to the plt.gcf().savefig
287 # pyplot as plt so we can make a call to the plt.gcf().savefig
287 self._pyplot_imported = False
288 self._pyplot_imported = False
288
289
289 # Prepopulate the namespace.
290 # Prepopulate the namespace.
290 for line in exec_lines:
291 for line in exec_lines:
291 self.process_input_line(line, store_history=False)
292 self.process_input_line(line, store_history=False)
292
293
293 def clear_cout(self):
294 def clear_cout(self):
294 self.cout.seek(0)
295 self.cout.seek(0)
295 self.cout.truncate(0)
296 self.cout.truncate(0)
296
297
297 def process_input_line(self, line, store_history=True):
298 def process_input_line(self, line, store_history=True):
298 """process the input, capturing stdout"""
299 """process the input, capturing stdout"""
299
300
300 stdout = sys.stdout
301 stdout = sys.stdout
301 splitter = self.IP.input_splitter
302 splitter = self.IP.input_splitter
302 try:
303 try:
303 sys.stdout = self.cout
304 sys.stdout = self.cout
304 splitter.push(line)
305 splitter.push(line)
305 more = splitter.push_accepts_more()
306 more = splitter.push_accepts_more()
306 if not more:
307 if not more:
307 source_raw = splitter.source_raw_reset()[1]
308 source_raw = splitter.source_raw_reset()[1]
308 self.IP.run_cell(source_raw, store_history=store_history)
309 self.IP.run_cell(source_raw, store_history=store_history)
309 finally:
310 finally:
310 sys.stdout = stdout
311 sys.stdout = stdout
311
312
312 def process_image(self, decorator):
313 def process_image(self, decorator):
313 """
314 """
314 # build out an image directive like
315 # build out an image directive like
315 # .. image:: somefile.png
316 # .. image:: somefile.png
316 # :width 4in
317 # :width 4in
317 #
318 #
318 # from an input like
319 # from an input like
319 # savefig somefile.png width=4in
320 # savefig somefile.png width=4in
320 """
321 """
321 savefig_dir = self.savefig_dir
322 savefig_dir = self.savefig_dir
322 source_dir = self.source_dir
323 source_dir = self.source_dir
323 saveargs = decorator.split(' ')
324 saveargs = decorator.split(' ')
324 filename = saveargs[1]
325 filename = saveargs[1]
325 # insert relative path to image file in source
326 # insert relative path to image file in source
326 outfile = os.path.relpath(os.path.join(savefig_dir,filename),
327 outfile = os.path.relpath(os.path.join(savefig_dir,filename),
327 source_dir)
328 source_dir)
328
329
329 imagerows = ['.. image:: %s'%outfile]
330 imagerows = ['.. image:: %s'%outfile]
330
331
331 for kwarg in saveargs[2:]:
332 for kwarg in saveargs[2:]:
332 arg, val = kwarg.split('=')
333 arg, val = kwarg.split('=')
333 arg = arg.strip()
334 arg = arg.strip()
334 val = val.strip()
335 val = val.strip()
335 imagerows.append(' :%s: %s'%(arg, val))
336 imagerows.append(' :%s: %s'%(arg, val))
336
337
337 image_file = os.path.basename(outfile) # only return file name
338 image_file = os.path.basename(outfile) # only return file name
338 image_directive = '\n'.join(imagerows)
339 image_directive = '\n'.join(imagerows)
339 return image_file, image_directive
340 return image_file, image_directive
340
341
341 # Callbacks for each type of token
342 # Callbacks for each type of token
342 def process_input(self, data, input_prompt, lineno):
343 def process_input(self, data, input_prompt, lineno):
343 """
344 """
344 Process data block for INPUT token.
345 Process data block for INPUT token.
345
346
346 """
347 """
347 decorator, input, rest = data
348 decorator, input, rest = data
348 image_file = None
349 image_file = None
349 image_directive = None
350 image_directive = None
350
351
351 is_verbatim = decorator=='@verbatim' or self.is_verbatim
352 is_verbatim = decorator=='@verbatim' or self.is_verbatim
352 is_doctest = (decorator is not None and \
353 is_doctest = (decorator is not None and \
353 decorator.startswith('@doctest')) or self.is_doctest
354 decorator.startswith('@doctest')) or self.is_doctest
354 is_suppress = decorator=='@suppress' or self.is_suppress
355 is_suppress = decorator=='@suppress' or self.is_suppress
355 is_savefig = decorator is not None and \
356 is_savefig = decorator is not None and \
356 decorator.startswith('@savefig')
357 decorator.startswith('@savefig')
357
358
358 input_lines = input.split('\n')
359 input_lines = input.split('\n')
359 if len(input_lines) > 1:
360 if len(input_lines) > 1:
360 if input_lines[-1] != "":
361 if input_lines[-1] != "":
361 input_lines.append('') # make sure there's a blank line
362 input_lines.append('') # make sure there's a blank line
362 # so splitter buffer gets reset
363 # so splitter buffer gets reset
363
364
364 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
365 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
365 Nc = len(continuation)
366 Nc = len(continuation)
366
367
367 if is_savefig:
368 if is_savefig:
368 image_file, image_directive = self.process_image(decorator)
369 image_file, image_directive = self.process_image(decorator)
369
370
370 ret = []
371 ret = []
371 is_semicolon = False
372 is_semicolon = False
372
373
373 for i, line in enumerate(input_lines):
374 for i, line in enumerate(input_lines):
374 if line.endswith(';'):
375 if line.endswith(';'):
375 is_semicolon = True
376 is_semicolon = True
376
377
377 if i==0:
378 if i==0:
378 # process the first input line
379 # process the first input line
379 if is_verbatim:
380 if is_verbatim:
380 self.process_input_line('')
381 self.process_input_line('')
381 self.IP.execution_count += 1 # increment it anyway
382 self.IP.execution_count += 1 # increment it anyway
382 else:
383 else:
383 # only submit the line in non-verbatim mode
384 # only submit the line in non-verbatim mode
384 self.process_input_line(line, store_history=True)
385 self.process_input_line(line, store_history=True)
385 formatted_line = '%s %s'%(input_prompt, line)
386 formatted_line = '%s %s'%(input_prompt, line)
386 else:
387 else:
387 # process a continuation line
388 # process a continuation line
388 if not is_verbatim:
389 if not is_verbatim:
389 self.process_input_line(line, store_history=True)
390 self.process_input_line(line, store_history=True)
390
391
391 formatted_line = '%s %s'%(continuation, line)
392 formatted_line = '%s %s'%(continuation, line)
392
393
393 if not is_suppress:
394 if not is_suppress:
394 ret.append(formatted_line)
395 ret.append(formatted_line)
395
396
396 if not is_suppress and len(rest.strip()) and is_verbatim:
397 if not is_suppress and len(rest.strip()) and is_verbatim:
397 # the "rest" is the standard output of the
398 # the "rest" is the standard output of the
398 # input, which needs to be added in
399 # input, which needs to be added in
399 # verbatim mode
400 # verbatim mode
400 ret.append(rest)
401 ret.append(rest)
401
402
402 self.cout.seek(0)
403 self.cout.seek(0)
403 output = self.cout.read()
404 output = self.cout.read()
404 if not is_suppress and not is_semicolon:
405 if not is_suppress and not is_semicolon:
405 ret.append(output)
406 ret.append(output)
406 elif is_semicolon: # get spacing right
407 elif is_semicolon: # get spacing right
407 ret.append('')
408 ret.append('')
408
409
409 self.cout.truncate(0)
410 self.cout.truncate(0)
410 return (ret, input_lines, output, is_doctest, decorator, image_file,
411 return (ret, input_lines, output, is_doctest, decorator, image_file,
411 image_directive)
412 image_directive)
412
413
413
414
414 def process_output(self, data, output_prompt,
415 def process_output(self, data, output_prompt,
415 input_lines, output, is_doctest, decorator, image_file):
416 input_lines, output, is_doctest, decorator, image_file):
416 """
417 """
417 Process data block for OUTPUT token.
418 Process data block for OUTPUT token.
418
419
419 """
420 """
420 TAB = ' ' * 4
421 TAB = ' ' * 4
421
422
422 if is_doctest and output is not None:
423 if is_doctest and output is not None:
423
424
424 found = output
425 found = output
425 found = found.strip()
426 found = found.strip()
426 submitted = data.strip()
427 submitted = data.strip()
427
428
428 if self.directive is None:
429 if self.directive is None:
429 source = 'Unavailable'
430 source = 'Unavailable'
430 content = 'Unavailable'
431 content = 'Unavailable'
431 else:
432 else:
432 source = self.directive.state.document.current_source
433 source = self.directive.state.document.current_source
433 content = self.directive.content
434 content = self.directive.content
434 # Add tabs and join into a single string.
435 # Add tabs and join into a single string.
435 content = '\n'.join([TAB + line for line in content])
436 content = '\n'.join([TAB + line for line in content])
436
437
437 # Make sure the output contains the output prompt.
438 # Make sure the output contains the output prompt.
438 ind = found.find(output_prompt)
439 ind = found.find(output_prompt)
439 if ind < 0:
440 if ind < 0:
440 e = ('output does not contain output prompt\n\n'
441 e = ('output does not contain output prompt\n\n'
441 'Document source: {0}\n\n'
442 'Document source: {0}\n\n'
442 'Raw content: \n{1}\n\n'
443 'Raw content: \n{1}\n\n'
443 'Input line(s):\n{TAB}{2}\n\n'
444 'Input line(s):\n{TAB}{2}\n\n'
444 'Output line(s):\n{TAB}{3}\n\n')
445 'Output line(s):\n{TAB}{3}\n\n')
445 e = e.format(source, content, '\n'.join(input_lines),
446 e = e.format(source, content, '\n'.join(input_lines),
446 repr(found), TAB=TAB)
447 repr(found), TAB=TAB)
447 raise RuntimeError(e)
448 raise RuntimeError(e)
448 found = found[len(output_prompt):].strip()
449 found = found[len(output_prompt):].strip()
449
450
450 # Handle the actual doctest comparison.
451 # Handle the actual doctest comparison.
451 if decorator.strip() == '@doctest':
452 if decorator.strip() == '@doctest':
452 # Standard doctest
453 # Standard doctest
453 if found != submitted:
454 if found != submitted:
454 e = ('doctest failure\n\n'
455 e = ('doctest failure\n\n'
455 'Document source: {0}\n\n'
456 'Document source: {0}\n\n'
456 'Raw content: \n{1}\n\n'
457 'Raw content: \n{1}\n\n'
457 'On input line(s):\n{TAB}{2}\n\n'
458 'On input line(s):\n{TAB}{2}\n\n'
458 'we found output:\n{TAB}{3}\n\n'
459 'we found output:\n{TAB}{3}\n\n'
459 'instead of the expected:\n{TAB}{4}\n\n')
460 'instead of the expected:\n{TAB}{4}\n\n')
460 e = e.format(source, content, '\n'.join(input_lines),
461 e = e.format(source, content, '\n'.join(input_lines),
461 repr(found), repr(submitted), TAB=TAB)
462 repr(found), repr(submitted), TAB=TAB)
462 raise RuntimeError(e)
463 raise RuntimeError(e)
463 else:
464 else:
464 self.custom_doctest(decorator, input_lines, found, submitted)
465 self.custom_doctest(decorator, input_lines, found, submitted)
465
466
466 def process_comment(self, data):
467 def process_comment(self, data):
467 """Process data fPblock for COMMENT token."""
468 """Process data fPblock for COMMENT token."""
468 if not self.is_suppress:
469 if not self.is_suppress:
469 return [data]
470 return [data]
470
471
471 def save_image(self, image_file):
472 def save_image(self, image_file):
472 """
473 """
473 Saves the image file to disk.
474 Saves the image file to disk.
474 """
475 """
475 self.ensure_pyplot()
476 self.ensure_pyplot()
476 command = 'plt.gcf().savefig("%s")'%image_file
477 command = 'plt.gcf().savefig("%s")'%image_file
477 #print 'SAVEFIG', command # dbg
478 #print 'SAVEFIG', command # dbg
478 self.process_input_line('bookmark ipy_thisdir', store_history=False)
479 self.process_input_line('bookmark ipy_thisdir', store_history=False)
479 self.process_input_line('cd -b ipy_savedir', store_history=False)
480 self.process_input_line('cd -b ipy_savedir', store_history=False)
480 self.process_input_line(command, store_history=False)
481 self.process_input_line(command, store_history=False)
481 self.process_input_line('cd -b ipy_thisdir', store_history=False)
482 self.process_input_line('cd -b ipy_thisdir', store_history=False)
482 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
483 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
483 self.clear_cout()
484 self.clear_cout()
484
485
485 def process_block(self, block):
486 def process_block(self, block):
486 """
487 """
487 process block from the block_parser and return a list of processed lines
488 process block from the block_parser and return a list of processed lines
488 """
489 """
489 ret = []
490 ret = []
490 output = None
491 output = None
491 input_lines = None
492 input_lines = None
492 lineno = self.IP.execution_count
493 lineno = self.IP.execution_count
493
494
494 input_prompt = self.promptin % lineno
495 input_prompt = self.promptin % lineno
495 output_prompt = self.promptout % lineno
496 output_prompt = self.promptout % lineno
496 image_file = None
497 image_file = None
497 image_directive = None
498 image_directive = None
498
499
499 for token, data in block:
500 for token, data in block:
500 if token == COMMENT:
501 if token == COMMENT:
501 out_data = self.process_comment(data)
502 out_data = self.process_comment(data)
502 elif token == INPUT:
503 elif token == INPUT:
503 (out_data, input_lines, output, is_doctest, decorator,
504 (out_data, input_lines, output, is_doctest, decorator,
504 image_file, image_directive) = \
505 image_file, image_directive) = \
505 self.process_input(data, input_prompt, lineno)
506 self.process_input(data, input_prompt, lineno)
506 elif token == OUTPUT:
507 elif token == OUTPUT:
507 out_data = \
508 out_data = \
508 self.process_output(data, output_prompt,
509 self.process_output(data, output_prompt,
509 input_lines, output, is_doctest,
510 input_lines, output, is_doctest,
510 decorator, image_file)
511 decorator, image_file)
511 if out_data:
512 if out_data:
512 ret.extend(out_data)
513 ret.extend(out_data)
513
514
514 # save the image files
515 # save the image files
515 if image_file is not None:
516 if image_file is not None:
516 self.save_image(image_file)
517 self.save_image(image_file)
517
518
518 return ret, image_directive
519 return ret, image_directive
519
520
520 def ensure_pyplot(self):
521 def ensure_pyplot(self):
521 """
522 """
522 Ensures that pyplot has been imported into the embedded IPython shell.
523 Ensures that pyplot has been imported into the embedded IPython shell.
523
524
524 Also, makes sure to set the backend appropriately if not set already.
525 Also, makes sure to set the backend appropriately if not set already.
525
526
526 """
527 """
527 # We are here if the @figure pseudo decorator was used. Thus, it's
528 # We are here if the @figure pseudo decorator was used. Thus, it's
528 # possible that we could be here even if python_mplbackend were set to
529 # possible that we could be here even if python_mplbackend were set to
529 # `None`. That's also strange and perhaps worthy of raising an
530 # `None`. That's also strange and perhaps worthy of raising an
530 # exception, but for now, we just set the backend to 'agg'.
531 # exception, but for now, we just set the backend to 'agg'.
531
532
532 if not self._pyplot_imported:
533 if not self._pyplot_imported:
533 if 'matplotlib.backends' not in sys.modules:
534 if 'matplotlib.backends' not in sys.modules:
534 # Then ipython_matplotlib was set to None but there was a
535 # Then ipython_matplotlib was set to None but there was a
535 # call to the @figure decorator (and ipython_execlines did
536 # call to the @figure decorator (and ipython_execlines did
536 # not set a backend).
537 # not set a backend).
537 #raise Exception("No backend was set, but @figure was used!")
538 #raise Exception("No backend was set, but @figure was used!")
538 import matplotlib
539 import matplotlib
539 matplotlib.use('agg')
540 matplotlib.use('agg')
540
541
541 # Always import pyplot into embedded shell.
542 # Always import pyplot into embedded shell.
542 self.process_input_line('import matplotlib.pyplot as plt',
543 self.process_input_line('import matplotlib.pyplot as plt',
543 store_history=False)
544 store_history=False)
544 self._pyplot_imported = True
545 self._pyplot_imported = True
545
546
546 def process_pure_python(self, content):
547 def process_pure_python(self, content):
547 """
548 """
548 content is a list of strings. it is unedited directive content
549 content is a list of strings. it is unedited directive content
549
550
550 This runs it line by line in the InteractiveShell, prepends
551 This runs it line by line in the InteractiveShell, prepends
551 prompts as needed capturing stderr and stdout, then returns
552 prompts as needed capturing stderr and stdout, then returns
552 the content as a list as if it were ipython code
553 the content as a list as if it were ipython code
553 """
554 """
554 output = []
555 output = []
555 savefig = False # keep up with this to clear figure
556 savefig = False # keep up with this to clear figure
556 multiline = False # to handle line continuation
557 multiline = False # to handle line continuation
557 multiline_start = None
558 multiline_start = None
558 fmtin = self.promptin
559 fmtin = self.promptin
559
560
560 ct = 0
561 ct = 0
561
562
562 for lineno, line in enumerate(content):
563 for lineno, line in enumerate(content):
563
564
564 line_stripped = line.strip()
565 line_stripped = line.strip()
565 if not len(line):
566 if not len(line):
566 output.append(line)
567 output.append(line)
567 continue
568 continue
568
569
569 # handle decorators
570 # handle decorators
570 if line_stripped.startswith('@'):
571 if line_stripped.startswith('@'):
571 output.extend([line])
572 output.extend([line])
572 if 'savefig' in line:
573 if 'savefig' in line:
573 savefig = True # and need to clear figure
574 savefig = True # and need to clear figure
574 continue
575 continue
575
576
576 # handle comments
577 # handle comments
577 if line_stripped.startswith('#'):
578 if line_stripped.startswith('#'):
578 output.extend([line])
579 output.extend([line])
579 continue
580 continue
580
581
581 # deal with lines checking for multiline
582 # deal with lines checking for multiline
582 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
583 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
583 if not multiline:
584 if not multiline:
584 modified = u"%s %s" % (fmtin % ct, line_stripped)
585 modified = u"%s %s" % (fmtin % ct, line_stripped)
585 output.append(modified)
586 output.append(modified)
586 ct += 1
587 ct += 1
587 try:
588 try:
588 ast.parse(line_stripped)
589 ast.parse(line_stripped)
589 output.append(u'')
590 output.append(u'')
590 except Exception: # on a multiline
591 except Exception: # on a multiline
591 multiline = True
592 multiline = True
592 multiline_start = lineno
593 multiline_start = lineno
593 else: # still on a multiline
594 else: # still on a multiline
594 modified = u'%s %s' % (continuation, line)
595 modified = u'%s %s' % (continuation, line)
595 output.append(modified)
596 output.append(modified)
596
597
597 # if the next line is indented, it should be part of multiline
598 # if the next line is indented, it should be part of multiline
598 if len(content) > lineno + 1:
599 if len(content) > lineno + 1:
599 nextline = content[lineno + 1]
600 nextline = content[lineno + 1]
600 if len(nextline) - len(nextline.lstrip()) > 3:
601 if len(nextline) - len(nextline.lstrip()) > 3:
601 continue
602 continue
602 try:
603 try:
603 mod = ast.parse(
604 mod = ast.parse(
604 '\n'.join(content[multiline_start:lineno+1]))
605 '\n'.join(content[multiline_start:lineno+1]))
605 if isinstance(mod.body[0], ast.FunctionDef):
606 if isinstance(mod.body[0], ast.FunctionDef):
606 # check to see if we have the whole function
607 # check to see if we have the whole function
607 for element in mod.body[0].body:
608 for element in mod.body[0].body:
608 if isinstance(element, ast.Return):
609 if isinstance(element, ast.Return):
609 multiline = False
610 multiline = False
610 else:
611 else:
611 output.append(u'')
612 output.append(u'')
612 multiline = False
613 multiline = False
613 except Exception:
614 except Exception:
614 pass
615 pass
615
616
616 if savefig: # clear figure if plotted
617 if savefig: # clear figure if plotted
617 self.ensure_pyplot()
618 self.ensure_pyplot()
618 self.process_input_line('plt.clf()', store_history=False)
619 self.process_input_line('plt.clf()', store_history=False)
619 self.clear_cout()
620 self.clear_cout()
620 savefig = False
621 savefig = False
621
622
622 return output
623 return output
623
624
624 def custom_doctest(self, decorator, input_lines, found, submitted):
625 def custom_doctest(self, decorator, input_lines, found, submitted):
625 """
626 """
626 Perform a specialized doctest.
627 Perform a specialized doctest.
627
628
628 """
629 """
629 from .custom_doctests import doctests
630 from .custom_doctests import doctests
630
631
631 args = decorator.split()
632 args = decorator.split()
632 doctest_type = args[1]
633 doctest_type = args[1]
633 if doctest_type in doctests:
634 if doctest_type in doctests:
634 doctests[doctest_type](self, args, input_lines, found, submitted)
635 doctests[doctest_type](self, args, input_lines, found, submitted)
635 else:
636 else:
636 e = "Invalid option to @doctest: {0}".format(doctest_type)
637 e = "Invalid option to @doctest: {0}".format(doctest_type)
637 raise Exception(e)
638 raise Exception(e)
638
639
639
640
640 class IPythonDirective(Directive):
641 class IPythonDirective(Directive):
641
642
642 has_content = True
643 has_content = True
643 required_arguments = 0
644 required_arguments = 0
644 optional_arguments = 4 # python, suppress, verbatim, doctest
645 optional_arguments = 4 # python, suppress, verbatim, doctest
645 final_argumuent_whitespace = True
646 final_argumuent_whitespace = True
646 option_spec = { 'python': directives.unchanged,
647 option_spec = { 'python': directives.unchanged,
647 'suppress' : directives.flag,
648 'suppress' : directives.flag,
648 'verbatim' : directives.flag,
649 'verbatim' : directives.flag,
649 'doctest' : directives.flag,
650 'doctest' : directives.flag,
650 }
651 }
651
652
652 shell = None
653 shell = None
653
654
654 seen_docs = set()
655 seen_docs = set()
655
656
656 def get_config_options(self):
657 def get_config_options(self):
657 # contains sphinx configuration variables
658 # contains sphinx configuration variables
658 config = self.state.document.settings.env.config
659 config = self.state.document.settings.env.config
659
660
660 # get config variables to set figure output directory
661 # get config variables to set figure output directory
661 confdir = self.state.document.settings.env.app.confdir
662 confdir = self.state.document.settings.env.app.confdir
662 savefig_dir = config.ipython_savefig_dir
663 savefig_dir = config.ipython_savefig_dir
663 source_dir = os.path.dirname(self.state.document.current_source)
664 source_dir = os.path.dirname(self.state.document.current_source)
664 if savefig_dir is None:
665 if savefig_dir is None:
665 savefig_dir = config.html_static_path
666 savefig_dir = config.html_static_path
666 if isinstance(savefig_dir, list):
667 if isinstance(savefig_dir, list):
667 savefig_dir = savefig_dir[0] # safe to assume only one path?
668 savefig_dir = savefig_dir[0] # safe to assume only one path?
668 savefig_dir = os.path.join(confdir, savefig_dir)
669 savefig_dir = os.path.join(confdir, savefig_dir)
669
670
670 # get regex and prompt stuff
671 # get regex and prompt stuff
671 rgxin = config.ipython_rgxin
672 rgxin = config.ipython_rgxin
672 rgxout = config.ipython_rgxout
673 rgxout = config.ipython_rgxout
673 promptin = config.ipython_promptin
674 promptin = config.ipython_promptin
674 promptout = config.ipython_promptout
675 promptout = config.ipython_promptout
675 mplbackend = config.ipython_mplbackend
676 mplbackend = config.ipython_mplbackend
676 exec_lines = config.ipython_execlines
677 exec_lines = config.ipython_execlines
677
678
678 return (savefig_dir, source_dir, rgxin, rgxout,
679 return (savefig_dir, source_dir, rgxin, rgxout,
679 promptin, promptout, mplbackend, exec_lines)
680 promptin, promptout, mplbackend, exec_lines)
680
681
681 def setup(self):
682 def setup(self):
682 # Get configuration values.
683 # Get configuration values.
683 (savefig_dir, source_dir, rgxin, rgxout, promptin,
684 (savefig_dir, source_dir, rgxin, rgxout, promptin,
684 promptout, mplbackend, exec_lines) = self.get_config_options()
685 promptout, mplbackend, exec_lines) = self.get_config_options()
685
686
686 if self.shell is None:
687 if self.shell is None:
687 # We will be here many times. However, when the
688 # We will be here many times. However, when the
688 # EmbeddedSphinxShell is created, its interactive shell member
689 # EmbeddedSphinxShell is created, its interactive shell member
689 # is the same for each instance.
690 # is the same for each instance.
690
691
691 if mplbackend:
692 if mplbackend:
692 import matplotlib
693 import matplotlib
693 # Repeated calls to use() will not hurt us since `mplbackend`
694 # Repeated calls to use() will not hurt us since `mplbackend`
694 # is the same each time.
695 # is the same each time.
695 matplotlib.use(mplbackend)
696 matplotlib.use(mplbackend)
696
697
697 # Must be called after (potentially) importing matplotlib and
698 # Must be called after (potentially) importing matplotlib and
698 # setting its backend since exec_lines might import pylab.
699 # setting its backend since exec_lines might import pylab.
699 self.shell = EmbeddedSphinxShell(exec_lines)
700 self.shell = EmbeddedSphinxShell(exec_lines)
700
701
701 # Store IPython directive to enable better error messages
702 # Store IPython directive to enable better error messages
702 self.shell.directive = self
703 self.shell.directive = self
703
704
704 # reset the execution count if we haven't processed this doc
705 # reset the execution count if we haven't processed this doc
705 #NOTE: this may be borked if there are multiple seen_doc tmp files
706 #NOTE: this may be borked if there are multiple seen_doc tmp files
706 #check time stamp?
707 #check time stamp?
707 if not self.state.document.current_source in self.seen_docs:
708 if not self.state.document.current_source in self.seen_docs:
708 self.shell.IP.history_manager.reset()
709 self.shell.IP.history_manager.reset()
709 self.shell.IP.execution_count = 1
710 self.shell.IP.execution_count = 1
710 self.shell.IP.prompt_manager.width = 0
711 self.shell.IP.prompt_manager.width = 0
711 self.seen_docs.add(self.state.document.current_source)
712 self.seen_docs.add(self.state.document.current_source)
712
713
713 # and attach to shell so we don't have to pass them around
714 # and attach to shell so we don't have to pass them around
714 self.shell.rgxin = rgxin
715 self.shell.rgxin = rgxin
715 self.shell.rgxout = rgxout
716 self.shell.rgxout = rgxout
716 self.shell.promptin = promptin
717 self.shell.promptin = promptin
717 self.shell.promptout = promptout
718 self.shell.promptout = promptout
718 self.shell.savefig_dir = savefig_dir
719 self.shell.savefig_dir = savefig_dir
719 self.shell.source_dir = source_dir
720 self.shell.source_dir = source_dir
720
721
721 # setup bookmark for saving figures directory
722 # setup bookmark for saving figures directory
722 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
723 self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir,
723 store_history=False)
724 store_history=False)
724 self.shell.clear_cout()
725 self.shell.clear_cout()
725
726
726 return rgxin, rgxout, promptin, promptout
727 return rgxin, rgxout, promptin, promptout
727
728
728 def teardown(self):
729 def teardown(self):
729 # delete last bookmark
730 # delete last bookmark
730 self.shell.process_input_line('bookmark -d ipy_savedir',
731 self.shell.process_input_line('bookmark -d ipy_savedir',
731 store_history=False)
732 store_history=False)
732 self.shell.clear_cout()
733 self.shell.clear_cout()
733
734
734 def run(self):
735 def run(self):
735 debug = False
736 debug = False
736
737
737 #TODO, any reason block_parser can't be a method of embeddable shell
738 #TODO, any reason block_parser can't be a method of embeddable shell
738 # then we wouldn't have to carry these around
739 # then we wouldn't have to carry these around
739 rgxin, rgxout, promptin, promptout = self.setup()
740 rgxin, rgxout, promptin, promptout = self.setup()
740
741
741 options = self.options
742 options = self.options
742 self.shell.is_suppress = 'suppress' in options
743 self.shell.is_suppress = 'suppress' in options
743 self.shell.is_doctest = 'doctest' in options
744 self.shell.is_doctest = 'doctest' in options
744 self.shell.is_verbatim = 'verbatim' in options
745 self.shell.is_verbatim = 'verbatim' in options
745
746
746 # handle pure python code
747 # handle pure python code
747 if 'python' in self.arguments:
748 if 'python' in self.arguments:
748 content = self.content
749 content = self.content
749 self.content = self.shell.process_pure_python(content)
750 self.content = self.shell.process_pure_python(content)
750
751
751 parts = '\n'.join(self.content).split('\n\n')
752 parts = '\n'.join(self.content).split('\n\n')
752
753
753 lines = ['.. code-block:: ipython', '']
754 lines = ['.. code-block:: ipython', '']
754 figures = []
755 figures = []
755
756
756 for part in parts:
757 for part in parts:
757 block = block_parser(part, rgxin, rgxout, promptin, promptout)
758 block = block_parser(part, rgxin, rgxout, promptin, promptout)
758 if len(block):
759 if len(block):
759 rows, figure = self.shell.process_block(block)
760 rows, figure = self.shell.process_block(block)
760 for row in rows:
761 for row in rows:
761 lines.extend([' %s'%line for line in row.split('\n')])
762 lines.extend([' %s'%line for line in row.split('\n')])
762
763
763 if figure is not None:
764 if figure is not None:
764 figures.append(figure)
765 figures.append(figure)
765
766
766 for figure in figures:
767 for figure in figures:
767 lines.append('')
768 lines.append('')
768 lines.extend(figure.split('\n'))
769 lines.extend(figure.split('\n'))
769 lines.append('')
770 lines.append('')
770
771
771 if len(lines)>2:
772 if len(lines)>2:
772 if debug:
773 if debug:
773 print('\n'.join(lines))
774 print('\n'.join(lines))
774 else:
775 else:
775 # This has to do with input, not output. But if we comment
776 # This has to do with input, not output. But if we comment
776 # these lines out, then no IPython code will appear in the
777 # these lines out, then no IPython code will appear in the
777 # final output.
778 # final output.
778 self.state_machine.insert_input(
779 self.state_machine.insert_input(
779 lines, self.state_machine.input_lines.source(0))
780 lines, self.state_machine.input_lines.source(0))
780
781
781 # cleanup
782 # cleanup
782 self.teardown()
783 self.teardown()
783
784
784 return []
785 return []
785
786
786 # Enable as a proper Sphinx directive
787 # Enable as a proper Sphinx directive
787 def setup(app):
788 def setup(app):
788 setup.app = app
789 setup.app = app
789
790
790 app.add_directive('ipython', IPythonDirective)
791 app.add_directive('ipython', IPythonDirective)
791 app.add_config_value('ipython_savefig_dir', None, 'env')
792 app.add_config_value('ipython_savefig_dir', None, 'env')
792 app.add_config_value('ipython_rgxin',
793 app.add_config_value('ipython_rgxin',
793 re.compile('In \[(\d+)\]:\s?(.*)\s*'), 'env')
794 re.compile('In \[(\d+)\]:\s?(.*)\s*'), 'env')
794 app.add_config_value('ipython_rgxout',
795 app.add_config_value('ipython_rgxout',
795 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), 'env')
796 re.compile('Out\[(\d+)\]:\s?(.*)\s*'), 'env')
796 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
797 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
797 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
798 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
798 # We could just let matplotlib pick whatever is specified as the default
799 # We could just let matplotlib pick whatever is specified as the default
799 # backend in the matplotlibrc file, but this would cause issues if the
800 # backend in the matplotlibrc file, but this would cause issues if the
800 # backend didn't work in headless environments. For this reason, 'agg'
801 # backend didn't work in headless environments. For this reason, 'agg'
801 # is a good default backend choice.
802 # is a good default backend choice.
802 app.add_config_value('ipython_mplbackend', 'agg', 'env')
803 app.add_config_value('ipython_mplbackend', 'agg', 'env')
803 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
804 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
804 # __init__ method will treat it as [].
805 # __init__ method will treat it as [].
805 execlines = ['import numpy as np', 'import matplotlib.pyplot as plt']
806 execlines = ['import numpy as np', 'import matplotlib.pyplot as plt']
806 app.add_config_value('ipython_execlines', execlines, 'env')
807 app.add_config_value('ipython_execlines', execlines, 'env')
807
808
808 # Simple smoke test, needs to be converted to a proper automatic test.
809 # Simple smoke test, needs to be converted to a proper automatic test.
809 def test():
810 def test():
810
811
811 examples = [
812 examples = [
812 r"""
813 r"""
813 In [9]: pwd
814 In [9]: pwd
814 Out[9]: '/home/jdhunter/py4science/book'
815 Out[9]: '/home/jdhunter/py4science/book'
815
816
816 In [10]: cd bookdata/
817 In [10]: cd bookdata/
817 /home/jdhunter/py4science/book/bookdata
818 /home/jdhunter/py4science/book/bookdata
818
819
819 In [2]: from pylab import *
820 In [2]: from pylab import *
820
821
821 In [2]: ion()
822 In [2]: ion()
822
823
823 In [3]: im = imread('stinkbug.png')
824 In [3]: im = imread('stinkbug.png')
824
825
825 @savefig mystinkbug.png width=4in
826 @savefig mystinkbug.png width=4in
826 In [4]: imshow(im)
827 In [4]: imshow(im)
827 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
828 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
828
829
829 """,
830 """,
830 r"""
831 r"""
831
832
832 In [1]: x = 'hello world'
833 In [1]: x = 'hello world'
833
834
834 # string methods can be
835 # string methods can be
835 # used to alter the string
836 # used to alter the string
836 @doctest
837 @doctest
837 In [2]: x.upper()
838 In [2]: x.upper()
838 Out[2]: 'HELLO WORLD'
839 Out[2]: 'HELLO WORLD'
839
840
840 @verbatim
841 @verbatim
841 In [3]: x.st<TAB>
842 In [3]: x.st<TAB>
842 x.startswith x.strip
843 x.startswith x.strip
843 """,
844 """,
844 r"""
845 r"""
845
846
846 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
847 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
847 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
848 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
848
849
849 In [131]: print url.split('&')
850 In [131]: print url.split('&')
850 ['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']
851 ['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']
851
852
852 In [60]: import urllib
853 In [60]: import urllib
853
854
854 """,
855 """,
855 r"""\
856 r"""\
856
857
857 In [133]: import numpy.random
858 In [133]: import numpy.random
858
859
859 @suppress
860 @suppress
860 In [134]: numpy.random.seed(2358)
861 In [134]: numpy.random.seed(2358)
861
862
862 @doctest
863 @doctest
863 In [135]: numpy.random.rand(10,2)
864 In [135]: numpy.random.rand(10,2)
864 Out[135]:
865 Out[135]:
865 array([[ 0.64524308, 0.59943846],
866 array([[ 0.64524308, 0.59943846],
866 [ 0.47102322, 0.8715456 ],
867 [ 0.47102322, 0.8715456 ],
867 [ 0.29370834, 0.74776844],
868 [ 0.29370834, 0.74776844],
868 [ 0.99539577, 0.1313423 ],
869 [ 0.99539577, 0.1313423 ],
869 [ 0.16250302, 0.21103583],
870 [ 0.16250302, 0.21103583],
870 [ 0.81626524, 0.1312433 ],
871 [ 0.81626524, 0.1312433 ],
871 [ 0.67338089, 0.72302393],
872 [ 0.67338089, 0.72302393],
872 [ 0.7566368 , 0.07033696],
873 [ 0.7566368 , 0.07033696],
873 [ 0.22591016, 0.77731835],
874 [ 0.22591016, 0.77731835],
874 [ 0.0072729 , 0.34273127]])
875 [ 0.0072729 , 0.34273127]])
875
876
876 """,
877 """,
877
878
878 r"""
879 r"""
879 In [106]: print x
880 In [106]: print x
880 jdh
881 jdh
881
882
882 In [109]: for i in range(10):
883 In [109]: for i in range(10):
883 .....: print i
884 .....: print i
884 .....:
885 .....:
885 .....:
886 .....:
886 0
887 0
887 1
888 1
888 2
889 2
889 3
890 3
890 4
891 4
891 5
892 5
892 6
893 6
893 7
894 7
894 8
895 8
895 9
896 9
896 """,
897 """,
897
898
898 r"""
899 r"""
899
900
900 In [144]: from pylab import *
901 In [144]: from pylab import *
901
902
902 In [145]: ion()
903 In [145]: ion()
903
904
904 # use a semicolon to suppress the output
905 # use a semicolon to suppress the output
905 @savefig test_hist.png width=4in
906 @savefig test_hist.png width=4in
906 In [151]: hist(np.random.randn(10000), 100);
907 In [151]: hist(np.random.randn(10000), 100);
907
908
908
909
909 @savefig test_plot.png width=4in
910 @savefig test_plot.png width=4in
910 In [151]: plot(np.random.randn(10000), 'o');
911 In [151]: plot(np.random.randn(10000), 'o');
911 """,
912 """,
912
913
913 r"""
914 r"""
914 # use a semicolon to suppress the output
915 # use a semicolon to suppress the output
915 In [151]: plt.clf()
916 In [151]: plt.clf()
916
917
917 @savefig plot_simple.png width=4in
918 @savefig plot_simple.png width=4in
918 In [151]: plot([1,2,3])
919 In [151]: plot([1,2,3])
919
920
920 @savefig hist_simple.png width=4in
921 @savefig hist_simple.png width=4in
921 In [151]: hist(np.random.randn(10000), 100);
922 In [151]: hist(np.random.randn(10000), 100);
922
923
923 """,
924 """,
924 r"""
925 r"""
925 # update the current fig
926 # update the current fig
926 In [151]: ylabel('number')
927 In [151]: ylabel('number')
927
928
928 In [152]: title('normal distribution')
929 In [152]: title('normal distribution')
929
930
930
931
931 @savefig hist_with_text.png
932 @savefig hist_with_text.png
932 In [153]: grid(True)
933 In [153]: grid(True)
933
934
934 @doctest float
935 @doctest float
935 In [154]: 0.1 + 0.2
936 In [154]: 0.1 + 0.2
936 Out[154]: 0.3
937 Out[154]: 0.3
937
938
938 @doctest float
939 @doctest float
939 In [155]: np.arange(16).reshape(4,4)
940 In [155]: np.arange(16).reshape(4,4)
940 Out[155]:
941 Out[155]:
941 array([[ 0, 1, 2, 3],
942 array([[ 0, 1, 2, 3],
942 [ 4, 5, 6, 7],
943 [ 4, 5, 6, 7],
943 [ 8, 9, 10, 11],
944 [ 8, 9, 10, 11],
944 [12, 13, 14, 15]])
945 [12, 13, 14, 15]])
945
946
946 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
947 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
947
948
948 In [2]: x[0,0] = np.inf
949 In [2]: x[0,0] = np.inf
949
950
950 In [3]: x[0,1] = np.nan
951 In [3]: x[0,1] = np.nan
951
952
952 @doctest float
953 @doctest float
953 In [4]: x
954 In [4]: x
954 Out[4]:
955 Out[4]:
955 array([[ inf, nan, 2., 3.],
956 array([[ inf, nan, 2., 3.],
956 [ 4., 5., 6., 7.],
957 [ 4., 5., 6., 7.],
957 [ 8., 9., 10., 11.],
958 [ 8., 9., 10., 11.],
958 [ 12., 13., 14., 15.]])
959 [ 12., 13., 14., 15.]])
959
960
960
961
961 """,
962 """,
962 ]
963 ]
963 # skip local-file depending first example:
964 # skip local-file depending first example:
964 examples = examples[1:]
965 examples = examples[1:]
965
966
966 #ipython_directive.DEBUG = True # dbg
967 #ipython_directive.DEBUG = True # dbg
967 #options = dict(suppress=True) # dbg
968 #options = dict(suppress=True) # dbg
968 options = dict()
969 options = dict()
969 for example in examples:
970 for example in examples:
970 content = example.split('\n')
971 content = example.split('\n')
971 IPythonDirective('debug', arguments=None, options=options,
972 IPythonDirective('debug', arguments=None, options=options,
972 content=content, lineno=0,
973 content=content, lineno=0,
973 content_offset=None, block_text=None,
974 content_offset=None, block_text=None,
974 state=None, state_machine=None,
975 state=None, state_machine=None,
975 )
976 )
976
977
977 # Run test suite as a script
978 # Run test suite as a script
978 if __name__=='__main__':
979 if __name__=='__main__':
979 if not os.path.isdir('_static'):
980 if not os.path.isdir('_static'):
980 os.mkdir('_static')
981 os.mkdir('_static')
981 test()
982 test()
982 print('All OK? Check figures in _static/')
983 print('All OK? Check figures in _static/')
General Comments 0
You need to be logged in to leave comments. Login now