|
| 1 | +"""Simple sphinx extension that executes code in jupyter and inserts output.""" |
| 2 | + |
1 | 3 | from ._version import version_info, __version__
|
| 4 | +from sphinx.util import logging |
| 5 | +import docutils |
| 6 | +import ipywidgets |
| 7 | +import os |
| 8 | +from sphinx.util.fileutil import copy_asset |
| 9 | +from IPython.lib.lexers import IPythonTracebackLexer, IPython3Lexer |
| 10 | + |
| 11 | +from .ast import ( |
| 12 | + JupyterCell, |
| 13 | + JupyterCellNode, |
| 14 | + JupyterKernelNode, |
| 15 | + JupyterWidgetViewNode, |
| 16 | + JupyterWidgetStateNode, |
| 17 | + WIDGET_VIEW_MIMETYPE, |
| 18 | + jupyter_download_role, |
| 19 | +) |
| 20 | +from .execute import JupyterKernel, ExecuteJupyterCells |
| 21 | +from .thebelab import ThebeButton, ThebeButtonNode, ThebeOutputNode, ThebeSourceNode |
| 22 | + |
| 23 | +REQUIRE_URL_DEFAULT = ( |
| 24 | + "https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js" |
| 25 | +) |
| 26 | +THEBELAB_URL_DEFAULT = "https://unpkg.com/thebelab@^0.4.0" |
| 27 | + |
| 28 | +logger = logging.getLogger(__name__) |
| 29 | + |
| 30 | +############################################################################## |
| 31 | +# Constants and functions we'll use later |
| 32 | + |
| 33 | +# Used for nodes that do not need to be rendered |
| 34 | +def skip(self, node): |
| 35 | + raise docutils.nodes.SkipNode |
| 36 | + |
| 37 | +# Renders the children of a container |
| 38 | +render_container = ( |
| 39 | + lambda self, node: self.visit_container(node), |
| 40 | + lambda self, node: self.depart_container(node), |
| 41 | +) |
| 42 | + |
| 43 | +# Used to render the container and its children as HTML |
| 44 | +def visit_container_html(self, node): |
| 45 | + self.body.append(node.visit_html()) |
| 46 | + self.visit_container(node) |
| 47 | + |
| 48 | +def depart_container_html(self, node): |
| 49 | + self.depart_container(node) |
| 50 | + self.body.append(node.depart_html()) |
| 51 | + |
| 52 | +# Used to render an element node as HTML |
| 53 | +def visit_element_html(self, node): |
| 54 | + self.body.append(node.html()) |
| 55 | + raise docutils.nodes.SkipNode |
| 56 | + |
| 57 | +# Used to render the ThebeSourceNode conditionally for non-HTML builders |
| 58 | +def visit_thebe_source(self, node): |
| 59 | + if node["hide_code"]: |
| 60 | + raise docutils.nodes.SkipNode |
| 61 | + else: |
| 62 | + self.visit_container(node) |
| 63 | + |
| 64 | +render_thebe_source = ( |
| 65 | + visit_thebe_source, |
| 66 | + lambda self, node: self.depart_container(node), |
| 67 | +) |
| 68 | + |
| 69 | +############################################################################## |
| 70 | +# Sphinx callback functions |
| 71 | +def builder_inited(app): |
| 72 | + """ |
| 73 | + 2 cases |
| 74 | + case 1: ipywidgets 7, with require |
| 75 | + case 2: ipywidgets 7, no require |
| 76 | + """ |
| 77 | + require_url = app.config.jupyter_sphinx_require_url |
| 78 | + if require_url: |
| 79 | + app.add_js_file(require_url) |
| 80 | + embed_url = ( |
| 81 | + app.config.jupyter_sphinx_embed_url |
| 82 | + or ipywidgets.embed.DEFAULT_EMBED_REQUIREJS_URL |
| 83 | + ) |
| 84 | + else: |
| 85 | + embed_url = ( |
| 86 | + app.config.jupyter_sphinx_embed_url |
| 87 | + or ipywidgets.embed.DEFAULT_EMBED_SCRIPT_URL |
| 88 | + ) |
| 89 | + if embed_url: |
| 90 | + app.add_js_file(embed_url) |
| 91 | + |
| 92 | + # add jupyter-sphinx css |
| 93 | + app.add_css_file("jupyter-sphinx.css") |
| 94 | + # Check if a thebelab config was specified |
| 95 | + if app.config.jupyter_sphinx_thebelab_config: |
| 96 | + app.add_js_file("thebelab-helper.js") |
| 97 | + app.add_css_file("thebelab.css") |
| 98 | + |
| 99 | + |
| 100 | +def build_finished(app, env): |
| 101 | + if app.builder.format != "html": |
| 102 | + return |
| 103 | + |
| 104 | + # Copy stylesheet |
| 105 | + src = os.path.join(os.path.dirname(__file__), "css") |
| 106 | + dst = os.path.join(app.outdir, "_static") |
| 107 | + copy_asset(src, dst) |
| 108 | + |
| 109 | + thebe_config = app.config.jupyter_sphinx_thebelab_config |
| 110 | + if not thebe_config: |
| 111 | + return |
| 112 | + |
| 113 | + # Copy all thebelab related assets |
| 114 | + src = os.path.join(os.path.dirname(__file__), "thebelab") |
| 115 | + dst = os.path.join(app.outdir, "_static") |
| 116 | + copy_asset(src, dst) |
| 117 | + |
| 118 | + |
| 119 | +############################################################################## |
| 120 | +# Main setup |
| 121 | +def setup(app): |
| 122 | + """A temporary setup function so that we can use it here and in execute. |
| 123 | +
|
| 124 | + This should be removed and converted into `setup` after a deprecation |
| 125 | + cycle. |
| 126 | + """ |
| 127 | + # Configuration |
| 128 | + |
| 129 | + app.add_config_value( |
| 130 | + "jupyter_execute_kwargs", |
| 131 | + dict(timeout=-1, allow_errors=True, store_widget_state=True), |
| 132 | + "env", |
| 133 | + ) |
| 134 | + app.add_config_value("jupyter_execute_default_kernel", "python3", "env") |
| 135 | + app.add_config_value( |
| 136 | + "jupyter_execute_data_priority", |
| 137 | + [ |
| 138 | + WIDGET_VIEW_MIMETYPE, |
| 139 | + "application/javascript", |
| 140 | + "text/html", |
| 141 | + "image/svg+xml", |
| 142 | + "image/png", |
| 143 | + "image/jpeg", |
| 144 | + "text/latex", |
| 145 | + "text/plain", |
| 146 | + ], |
| 147 | + "env", |
| 148 | + ) |
| 149 | + |
| 150 | + # ipywidgets config |
| 151 | + app.add_config_value("jupyter_sphinx_require_url", REQUIRE_URL_DEFAULT, "html") |
| 152 | + app.add_config_value("jupyter_sphinx_embed_url", None, "html") |
| 153 | + |
| 154 | + # thebelab config, can be either a filename or a dict |
| 155 | + app.add_config_value("jupyter_sphinx_thebelab_config", None, "html") |
| 156 | + app.add_config_value("jupyter_sphinx_thebelab_url", THEBELAB_URL_DEFAULT, "html") |
| 157 | + |
| 158 | + # linenos config |
| 159 | + app.add_config_value("jupyter_sphinx_linenos", False, "env") |
| 160 | + app.add_config_value("jupyter_sphinx_continue_linenos", False, "env") |
| 161 | + |
| 162 | + # JupyterKernelNode is just a doctree marker for the |
| 163 | + # ExecuteJupyterCells transform, so we don't actually render it. |
| 164 | + app.add_node( |
| 165 | + JupyterKernelNode, |
| 166 | + html=(skip, None), |
| 167 | + latex=(skip, None), |
| 168 | + textinfo=(skip, None), |
| 169 | + text=(skip, None), |
| 170 | + man=(skip, None), |
| 171 | + ) |
| 172 | + |
| 173 | + # JupyterCellNode is a container that holds the input and |
| 174 | + # any output, so we render it as a container. |
| 175 | + app.add_node( |
| 176 | + JupyterCellNode, |
| 177 | + html=render_container, |
| 178 | + latex=render_container, |
| 179 | + textinfo=render_container, |
| 180 | + text=render_container, |
| 181 | + man=render_container, |
| 182 | + ) |
| 183 | + |
| 184 | + # JupyterWidgetViewNode holds widget view JSON, |
| 185 | + # but is only rendered properly in HTML documents. |
| 186 | + app.add_node( |
| 187 | + JupyterWidgetViewNode, |
| 188 | + html=(visit_element_html, None), |
| 189 | + latex=(skip, None), |
| 190 | + textinfo=(skip, None), |
| 191 | + text=(skip, None), |
| 192 | + man=(skip, None), |
| 193 | + ) |
| 194 | + # JupyterWidgetStateNode holds the widget state JSON, |
| 195 | + # but is only rendered in HTML documents. |
| 196 | + app.add_node( |
| 197 | + JupyterWidgetStateNode, |
| 198 | + html=(visit_element_html, None), |
| 199 | + latex=(skip, None), |
| 200 | + textinfo=(skip, None), |
| 201 | + text=(skip, None), |
| 202 | + man=(skip, None), |
| 203 | + ) |
| 204 | + |
| 205 | + # ThebeSourceNode holds the source code and is rendered if |
| 206 | + # hide-code is not specified. For HTML it is always rendered, |
| 207 | + # but hidden using the stylesheet |
| 208 | + app.add_node( |
| 209 | + ThebeSourceNode, |
| 210 | + html=(visit_container_html, depart_container_html), |
| 211 | + latex=render_thebe_source, |
| 212 | + textinfo=render_thebe_source, |
| 213 | + text=render_thebe_source, |
| 214 | + man=render_thebe_source, |
| 215 | + ) |
| 216 | + |
| 217 | + # ThebeOutputNode holds the output of the Jupyter cells |
| 218 | + # and is rendered if hide-output is not specified. |
| 219 | + app.add_node( |
| 220 | + ThebeOutputNode, |
| 221 | + html=(visit_container_html, depart_container_html), |
| 222 | + latex=render_container, |
| 223 | + textinfo=render_container, |
| 224 | + text=render_container, |
| 225 | + man=render_container, |
| 226 | + ) |
| 227 | + |
| 228 | + # ThebeButtonNode is the button that activates thebelab |
| 229 | + # and is only rendered for the HTML builder |
| 230 | + app.add_node( |
| 231 | + ThebeButtonNode, |
| 232 | + html=(visit_element_html, None), |
| 233 | + latex=(skip, None), |
| 234 | + textinfo=(skip, None), |
| 235 | + text=(skip, None), |
| 236 | + man=(skip, None), |
| 237 | + ) |
| 238 | + |
| 239 | + app.add_directive("jupyter-execute", JupyterCell) |
| 240 | + app.add_directive("jupyter-kernel", JupyterKernel) |
| 241 | + app.add_directive("thebe-button", ThebeButton) |
| 242 | + app.add_role("jupyter-download:notebook", jupyter_download_role) |
| 243 | + app.add_role("jupyter-download:script", jupyter_download_role) |
| 244 | + app.add_transform(ExecuteJupyterCells) |
| 245 | + |
| 246 | + # For syntax highlighting |
| 247 | + app.add_lexer("ipythontb", IPythonTracebackLexer()) |
| 248 | + app.add_lexer("ipython", IPython3Lexer()) |
| 249 | + |
| 250 | + app.connect("builder-inited", builder_inited) |
| 251 | + app.connect("build-finished", build_finished) |
| 252 | + |
| 253 | + return {"version": __version__, "parallel_read_safe": True} |
0 commit comments