jupynium 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jupynium/__init__.py +8 -0
- jupynium/__main__.py +6 -0
- jupynium/_version.py +12 -0
- jupynium/buffer.py +491 -0
- jupynium/cmds/__init__.py +0 -0
- jupynium/cmds/ipynb2jupytext.py +76 -0
- jupynium/cmds/jupynium.py +640 -0
- jupynium/definitions.py +9 -0
- jupynium/events_control.py +997 -0
- jupynium/ipynb.py +78 -0
- jupynium/js/get_cell_inputs.js +8 -0
- jupynium/js/kernel_complete.js +21 -0
- jupynium/js/kernel_inspect.js +20 -0
- jupynium/js/set_cell_text.js +11 -0
- jupynium/js/update_cell_selection.js +26 -0
- jupynium/jupyter_notebook_selenium.py +33 -0
- jupynium/lua/autocmd_vimleave.lua +13 -0
- jupynium/lua/cmp.lua +63 -0
- jupynium/lua/commands.lua +28 -0
- jupynium/lua/defaults.lua +46 -0
- jupynium/lua/helpers.lua +738 -0
- jupynium/lua/notify.lua +62 -0
- jupynium/nvim.py +74 -0
- jupynium/process.py +29 -0
- jupynium/pynvim_helpers.py +74 -0
- jupynium/rpc_messages.py +122 -0
- jupynium/selenium_helpers.py +89 -0
- jupynium-0.2.6.dist-info/METADATA +654 -0
- jupynium-0.2.6.dist-info/RECORD +32 -0
- jupynium-0.2.6.dist-info/WHEEL +4 -0
- jupynium-0.2.6.dist-info/entry_points.txt +3 -0
- jupynium-0.2.6.dist-info/licenses/LICENSE +22 -0
jupynium/__init__.py
ADDED
jupynium/__main__.py
ADDED
jupynium/_version.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# THIS "versionfile" IS GENERATED BY version-pioneer-0.0.13
|
|
3
|
+
# by evaluating the original versionscript and storing the computed versions as a constant.
|
|
4
|
+
|
|
5
|
+
def get_version_dict():
|
|
6
|
+
return {'version': '0.2.6', 'full_revisionid': 'f8f6ee57b83395f73abd831bde28b3206ec09d97', 'dirty': False, 'error': None, 'date': '2025-02-18T00:28:20+0000'}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
print(json.dumps(get_version_dict()))
|
jupynium/buffer.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from importlib.resources import files as resfiles
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .jupyter_notebook_selenium import insert_cell_at
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
set_cell_text_js_code = (resfiles("jupynium") / "js" / "set_cell_text.js").read_text()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JupyniumBuffer:
|
|
19
|
+
"""
|
|
20
|
+
Deal with the Nvim buffer and its cell information.
|
|
21
|
+
|
|
22
|
+
This does have a functionality to sync with the Notebook.
|
|
23
|
+
It can also be part of the buffer, and anything above the first cell will be `header_cell_type`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
buf: list[str] | None = None,
|
|
29
|
+
header_cell_type: str = "header",
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
self.buf is a list of lines of the nvim buffer.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
header_cell_type: Use only when partial update.
|
|
36
|
+
"""
|
|
37
|
+
if buf is None:
|
|
38
|
+
# each cell's row length. 0-th cell is not a cell, but it's the header.
|
|
39
|
+
# You can put anything above and it won't be synced to Jupyter Notebook.
|
|
40
|
+
self.buf = [""]
|
|
41
|
+
self.num_rows_per_cell: list[int] = [1]
|
|
42
|
+
|
|
43
|
+
self.cell_types = ["header"] # 0-th cell is not a cell.
|
|
44
|
+
else:
|
|
45
|
+
self.buf = buf
|
|
46
|
+
self.full_analyse_buf(header_cell_type)
|
|
47
|
+
|
|
48
|
+
def full_analyse_buf(self, header_cell_type: str = "header"):
|
|
49
|
+
"""
|
|
50
|
+
Main parser for the jupynium format (*.ju.*).
|
|
51
|
+
|
|
52
|
+
This function needs to support partial update.
|
|
53
|
+
|
|
54
|
+
E.g. by looking at 1 line of change, it should be able to understand if:
|
|
55
|
+
- the change is within a cell
|
|
56
|
+
- cell creation/deletion
|
|
57
|
+
- cell type change
|
|
58
|
+
|
|
59
|
+
During the partial update, the header cell will be continuation from
|
|
60
|
+
the existing cell.
|
|
61
|
+
We don't know if it will be header/cell/markdown.
|
|
62
|
+
So we need to pass the header_cell_type.
|
|
63
|
+
(This is deprecated, in favour of self.get_cells_text() dealing with
|
|
64
|
+
content processing.)
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
header_cell_type: Used to be used only when partial update.
|
|
68
|
+
Now deprecated.
|
|
69
|
+
"""
|
|
70
|
+
num_rows_this_cell = 0
|
|
71
|
+
num_rows_per_cell = []
|
|
72
|
+
cell_types = [header_cell_type]
|
|
73
|
+
for _row, line in enumerate(self.buf):
|
|
74
|
+
if line.startswith(("# %% [md]", "# %% [markdown]")):
|
|
75
|
+
num_rows_per_cell.append(num_rows_this_cell)
|
|
76
|
+
num_rows_this_cell = 1
|
|
77
|
+
cell_types.append("markdown")
|
|
78
|
+
# Treat '# %% Cell Title' as a code cell
|
|
79
|
+
# But not magic commands such as '# %%timeit'.
|
|
80
|
+
# See: https://github.com/kiyoon/jupynium.nvim/pull/127
|
|
81
|
+
elif line[:5].strip() == "# %%":
|
|
82
|
+
num_rows_per_cell.append(num_rows_this_cell)
|
|
83
|
+
num_rows_this_cell = 1
|
|
84
|
+
cell_types.append("code")
|
|
85
|
+
else:
|
|
86
|
+
num_rows_this_cell += 1
|
|
87
|
+
num_rows_per_cell.append(num_rows_this_cell)
|
|
88
|
+
|
|
89
|
+
self.num_rows_per_cell = num_rows_per_cell
|
|
90
|
+
self.cell_types = cell_types
|
|
91
|
+
|
|
92
|
+
def _process_cell_text(self, cell_type: str, lines: list[str]):
|
|
93
|
+
"""
|
|
94
|
+
Assuming that lines is just one cell's content, process it.
|
|
95
|
+
"""
|
|
96
|
+
if cell_type == "code":
|
|
97
|
+
return "\n".join(
|
|
98
|
+
line[2:] if line.startswith("# %") else line for line in lines
|
|
99
|
+
)
|
|
100
|
+
elif cell_type == "markdown":
|
|
101
|
+
if len(lines) > 0 and lines[0] == '"""':
|
|
102
|
+
return "\n".join(line for line in lines if not line.startswith('"""'))
|
|
103
|
+
else:
|
|
104
|
+
return "\n".join(line.removeprefix("# ") for line in lines)
|
|
105
|
+
else:
|
|
106
|
+
# header
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
def get_cells_text(
|
|
110
|
+
self, start_cell_idx: int, end_cell_idx: int, *, strip: bool = True
|
|
111
|
+
) -> list[str]:
|
|
112
|
+
"""
|
|
113
|
+
Get processed cell text.
|
|
114
|
+
|
|
115
|
+
In a code cell, remove comments for the magic commands.
|
|
116
|
+
e.g. '# %time' -> '%time'
|
|
117
|
+
In a markdown cell, remove the leading # from the lines or multiline string.
|
|
118
|
+
e.g. '# # Markdown header' -> '# Markdown header'
|
|
119
|
+
"""
|
|
120
|
+
if start_cell_idx == 0:
|
|
121
|
+
start_row_offset = 0
|
|
122
|
+
else:
|
|
123
|
+
start_row_offset = 1
|
|
124
|
+
|
|
125
|
+
texts_per_cell = []
|
|
126
|
+
start_row = self.get_cell_start_row(start_cell_idx)
|
|
127
|
+
texts_per_cell.append(
|
|
128
|
+
self._process_cell_text(
|
|
129
|
+
self.cell_types[start_cell_idx],
|
|
130
|
+
self.buf[
|
|
131
|
+
start_row + start_row_offset : start_row
|
|
132
|
+
+ self.num_rows_per_cell[start_cell_idx]
|
|
133
|
+
],
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for cell_idx in range(start_cell_idx + 1, end_cell_idx + 1):
|
|
138
|
+
start_row += self.num_rows_per_cell[cell_idx - 1]
|
|
139
|
+
texts_per_cell.append(
|
|
140
|
+
self._process_cell_text(
|
|
141
|
+
self.cell_types[cell_idx],
|
|
142
|
+
self.buf[
|
|
143
|
+
start_row + 1 : start_row + self.num_rows_per_cell[cell_idx]
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if strip:
|
|
149
|
+
texts_per_cell = [x.strip() for x in texts_per_cell]
|
|
150
|
+
|
|
151
|
+
return texts_per_cell
|
|
152
|
+
|
|
153
|
+
def get_cell_text(self, cell_idx: int, *, strip: bool = True) -> str:
|
|
154
|
+
return self.get_cells_text(cell_idx, cell_idx, strip=strip)[0]
|
|
155
|
+
|
|
156
|
+
def process_on_lines(
|
|
157
|
+
self,
|
|
158
|
+
driver: WebDriver,
|
|
159
|
+
*,
|
|
160
|
+
strip: bool,
|
|
161
|
+
lines: list[str],
|
|
162
|
+
start_row: int,
|
|
163
|
+
old_end_row: int,
|
|
164
|
+
new_end_row: int,
|
|
165
|
+
):
|
|
166
|
+
(
|
|
167
|
+
notebook_cell_operations,
|
|
168
|
+
modified_cell_idx_start,
|
|
169
|
+
modified_cell_idx_end,
|
|
170
|
+
) = self._on_lines_update_buf(lines, start_row, old_end_row, new_end_row)
|
|
171
|
+
self._apply_cell_operations(driver, notebook_cell_operations)
|
|
172
|
+
|
|
173
|
+
num_cells = self.num_cells_in_notebook
|
|
174
|
+
num_cells_in_notebook = driver.execute_script(
|
|
175
|
+
"return Jupyter.notebook.ncells();"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if num_cells_in_notebook != num_cells:
|
|
179
|
+
self.full_sync_to_notebook(driver, strip=strip)
|
|
180
|
+
else:
|
|
181
|
+
self._partial_sync_to_notebook(
|
|
182
|
+
driver, modified_cell_idx_start, modified_cell_idx_end, strip=strip
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _on_lines_update_buf(
|
|
186
|
+
self, lines: list[str], start_row: int, old_end_row: int, new_end_row: int
|
|
187
|
+
):
|
|
188
|
+
"""Replace start_row:old_end_row to lines from self.buf."""
|
|
189
|
+
# Analyse how many cells are removed
|
|
190
|
+
notebook_cell_delete_operations = []
|
|
191
|
+
notebook_cell_operations = []
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
cell_idx, _, row_within_cell = self.get_cell_index_from_row(start_row)
|
|
195
|
+
|
|
196
|
+
if row_within_cell == 0 and cell_idx > 0:
|
|
197
|
+
# If the row is the first row of a cell, and it's not the first cell,
|
|
198
|
+
# then it's a cell separator.
|
|
199
|
+
row_within_cell = self.num_rows_per_cell[cell_idx - 1]
|
|
200
|
+
cell_idx -= 1
|
|
201
|
+
except IndexError:
|
|
202
|
+
assert start_row == old_end_row == self.num_rows
|
|
203
|
+
cell_idx = self.num_cells - 1
|
|
204
|
+
row_within_cell = self.num_rows_per_cell[-1]
|
|
205
|
+
|
|
206
|
+
modified_cell_idx_start = cell_idx
|
|
207
|
+
|
|
208
|
+
lines_to_remove = old_end_row - start_row
|
|
209
|
+
|
|
210
|
+
while lines_to_remove > 0:
|
|
211
|
+
if row_within_cell < self.num_rows_per_cell[cell_idx]:
|
|
212
|
+
# If the row is within the cell, then it's a cell content.
|
|
213
|
+
self.num_rows_per_cell[cell_idx] -= 1
|
|
214
|
+
else:
|
|
215
|
+
# If the row is not within the cell, then it's a cell separator.
|
|
216
|
+
notebook_cell_delete_operations.append(("delete", cell_idx + 1, None))
|
|
217
|
+
self.num_rows_per_cell[cell_idx] += (
|
|
218
|
+
self.num_rows_per_cell[cell_idx + 1] - 1
|
|
219
|
+
)
|
|
220
|
+
del self.num_rows_per_cell[cell_idx + 1]
|
|
221
|
+
del self.cell_types[cell_idx + 1]
|
|
222
|
+
lines_to_remove -= 1
|
|
223
|
+
|
|
224
|
+
# Analyse how many cells are added
|
|
225
|
+
new_lines_buf = JupyniumBuffer(
|
|
226
|
+
lines,
|
|
227
|
+
header_cell_type=self.cell_types[
|
|
228
|
+
cell_idx
|
|
229
|
+
], # This is required as we're analysing partially.
|
|
230
|
+
)
|
|
231
|
+
if new_lines_buf.num_cells - 1 == 0:
|
|
232
|
+
self.num_rows_per_cell[cell_idx] += new_lines_buf.num_rows_per_cell[0]
|
|
233
|
+
notebook_cell_operations = notebook_cell_delete_operations
|
|
234
|
+
else:
|
|
235
|
+
num_delete_cells = len(notebook_cell_delete_operations)
|
|
236
|
+
num_insert_cells = new_lines_buf.num_cells - 1
|
|
237
|
+
if num_delete_cells > num_insert_cells:
|
|
238
|
+
notebook_cell_operations = notebook_cell_delete_operations[
|
|
239
|
+
:num_insert_cells
|
|
240
|
+
]
|
|
241
|
+
elif num_delete_cells < num_insert_cells:
|
|
242
|
+
notebook_cell_operations = [
|
|
243
|
+
(
|
|
244
|
+
"cell_type",
|
|
245
|
+
cell_idx + 1,
|
|
246
|
+
new_lines_buf.cell_types[
|
|
247
|
+
1 : 1 + len(notebook_cell_delete_operations)
|
|
248
|
+
],
|
|
249
|
+
)
|
|
250
|
+
]
|
|
251
|
+
notebook_cell_operations.append(
|
|
252
|
+
(
|
|
253
|
+
"insert",
|
|
254
|
+
cell_idx + 1,
|
|
255
|
+
new_lines_buf.cell_types[
|
|
256
|
+
1 + len(notebook_cell_delete_operations) :
|
|
257
|
+
],
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
notebook_cell_operations = [
|
|
262
|
+
(
|
|
263
|
+
"cell_type",
|
|
264
|
+
cell_idx + 1,
|
|
265
|
+
new_lines_buf.cell_types[1:],
|
|
266
|
+
)
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
num_tail_rows = self.num_rows_per_cell[cell_idx] - row_within_cell
|
|
270
|
+
self.num_rows_per_cell[cell_idx] = (
|
|
271
|
+
row_within_cell + new_lines_buf.num_rows_per_cell[0]
|
|
272
|
+
)
|
|
273
|
+
new_lines_buf.num_rows_per_cell[-1] += num_tail_rows
|
|
274
|
+
self.num_rows_per_cell[cell_idx + 1 : cell_idx + 1] = (
|
|
275
|
+
new_lines_buf.num_rows_per_cell[1:]
|
|
276
|
+
)
|
|
277
|
+
self.cell_types[cell_idx + 1 : cell_idx + 1] = new_lines_buf.cell_types[1:]
|
|
278
|
+
|
|
279
|
+
modified_cell_idx_end = modified_cell_idx_start + new_lines_buf.num_cells - 1
|
|
280
|
+
|
|
281
|
+
# Now actually replace the lines
|
|
282
|
+
# Optimisation: if the number of lines is not changed,
|
|
283
|
+
# which is most of the cases,
|
|
284
|
+
# then we can just replace the the strings in the list
|
|
285
|
+
# instead of modifying list itself.
|
|
286
|
+
if old_end_row == new_end_row:
|
|
287
|
+
for i, line in enumerate(lines):
|
|
288
|
+
self.buf[start_row + i] = line
|
|
289
|
+
else:
|
|
290
|
+
# If the number of lines is changed,
|
|
291
|
+
# then we need to remove the old lines and insert the new lines.
|
|
292
|
+
self.buf[start_row:old_end_row] = lines
|
|
293
|
+
|
|
294
|
+
return notebook_cell_operations, modified_cell_idx_start, modified_cell_idx_end
|
|
295
|
+
|
|
296
|
+
def _apply_cell_operations(
|
|
297
|
+
self,
|
|
298
|
+
driver: WebDriver,
|
|
299
|
+
notebook_cell_operations: list[tuple[str, int, list[str]]],
|
|
300
|
+
):
|
|
301
|
+
# Remove / create cells in Notebook
|
|
302
|
+
for operation, cell_idx, cell_types in notebook_cell_operations:
|
|
303
|
+
nb_cell_idx = cell_idx - 1
|
|
304
|
+
if operation == "delete":
|
|
305
|
+
logger.info(f"Deleting cell {nb_cell_idx} from Notebook")
|
|
306
|
+
driver.execute_script(
|
|
307
|
+
"Jupyter.notebook.delete_cell(arguments[0]);", nb_cell_idx
|
|
308
|
+
)
|
|
309
|
+
elif operation == "insert":
|
|
310
|
+
for i, cell_type in enumerate(cell_types):
|
|
311
|
+
logger.info(f"Inserting cell {nb_cell_idx + i} from Notebook")
|
|
312
|
+
insert_cell_at(driver, cell_type, nb_cell_idx + i)
|
|
313
|
+
elif operation == "cell_type":
|
|
314
|
+
for i, cell_type in enumerate(cell_types):
|
|
315
|
+
logger.info(
|
|
316
|
+
f"Cell {nb_cell_idx + i} type change to {cell_type} "
|
|
317
|
+
"from Notebook"
|
|
318
|
+
)
|
|
319
|
+
# "markdown"
|
|
320
|
+
if cell_type == "markdown":
|
|
321
|
+
driver.execute_script(
|
|
322
|
+
"Jupyter.notebook.cells_to_markdown([arguments[0]]);",
|
|
323
|
+
nb_cell_idx + i,
|
|
324
|
+
)
|
|
325
|
+
elif cell_type == "code":
|
|
326
|
+
driver.execute_script(
|
|
327
|
+
"Jupyter.notebook.cells_to_code([arguments[0]]);",
|
|
328
|
+
nb_cell_idx + i,
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
raise ValueError(f"Unknown cell type {cell_type}")
|
|
332
|
+
|
|
333
|
+
def get_cell_start_row(self, cell_idx: int):
|
|
334
|
+
return sum(self.num_rows_per_cell[:cell_idx])
|
|
335
|
+
|
|
336
|
+
def get_cell_index_from_row(
|
|
337
|
+
self,
|
|
338
|
+
row: int,
|
|
339
|
+
num_rows_per_cell: list[int] | None = None,
|
|
340
|
+
*,
|
|
341
|
+
raise_out_of_bound: bool = True,
|
|
342
|
+
) -> tuple[int, int, int]:
|
|
343
|
+
"""
|
|
344
|
+
Returns the cell index for the given row.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
row: row index
|
|
348
|
+
num_rows_per_cell: number of rows per cell. If None, use self.num_rows_per_cell
|
|
349
|
+
raise_out_of_bound: whether to raise an IndexError if the row is out of bound
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
int: cell index
|
|
353
|
+
int: cell start row
|
|
354
|
+
int: row index within the cell
|
|
355
|
+
"""
|
|
356
|
+
if num_rows_per_cell is None:
|
|
357
|
+
num_rows_per_cell = self.num_rows_per_cell
|
|
358
|
+
|
|
359
|
+
cell_start_row = 0
|
|
360
|
+
i = 0
|
|
361
|
+
for i, num_rows in enumerate(num_rows_per_cell):
|
|
362
|
+
if cell_start_row + num_rows > row:
|
|
363
|
+
return i, cell_start_row, row - cell_start_row
|
|
364
|
+
cell_start_row += num_rows
|
|
365
|
+
|
|
366
|
+
# Out of bound. Could be adding a new line.
|
|
367
|
+
if raise_out_of_bound:
|
|
368
|
+
raise IndexError(f"Could not find cell for row {row}")
|
|
369
|
+
else:
|
|
370
|
+
return i, cell_start_row, row - cell_start_row
|
|
371
|
+
|
|
372
|
+
def _check_validity(self):
|
|
373
|
+
assert len(self.buf) == sum(self.num_rows_per_cell)
|
|
374
|
+
assert len(self.cell_types) == len(self.num_rows_per_cell)
|
|
375
|
+
assert self.cell_types[0] == "header"
|
|
376
|
+
assert all(x in ("code", "markdown") for x in self.cell_types[1:])
|
|
377
|
+
|
|
378
|
+
def _partial_sync_to_notebook(
|
|
379
|
+
self, driver: WebDriver, start_cell_idx: int, end_cell_idx: int, *, strip=True
|
|
380
|
+
):
|
|
381
|
+
"""
|
|
382
|
+
Given the range of cells to update, sync the JupyniumBuffer with the notebook.
|
|
383
|
+
|
|
384
|
+
Note:
|
|
385
|
+
Cell 1 in JupyniumBuffer is cell 0 in Notebook.
|
|
386
|
+
Args are inclusive range in ju.py JupyniumBuffer
|
|
387
|
+
"""
|
|
388
|
+
assert start_cell_idx <= end_cell_idx < self.num_cells
|
|
389
|
+
|
|
390
|
+
if self.num_cells == 1:
|
|
391
|
+
# Markdown file
|
|
392
|
+
driver.execute_script(
|
|
393
|
+
"Jupyter.notebook.cells_to_markdown([0]);"
|
|
394
|
+
"Jupyter.notebook.get_cell(0).set_text(arguments[0]);"
|
|
395
|
+
"Jupyter.notebook.get_cell(0).render()",
|
|
396
|
+
"\n".join(self.buf),
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
# Notebook file
|
|
400
|
+
|
|
401
|
+
if end_cell_idx == 0:
|
|
402
|
+
# Nothing to update
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
if start_cell_idx == 0:
|
|
406
|
+
start_cell_idx = 1
|
|
407
|
+
|
|
408
|
+
texts_per_cell = self.get_cells_text(
|
|
409
|
+
start_cell_idx, end_cell_idx, strip=strip
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
code_cell_indices = [
|
|
413
|
+
start_cell_idx + i
|
|
414
|
+
for i, cell_type in enumerate(
|
|
415
|
+
self.cell_types[start_cell_idx : end_cell_idx + 1]
|
|
416
|
+
)
|
|
417
|
+
if cell_type == "code"
|
|
418
|
+
]
|
|
419
|
+
markdown_cell_indices = [
|
|
420
|
+
start_cell_idx + i
|
|
421
|
+
for i, cell_type in enumerate(
|
|
422
|
+
self.cell_types[start_cell_idx : end_cell_idx + 1]
|
|
423
|
+
)
|
|
424
|
+
if cell_type == "markdown"
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
if len(code_cell_indices) > 0:
|
|
428
|
+
logger.info(f"Converting to code cells: {code_cell_indices}")
|
|
429
|
+
for i in code_cell_indices:
|
|
430
|
+
driver.execute_script(
|
|
431
|
+
"Jupyter.notebook.cells_to_code([arguments[0]]);", i - 1
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if len(markdown_cell_indices) > 0:
|
|
435
|
+
logger.info(f"Converting to markdown cells: {markdown_cell_indices}")
|
|
436
|
+
for i in markdown_cell_indices:
|
|
437
|
+
driver.execute_script(
|
|
438
|
+
"Jupyter.notebook.cells_to_markdown([arguments[0]]);", i - 1
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# This will render markdown cells
|
|
442
|
+
driver.execute_script(
|
|
443
|
+
set_cell_text_js_code,
|
|
444
|
+
start_cell_idx - 1,
|
|
445
|
+
end_cell_idx - 1,
|
|
446
|
+
*texts_per_cell,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def full_sync_to_notebook(self, driver: WebDriver, *, strip: bool = True):
|
|
450
|
+
# Full sync with notebook.
|
|
451
|
+
# WARNING: syncing may result in data loss.
|
|
452
|
+
num_cells = self.num_cells_in_notebook
|
|
453
|
+
num_cells_in_notebook = driver.execute_script(
|
|
454
|
+
"return Jupyter.notebook.ncells();"
|
|
455
|
+
)
|
|
456
|
+
if num_cells > num_cells_in_notebook:
|
|
457
|
+
for _ in range(num_cells - num_cells_in_notebook):
|
|
458
|
+
driver.execute_script("Jupyter.notebook.insert_cell_below();")
|
|
459
|
+
elif num_cells < num_cells_in_notebook:
|
|
460
|
+
for _ in range(num_cells_in_notebook - num_cells):
|
|
461
|
+
driver.execute_script("Jupyter.notebook.delete_cell(-1);")
|
|
462
|
+
|
|
463
|
+
self._partial_sync_to_notebook(driver, 0, self.num_cells - 1, strip=strip)
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def num_cells(self):
|
|
467
|
+
return len(self.num_rows_per_cell)
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def num_cells_in_notebook(self):
|
|
471
|
+
"""
|
|
472
|
+
Get the number of cells in the notebook.
|
|
473
|
+
|
|
474
|
+
If the buffer has 1 cell (no separator), it will be treated as markdown file.
|
|
475
|
+
If the buffer has more than 1 cell, it will be treated as notebook.
|
|
476
|
+
|
|
477
|
+
Notebook always has 1 cell minimum.
|
|
478
|
+
"""
|
|
479
|
+
return max(self.num_cells - 1, 1)
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def num_rows(self):
|
|
483
|
+
return len(self.buf)
|
|
484
|
+
|
|
485
|
+
def __eq__(self, other: object) -> bool:
|
|
486
|
+
assert isinstance(other, JupyniumBuffer)
|
|
487
|
+
return (
|
|
488
|
+
self.buf == other.buf
|
|
489
|
+
and self.num_rows_per_cell == other.num_rows_per_cell
|
|
490
|
+
and self.cell_types == other.cell_types
|
|
491
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/use/bin/env python3
|
|
2
|
+
# ruff: noqa: T201
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from jupynium.ipynb import ipynb2jupytext, load_ipynb
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_parser():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description="Convert ipynb to a jupytext percent format (.ju.py).",
|
|
14
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("ipynb_path", help="Path to ipynb file")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"output_jupy_path",
|
|
19
|
+
nargs="?",
|
|
20
|
+
help="Path to output jupynium file. "
|
|
21
|
+
"If not specified, use file name of ipynb file or print to stdout (--stdout)",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"-y", "--yes", action="store_true", help="Do not ask for confirmation"
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-c", "--code_only", action="store_true", help="Only convert code cells"
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument("-s", "--stdout", action="store_true", help="Print to stdout")
|
|
30
|
+
return parser
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_args(args, parser):
|
|
34
|
+
if args.stdout and args.yes:
|
|
35
|
+
parser.error("Either one of --stdout or --yes can be specified")
|
|
36
|
+
|
|
37
|
+
if args.output_jupy_path is not None and args.stdout:
|
|
38
|
+
parser.error("Either one of --stdout or output_jupy_path can be specified")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
parser = get_parser()
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
check_args(args, parser)
|
|
45
|
+
|
|
46
|
+
ipynb = load_ipynb(args.ipynb_path)
|
|
47
|
+
jupy = ipynb2jupytext(ipynb, code_only=args.code_only)
|
|
48
|
+
|
|
49
|
+
if args.stdout:
|
|
50
|
+
for line in jupy:
|
|
51
|
+
print(line)
|
|
52
|
+
else:
|
|
53
|
+
if args.output_jupy_path is None:
|
|
54
|
+
output_jupy_path = Path(args.ipynb_path).with_suffix(".ju.py")
|
|
55
|
+
else:
|
|
56
|
+
output_jupy_path = Path(args.output_jupy_path)
|
|
57
|
+
|
|
58
|
+
output_jupy_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
if output_jupy_path.is_file() and not args.yes:
|
|
61
|
+
print(f"Do you want to overwrite {output_jupy_path}?")
|
|
62
|
+
answer = input("y/n: ")
|
|
63
|
+
if answer != "y":
|
|
64
|
+
print("Aborted")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
with open(output_jupy_path, "w") as f:
|
|
68
|
+
for line in jupy:
|
|
69
|
+
f.write(line)
|
|
70
|
+
f.write("\n")
|
|
71
|
+
|
|
72
|
+
print(f'Converted "{args.ipynb_path}" to "{output_jupy_path}"')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|