-
Notifications
You must be signed in to change notification settings - Fork 86
/
Copy path__init__.py
415 lines (346 loc) · 14.5 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
__version__ = "0.13.0"
import os
from collections.abc import Sequence
from pathlib import Path
from typing import cast
from docutils import nodes as docnodes
from IPython.lib.lexers import IPython3Lexer, IPythonTracebackLexer
from ipywidgets.embed import DEFAULT_EMBED_REQUIREJS_URL, DEFAULT_EMBED_SCRIPT_URL
from jupyter_sphinx import REQUIRE_URL_DEFAULT
from jupyter_sphinx.ast import JupyterWidgetStateNode, JupyterWidgetViewNode
from jupyter_sphinx.utils import sphinx_abs_dir
from myst_parser import setup_sphinx as setup_myst_parser
from sphinx.addnodes import download_reference
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment import BuildEnvironment
from sphinx.errors import SphinxError
from sphinx.util import import_object, logging
from sphinx.util.docutils import ReferenceRole, SphinxDirective
from .exec_table import setup_exec_table
from .execution import update_execution_cache
from .nb_glue import glue # noqa: F401
from .nb_glue.domain import (
NbGlueDomain,
PasteInlineNode,
PasteMathNode,
PasteNode,
PasteTextNode,
)
from .nb_glue.transform import PasteNodesToDocutils
from .nodes import CellInputNode, CellNode, CellOutputBundleNode, CellOutputNode
from .parser import NotebookParser
from .render_outputs import (
CellOutputsToNodes,
get_default_render_priority,
load_renderer,
)
LOGGER = logging.getLogger(__name__)
def setup(app: Sphinx):
"""Initialize Sphinx extension."""
# Allow parsing ipynb files
app.add_source_suffix(".md", "myst-nb")
app.add_source_suffix(".ipynb", "myst-nb")
app.add_source_parser(NotebookParser)
app.setup_extension("sphinx_togglebutton")
# Helper functions for the registry, pulled from jupyter-sphinx
def skip(self, node):
raise docnodes.SkipNode
# Used to render an element node as HTML
def visit_element_html(self, node):
self.body.append(node.html())
raise docnodes.SkipNode
# Shortcut for registering our container nodes
render_container = (
lambda self, node: self.visit_container(node),
lambda self, node: self.depart_container(node),
)
# Register our container nodes, these should behave just like a regular container
for node in [CellNode, CellInputNode, CellOutputNode]:
app.add_node(
node,
override=True,
html=(render_container),
latex=(render_container),
textinfo=(render_container),
text=(render_container),
man=(render_container),
)
# Register the output bundle node.
# No translators should touch this node because we'll replace it in a post-transform
app.add_node(
CellOutputBundleNode,
override=True,
html=(skip, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
# these nodes hold widget state/view JSON,
# but are only rendered properly in HTML documents.
for node in [JupyterWidgetStateNode, JupyterWidgetViewNode]:
app.add_node(
node,
override=True,
html=(visit_element_html, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
# Register our inline nodes so they can be parsed as a part of titles
# No translators should touch these nodes because we'll replace them in a transform
for node in [PasteMathNode, PasteNode, PasteTextNode, PasteInlineNode]:
app.add_node(
node,
override=True,
html=(skip, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
)
# Add configuration for the cache
app.add_config_value("jupyter_cache", "", "env")
app.add_config_value("execution_excludepatterns", [], "env")
app.add_config_value("jupyter_execute_notebooks", "auto", "env")
app.add_config_value("execution_timeout", 30, "env")
app.add_config_value("execution_allow_errors", False, "env")
app.add_config_value("execution_in_temp", False, "env")
# show traceback in stdout (in addition to writing to file)
# this is useful in e.g. RTD where one cannot inspect a file
app.add_config_value("execution_show_tb", False, "")
app.add_config_value("nb_custom_formats", {}, "env")
# render config
app.add_config_value("nb_render_key", "render", "env")
app.add_config_value("nb_render_priority", {}, "env")
app.add_config_value("nb_render_plugin", "default", "env")
app.add_config_value("nb_render_text_lexer", "myst-ansi", "env")
app.add_config_value("nb_output_stderr", "show", "env")
# Register our post-transform which will convert output bundles to nodes
app.add_post_transform(PasteNodesToDocutils)
app.add_post_transform(CellOutputsToNodes)
# Add myst-parser transforms and configuration
setup_myst_parser(app)
# Events
app.connect("config-inited", validate_config_values)
app.connect("builder-inited", static_path)
app.connect("builder-inited", set_valid_execution_paths)
app.connect("builder-inited", set_up_execution_data)
app.connect("builder-inited", set_render_priority)
app.connect("env-purge-doc", remove_execution_data)
app.connect("env-get-outdated", update_execution_cache)
app.connect("config-inited", add_exclude_patterns)
app.connect("config-inited", update_togglebutton_classes)
app.connect("env-updated", save_glue_cache)
app.connect("config-inited", add_nb_custom_formats)
app.connect("env-updated", load_ipywidgets_js)
from myst_nb.ansi_lexer import AnsiColorLexer
# For syntax highlighting
app.add_lexer("ipythontb", IPythonTracebackLexer)
app.add_lexer("ipython", IPython3Lexer)
app.add_lexer("myst-ansi", AnsiColorLexer)
# Add components
app.add_directive("code-cell", CodeCell)
app.add_role("nb-download", JupyterDownloadRole())
app.add_css_file("mystnb.css")
app.add_domain(NbGlueDomain)
# execution statistics table
setup_exec_table(app)
# TODO need to deal with key clashes in NbGlueDomain.merge_domaindata
# before this is parallel_read_safe
return {"version": __version__, "parallel_read_safe": False}
class MystNbConfigError(SphinxError):
"""Error specific to MyST-NB."""
category = "MyST NB Configuration Error"
def validate_config_values(app: Sphinx, config):
"""Validate configuration values."""
execute_mode = app.config["jupyter_execute_notebooks"]
if execute_mode not in ["force", "auto", "cache", "off"]:
raise MystNbConfigError(
"'jupyter_execute_notebooks' can be: "
f"`force`, `auto`, `cache` or `off`, but got: {execute_mode}",
)
if app.config["jupyter_cache"] and execute_mode != "cache":
raise MystNbConfigError(
"'jupyter_cache' is set, "
f"but 'jupyter_execute_notebooks' is not `cache`: {execute_mode}"
)
if app.config["jupyter_cache"] and not os.path.isdir(app.config["jupyter_cache"]):
raise MystNbConfigError(
f"'jupyter_cache' is not a directory: {app.config['jupyter_cache']}",
)
if not isinstance(app.config["nb_custom_formats"], dict):
raise MystNbConfigError(
"'nb_custom_formats' should be a dictionary: "
f"{app.config['nb_custom_formats']}"
)
for name, converter in app.config["nb_custom_formats"].items():
if not isinstance(name, str):
raise MystNbConfigError(
f"'nb_custom_formats' keys should br a string: {name}"
)
if isinstance(converter, str):
app.config["nb_custom_formats"][name] = (converter, {})
elif not (isinstance(converter, Sequence) and len(converter) in [2, 3]):
raise MystNbConfigError(
"'nb_custom_formats' values must be "
f"either strings or 2/3-element sequences, got: {converter}"
)
converter_str = app.config["nb_custom_formats"][name][0]
caller = import_object(
converter_str,
f"MyST-NB nb_custom_formats: {name}",
)
if not callable(caller):
raise MystNbConfigError(
f"`nb_custom_formats.{name}` converter is not callable: {caller}"
)
if len(app.config["nb_custom_formats"][name]) == 2:
app.config["nb_custom_formats"][name].append(None)
elif not isinstance(app.config["nb_custom_formats"][name][2], bool):
raise MystNbConfigError(
f"`nb_custom_formats.{name}.commonmark_only` arg is not boolean"
)
if not isinstance(app.config["nb_render_key"], str):
raise MystNbConfigError("`nb_render_key` is not a string")
if app.config["nb_output_stderr"] not in [
"show",
"remove",
"remove-warn",
"warn",
"error",
"severe",
]:
raise MystNbConfigError(
"`nb_output_stderr` not one of: "
"'show', 'remove', 'remove-warn', 'warn', 'error', 'severe'"
)
# try loading notebook output renderer
load_renderer(app.config["nb_render_plugin"])
def static_path(app: Sphinx):
static_path = Path(__file__).absolute().with_name("_static")
app.config.html_static_path.append(str(static_path))
def load_ipywidgets_js(app: Sphinx, env: BuildEnvironment) -> None:
"""Add ipywidget JavaScript to HTML pages.
We adapt the code in sphinx.ext.mathjax,
to only add this JS if widgets have been found in any notebooks.
(ideally we would only add it to the pages containing widgets,
but this is not trivial in sphinx)
There are 2 cases:
- ipywidgets 7, with require
- ipywidgets 7, no require
We reuse settings, if available, for jupyter-sphinx
"""
if app.builder.format != "html" or not app.env.nb_contains_widgets:
return
builder = cast(StandaloneHTMLBuilder, app.builder)
require_url_default = (
REQUIRE_URL_DEFAULT
if "jupyter_sphinx_require_url" not in app.config
else app.config.jupyter_sphinx_require_url
)
embed_url_default = (
None
if "jupyter_sphinx_embed_url" not in app.config
else app.config.jupyter_sphinx_embed_url
)
if require_url_default:
builder.add_js_file(require_url_default)
embed_url = embed_url_default or DEFAULT_EMBED_REQUIREJS_URL
else:
embed_url = embed_url_default or DEFAULT_EMBED_SCRIPT_URL
if embed_url:
builder.add_js_file(embed_url)
def set_render_priority(app: Sphinx):
"""Set the render priority for the particular builder."""
builder = app.builder.name
if app.config.nb_render_priority and builder in app.config.nb_render_priority:
app.env.nb_render_priority = app.config.nb_render_priority[builder]
else:
app.env.nb_render_priority = get_default_render_priority(builder)
if app.env.nb_render_priority is None:
raise MystNbConfigError(f"`nb_render_priority` not set for builder: {builder}")
try:
for item in app.env.nb_render_priority:
assert isinstance(item, str)
except Exception:
raise MystNbConfigError(
f"`nb_render_priority` is not a list of str: {app.env.nb_render_priority}"
)
def set_valid_execution_paths(app: Sphinx):
"""Set files excluded from execution, and valid file suffixes
Patterns given in execution_excludepatterns conf variable from executing.
"""
app.env.nb_excluded_exec_paths = {
str(path)
for pat in app.config["execution_excludepatterns"]
for path in Path().cwd().rglob(pat)
}
LOGGER.verbose("MyST-NB: Excluded Paths: %s", app.env.nb_excluded_exec_paths)
app.env.nb_allowed_exec_suffixes = {
suffix
for suffix, parser_type in app.config["source_suffix"].items()
if parser_type in ("myst-nb",)
}
app.env.nb_contains_widgets = False
def set_up_execution_data(app: Sphinx):
if not hasattr(app.env, "nb_execution_data"):
app.env.nb_execution_data = {}
if not hasattr(app.env, "nb_execution_data_changed"):
app.env.nb_execution_data_changed = False
app.env.nb_execution_data_changed = False
def remove_execution_data(app: Sphinx, env, docname):
if docname in app.env.nb_execution_data:
app.env.nb_execution_data.pop(docname)
app.env.nb_execution_data_changed = True
def add_nb_custom_formats(app: Sphinx, config):
"""Add custom conversion formats."""
for suffix in config.nb_custom_formats:
app.add_source_suffix(suffix, "myst-nb")
def add_exclude_patterns(app: Sphinx, config):
"""Add default exclude patterns (if not already present)."""
if "**.ipynb_checkpoints" not in config.exclude_patterns:
config.exclude_patterns.append("**.ipynb_checkpoints")
def update_togglebutton_classes(app: Sphinx, config):
to_add = [
".tag_hide_input div.cell_input",
".tag_hide-input div.cell_input",
".tag_hide_output div.cell_output",
".tag_hide-output div.cell_output",
".tag_hide_cell.cell",
".tag_hide-cell.cell",
]
for selector in to_add:
config.togglebutton_selector += f", {selector}"
def save_glue_cache(app: Sphinx, env):
NbGlueDomain.from_env(env).write_cache()
class JupyterDownloadRole(ReferenceRole):
def run(self):
reftarget = sphinx_abs_dir(self.env, self.target)
node = download_reference(self.rawtext, reftarget=reftarget)
self.set_source_info(node)
title = self.title if self.has_explicit_title else self.target
node += docnodes.literal(
self.rawtext, title, classes=["xref", "download", "myst-nb"]
)
return [node], []
class CodeCell(SphinxDirective):
"""Raises a warning if it is triggered, it should not make it to the doctree."""
optional_arguments = 1
final_argument_whitespace = True
has_content = True
def run(self):
LOGGER.warning(
(
"Found an unexpected `code-cell` directive. "
"Either this file was not converted to a notebook, "
"because Jupytext header content was missing, "
"or the `code-cell` was not converted, because it is nested. "
"See https://myst-nb.readthedocs.io/en/latest/use/markdown.html "
"for more information."
),
location=(self.env.docname, self.lineno),
)
return []