-
Notifications
You must be signed in to change notification settings - Fork 91
Expand file tree
/
Copy pathbase.py
More file actions
179 lines (141 loc) · 5.48 KB
/
base.py
File metadata and controls
179 lines (141 loc) · 5.48 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
"""Module for executing notebooks."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from nbformat import NotebookNode
from typing_extensions import TypedDict, final
from myst_nb.core.config import NbParserConfig
from myst_nb.core.loggers import LoggerType
from myst_nb.core.nb_to_tokens import nb_node_to_dict
from myst_nb.ext.glue import extract_glue_data
class ExecutionResult(TypedDict):
"""Result of executing a notebook."""
mtime: float
"""POSIX timestamp of the execution time"""
runtime: float | None
"""runtime in seconds"""
method: str
"""method used to execute the notebook"""
succeeded: bool
"""True if the notebook executed successfully"""
error: str | None
"""error type if the notebook failed to execute"""
traceback: str | None
"""traceback if the notebook failed"""
class ExecutionError(Exception):
"""An exception for failed execution and `execution_raise_on_error` is true."""
class EvalNameError(Exception):
"""An exception for if an evaluation variable name is invalid."""
class NotebookClientBase:
"""A base client for interacting with Jupyter notebooks.
This class is intended to be used as a context manager,
and should only be entered once.
Subclasses should override the `start_client` and `close_client` methods.
"""
def __init__(
self,
notebook: NotebookNode,
path: Path | None,
nb_config: NbParserConfig,
logger: LoggerType,
**kwargs: Any,
):
"""Initialize the client."""
self._notebook = notebook
self._path = path
self._nb_config = nb_config
self._logger = logger
self._kwargs = kwargs
self._glue_data: dict[str, NotebookNode] = {}
self._exec_metadata: ExecutionResult | None = None
# get or create source map of cell to source line
# use 1-based indexing rather than 0, or pseudo base of the cell index
_source_map: list[int] = notebook.metadata.get("source_map", None)
self._source_map = [
(_source_map[i] if _source_map else ((i + 1) * 10000)) + 1
for i, _ in enumerate(notebook.cells)
]
@final
def __enter__(self) -> NotebookClientBase:
"""Enter the context manager."""
self.start_client()
# extract glue data from the notebook
self._glue_data = extract_glue_data(
self.notebook, self._source_map, self.logger
)
return self
@final
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit the context manager."""
self.close_client(exc_type, exc_val, exc_tb)
def start_client(self):
"""Start the client."""
def finalise_client(self):
"""Finalise the client.
This is called before final rendering and should be used,
for example, to finalise the widget state on the metadata.
"""
def close_client(self, exc_type, exc_val, exc_tb):
"""Close the client."""
@property
def notebook(self) -> NotebookNode:
"""Get the notebook."""
return self._notebook
@property
def path(self) -> Path | None:
"""Get the notebook path."""
return self._path
@property
def nb_config(self) -> NbParserConfig:
"""Get the notebook configuration."""
return self._nb_config
@property
def logger(self) -> LoggerType:
"""Get the logger."""
return self._logger
@property
def glue_data(self) -> dict[str, NotebookNode]:
"""Get the glue data."""
return self._glue_data
@property
def exec_metadata(self) -> ExecutionResult | None:
"""Get the execution metadata."""
return self._exec_metadata
@exec_metadata.setter
def exec_metadata(self, value: ExecutionResult):
"""Set the execution metadata."""
self._exec_metadata = value
def cell_line(self, cell_index: int) -> int:
"""Get the source line number of a cell."""
return self._source_map[cell_index]
@property
def nb_metadata(self) -> dict[str, Any]:
"""Get the notebook level metadata."""
return nb_node_to_dict(self.notebook.get("metadata", {}))
def nb_source_code_lexer(self) -> str | None:
"""Get the lexer for the notebook source code."""
metadata = self.notebook.get("metadata", {})
langinfo = metadata.get("language_info") or {}
lexer = langinfo.get("pygments_lexer") or langinfo.get("name", None)
if lexer is None:
lexer = (metadata.get("kernelspec") or {}).get("language", None)
return lexer
def code_cell_outputs(
self, cell_index: int
) -> tuple[int | None, list[NotebookNode]]:
"""Get the outputs of a cell.
:returns: a tuple of the execution_count and the outputs
:raises IndexError: if the cell index is out of range
"""
cells = self.notebook.get("cells", [])
cell = cells[cell_index]
return cell.get("execution_count", None), cell.get("outputs", [])
def eval_variable(self, name: str) -> list[NotebookNode]:
"""Retrieve the value of a variable from the kernel.
:param name: the name of the variable,
must match the regex `[a-zA-Z][a-zA-Z0-9_]*`
:returns: code cell outputs
:raises NotImplementedError: if the execution mode does not support this feature
:raises EvalNameError: if the variable name is invalid
"""
raise NotImplementedError