-
Notifications
You must be signed in to change notification settings - Fork 91
Expand file tree
/
Copy path__init__.py
More file actions
249 lines (198 loc) · 7.86 KB
/
__init__.py
File metadata and controls
249 lines (198 loc) · 7.86 KB
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
"""Roles/directives for evaluating variables in the notebook."""
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING, Any
from docutils import nodes
from docutils.parsers.rst import directives as spec
from myst_nb.core.execute.base import EvalNameError
from myst_nb.core.render import NbElementRenderer
from myst_nb.core.variables import (
RetrievalError,
VariableOutput,
create_warning,
render_variable_outputs,
)
from myst_nb.ext.utils import DirectiveBase, RoleBase
try:
from sphinx.domains import Domain
except ImportError:
# for docutils only use
Domain = object # type: ignore
if TYPE_CHECKING:
from sphinx.application import Sphinx
from myst_nb.docutils_ import DocutilsApp
eval_warning = partial(create_warning, subtype="eval")
def retrieve_eval_data(document: nodes.document, key: str) -> list[VariableOutput]:
"""Retrieve the glue data from a specific document."""
if "nb_renderer" not in document:
raise RetrievalError("This document does not have a running kernel")
element: NbElementRenderer = document["nb_renderer"]
# evaluate the variable
try:
outputs = element.renderer.nb_client.eval_variable(key)
except NotImplementedError:
raise RetrievalError("This document does not have a running kernel")
except EvalNameError:
raise RetrievalError(
f"The expression {key!r} is not valid according to the configured pattern"
)
except Exception as exc:
raise RetrievalError(f"variable evaluation error: {exc}")
if not outputs:
raise RetrievalError(f"expression {key!r} does not return any outputs")
# the returned outputs could be one of the following:
# https://nbformat.readthedocs.io/en/latest/format_description.html#code-cell-outputs
for output in outputs:
if output.get("output_type") == "error":
msg = f"{output.get('ename', '')}: {output.get('evalue', '')}"
raise RetrievalError(msg)
return [
VariableOutput(
data=output.get("data", {}),
metadata=output.get("metadata", {}),
nb_renderer=element,
vtype="eval",
index=i,
)
for i, output in enumerate(outputs)
]
class EvalRoleAny(RoleBase):
"""A role for evaluating value outputs from the kernel,
using render priority to decide the output mime type.
"""
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
try:
data = retrieve_eval_data(self.document, self.text)
except RetrievalError as exc:
return [], [eval_warning(str(exc), self.document, self.line)]
# for text/plain, we want to strip quotes from strings
for output in data:
output.metadata["strip_text_quotes"] = True
_nodes = render_variable_outputs(
data,
self.document,
self.line,
self.source,
inline=True,
)
return _nodes, []
class EvalDirectiveAny(DirectiveBase):
"""A directive for evaluating value outputs from the kernel,
using render priority to decide the output mime type.
"""
required_arguments = 1 # the key
final_argument_whitespace = False
has_content = False
def run(self) -> list[nodes.Node]:
"""Run the directive."""
try:
data = retrieve_eval_data(self.document, self.arguments[0])
except RetrievalError as exc:
return [eval_warning(str(exc), self.document, self.line)]
return render_variable_outputs(
data,
self.document,
self.line,
self.source,
inline=False,
)
class EvalFigureDirective(DirectiveBase):
"""A directive for pasting code outputs from notebooks, wrapped in a figure.
Mirrors:
https://github.com/docutils-mirror/docutils/blob/9649abee47b4ce4db51be1d90fcb1fb500fa78b3/docutils/parsers/rst/directives/images.py#95
"""
required_arguments = 1 # the key
final_argument_whitespace = False
has_content = True
def align(argument):
return spec.choice(argument, ("left", "center", "right"))
def figwidth_value(argument):
return spec.length_or_percentage_or_unitless(argument, "px")
option_spec = {
# note we don't add converters for image options,
# since this is handled in `NbElementRenderer.render_image`
"alt": spec.unchanged,
"height": spec.unchanged,
"width": spec.unchanged,
"scale": spec.unchanged,
"class": spec.unchanged,
"figwidth": figwidth_value,
"figclass": spec.class_option,
"align": align,
"name": spec.unchanged,
}
def run(self):
try:
data = retrieve_eval_data(self.document, self.arguments[0])
except RetrievalError as exc:
return [eval_warning(str(exc), self.document, self.line)]
render: dict[str, Any] = {}
for key in ("alt", "height", "width", "scale", "class"):
if key in self.options:
render.setdefault("image", {})[
key.replace("classes", "class")
] = self.options[key]
mime_nodes = render_variable_outputs(
data, self.document, self.line, self.source, render=render
)
# note: most of this is copied directly from sphinx.Figure
# create figure node
figure_node = nodes.figure("", *mime_nodes)
self.set_source_info(figure_node)
# add attributes
figwidth = self.options.pop("figwidth", None)
figclasses = self.options.pop("figclass", None)
align = self.options.pop("align", None)
if figwidth is not None:
figure_node["width"] = figwidth
if figclasses:
figure_node["classes"] += figclasses
if align:
figure_node["align"] = align
# add target
self.add_name(figure_node)
# create the caption and legend
if self.content:
node = nodes.Element() # anonymous container for parsing
self.state.nested_parse(self.content, self.content_offset, node)
first_node = node[0]
if isinstance(first_node, nodes.paragraph):
caption = nodes.caption(first_node.rawsource, "", *first_node.children)
caption.source = first_node.source
caption.line = first_node.line
figure_node += caption
elif not (isinstance(first_node, nodes.comment) and len(first_node) == 0):
error = eval_warning(
"Figure caption must be a paragraph or empty comment.",
self.document,
self.lineno,
)
return [figure_node, error]
if len(node) > 1:
figure_node += nodes.legend("", *node[1:])
return [figure_node]
class NbEvalDomain(Domain):
"""A sphinx domain for defining eval roles and directives.
Note, the only reason we use this,
is that sphinx will not allow for `:` in a directive/role name,
if it is part of a domain.
"""
name = "eval"
label = "NotebookEval"
# data version, bump this when the format of self.data changes
data_version = 1
directives = {"figure": EvalFigureDirective}
roles: dict = {}
def merge_domaindata(self, *args, **kwargs):
pass
def resolve_any_xref(self, *args, **kwargs):
return []
def load_eval_sphinx(app: Sphinx) -> None:
"""Load the eval domain."""
app.add_role("eval", EvalRoleAny(), override=True)
app.add_directive("eval", EvalDirectiveAny, override=True)
app.add_domain(NbEvalDomain)
def load_eval_docutils(app: DocutilsApp) -> None:
app.roles["eval"] = EvalRoleAny()
app.directives["eval"] = EvalDirectiveAny
app.directives["eval:figure"] = EvalFigureDirective