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