Coverage for C:\Repos\leo-editor\leo\plugins\importers\ipynb.py: 14%
184 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1#@+leo-ver=5-thin
2#@+node:ekr.20160412101008.1: * @file ../plugins/importers/ipynb.py
3"""The @auto importer for Jupyter (.ipynb) files."""
4import re
5from typing import List
6from leo.core import leoGlobals as g
7from leo.core.leoNodes import Position as Pos
8try:
9 import nbformat
10except ImportError:
11 nbformat = None
12#@+others
13#@+node:ekr.20211209081012.1: ** function: do_import
14def do_import(c, s, parent):
15 return Import_IPYNB(c.importCommands).run(s, parent)
16#@+node:ekr.20160412101537.2: ** class Import_IPYNB
17class Import_IPYNB:
18 """A class to import .ipynb files."""
20 def __init__(self, c=None, importCommands=None, **kwargs):
21 self.c = importCommands.c if importCommands else c
22 self.cell_n = 0 # The number of untitled cells.
23 self.parent = None # The parent for the next created node.
24 self.root = None # The root of the to-be-created outline.
26 #@+others
27 #@+node:ekr.20180408112531.1: *3* ipynb.Entries & helpers
28 #@+node:ekr.20160412101537.14: *4* ipynb.import_file
29 def import_file(self, fn, root):
30 """
31 Import the given .ipynb file.
32 https://nbformat.readthedocs.org/en/latest/format_description.html
33 """
34 # #1601:
35 if not nbformat:
36 g.es_print('import-jupyter-notebook requires nbformat package')
37 return
38 c = self.c
39 self.fn = fn
40 self.parent = None
41 self.root = root.copy()
42 d = self.parse(fn)
43 if not d:
44 return
45 self.do_prefix(d)
46 for cell in self.cells:
47 self.do_cell(cell)
48 self.indent_cells()
49 self.add_markup()
50 c.selectPosition(self.root)
51 c.redraw()
52 #@+node:ekr.20160412103110.1: *4* ipynb.run
53 def run(self, s, parent, parse_body=False):
54 """
55 @auto entry point. Called by code in leoImport.py.
56 """
57 # #1601:
58 if not nbformat:
59 g.es_print('import-jupyter-notebook requires nbformat package')
60 return
61 c = self.c
62 fn = parent.atAutoNodeName()
63 if c and fn:
64 self.import_file(fn, parent)
65 # Similar to Importer.run.
66 parent.b = (
67 '@nocolor-node\n\n' +
68 'Note: This node\'s body text is ignored when writing this file.\n\n' +
69 'The @others directive is not required\n'
70 )
71 for p in parent.self_and_subtree():
72 p.clearDirty()
73 # #1451: The caller should be responsible for this.
74 # if changed:
75 # c.setChanged()
76 # else:
77 # c.clearChanged()
78 elif not c or not fn:
79 g.trace('can not happen', c, fn)
80 #@+node:ekr.20160412101537.15: *4* ipynb.indent_cells & helper
81 re_header1 = re.compile(r'^.*<[hH]([123456])>(.*)</[hH]([123456])>')
82 re_header2 = re.compile(r'^\s*([#]+)')
84 def indent_cells(self):
85 """
86 Indent md nodes in self.root.children().
87 <h1> nodes and non-md nodes stay where they are,
88 <h2> nodes become children of <h1> nodes, etc.
90 Similarly for indentation based on '#' headline markup.
91 """
92 def to_int(n):
93 try:
94 return int(n)
95 except Exception:
96 return None
98 # Careful: links change during this loop.
99 p = self.root.firstChild()
100 stack: List[Pos] = []
101 after = self.root.nodeAfterTree()
102 root_level = self.root.level()
103 n = 1
104 while p and p != self.root and p != after:
105 # Check the first 5 lines of p.b.
106 lines = g.splitLines(p.b)
107 found = None
108 for i, s in enumerate(lines[:5]):
109 m1 = self.re_header1.search(s)
110 m2 = self.re_header2.search(s)
111 if m1:
112 n = to_int(m1.group(1))
113 if n is not None:
114 found = i
115 break
116 elif m2:
117 n = len(m2.group(1))
118 found = i
119 break
120 if found is None:
121 cell = self.get_ua(p, 'cell')
122 meta = cell.get('metadata')
123 n = meta and meta.get('leo_level')
124 n = to_int(n)
125 else:
126 p.b = ''.join(lines[:found] + lines[found + 1 :])
127 assert p.level() == root_level + 1, (p.level(), p.h)
128 stack = self.move_node(n, p, stack)
129 p.moveToNodeAfterTree()
130 #@+node:ekr.20160412101537.9: *4* ipynb.add_markup
131 def add_markup(self):
132 """Add @language directives, but only if necessary."""
133 for p in self.root.subtree():
134 level = p.level() - self.root.level()
135 language = g.getLanguageAtPosition(self.c, p)
136 cell = self.get_ua(p, 'cell')
137 # # Always put @language directives in top-level imported nodes.
138 if cell.get('cell_type') == 'markdown':
139 if level < 2 or language not in ('md', 'markdown'):
140 p.b = '@language md\n@wrap\n\n%s' % p.b
141 else:
142 if level < 2 or language != 'python':
143 p.b = '@language python\n\n%s' % p.b
144 #@+node:ekr.20180408112600.1: *3* ipynb.JSON handlers
145 #@+node:ekr.20160412101537.12: *4* ipynb.do_cell
146 def do_cell(self, cell):
148 if self.is_empty_code(cell):
149 return
150 self.parent = cell_p = self.root.insertAsLastChild()
151 # Expand the node if metadata: collapsed is False
152 meta = cell.get('metadata')
153 collapsed = meta and meta.get('collapsed')
154 h = meta.get('leo_headline')
155 if not h:
156 self.cell_n += 1
157 h = 'cell %s' % self.cell_n
158 self.parent.h = h
159 if collapsed is not None and not collapsed:
160 cell_p.v.expand()
161 # Handle the body text.
162 val = cell.get('source')
163 if val and val.strip():
164 # add_markup will add directives later.
165 cell_p.b = val.strip() + '\n'
166 del cell['source']
167 self.set_ua(cell_p, 'cell', cell)
168 #@+node:ekr.20160412101537.13: *4* ipynb.do_prefix
169 def do_prefix(self, d):
170 """Handle everything except the 'cells' attribute."""
171 if d:
172 # Expand the root if requested.
173 if 1: # The @auto logic defeats this, but this is correct.
174 meta = d.get('metadata')
175 collapsed = meta and meta.get('collapsed')
176 if collapsed is not None and not collapsed:
177 self.root.v.expand()
178 self.cells = d.get('cells', [])
179 if self.cells:
180 del d['cells']
181 self.set_ua(self.root, 'prefix', d)
182 #@+node:ekr.20160412101537.22: *4* ipynb.is_empty_code
183 def is_empty_code(self, cell):
184 """Return True if cell is an empty code cell."""
185 if cell.get('cell_type') != 'code':
186 return False
187 metadata = cell.get('metadata')
188 outputs = cell.get('outputs')
189 source = cell.get('source')
190 keys = sorted(metadata.keys())
191 if 'collapsed' in metadata:
192 keys.remove('collapsed')
193 return not source and not keys and not outputs
194 #@+node:ekr.20160412101537.24: *4* ipynb.parse
195 nb_warning_given = False
197 def parse(self, fn):
198 """Parse the file, which should be JSON format."""
199 if not nbformat:
200 if not self.nb_warning_given:
201 self.nb_warning_given = True
202 g.es_print('@auto for .ipynb files requires the nbformat package', color='red')
203 return None
204 if g.os_path_exists(fn):
205 with open(fn) as f:
206 # payload_source = f.name
207 payload = f.read()
208 try:
209 nb = nbformat.reads(payload, as_version=4)
210 return nb
211 except Exception:
212 g.es_exception()
213 return None
214 else:
215 g.es_print('not found', fn)
216 return None
217 #@+node:ekr.20180408112636.1: *3* ipynb.Utils
218 #@+node:ekr.20160412101845.24: *4* ipynb.get_file_name
219 def get_file_name(self):
220 """Open a dialog to write a Jupyter (.ipynb) file."""
221 c = self.c
222 fn = g.app.gui.runOpenFileDialog(
223 c,
224 defaultextension=".ipynb",
225 filetypes=[
226 ("Jupyter notebooks", "*.ipynb"),
227 ("All files", "*"),
228 ],
229 title="Import Jupyter Notebook",
230 )
231 c.bringToFront()
232 return fn
233 #@+node:ekr.20180409152738.1: *4* ipynb.get_ua
234 def get_ua(self, p, key=None):
235 """Return the ipynb uA. If key is given, return the inner dict."""
236 d = p.v.u.get('ipynb')
237 if not d:
238 return {}
239 if key:
240 return d.get(key)
241 return d
242 #@+node:ekr.20160412101537.16: *4* ipynb.move_node
243 def move_node(self, n, p, stack):
244 """Move node to level n"""
245 # Cut back the stack so that p will be at level n (if possible).
246 if n is None:
247 n = 1
248 if stack:
249 stack = stack[:n]
250 if len(stack) == n:
251 prev = stack.pop()
252 p.moveAfter(prev)
253 else:
254 # p will be under-indented if len(stack) < n-1
255 # This depends on user markup, so it can't be helped.
256 parent = stack[-1]
257 n2 = parent.numberOfChildren()
258 p.moveToNthChildOf(parent, n2)
259 # Push p *after* moving p.
260 stack.append(p.copy())
261 return stack
262 #@+node:ekr.20180407175655.1: *4* ipynb.set_ua
263 def set_ua(self, p, key, val):
264 """Set p.v.u"""
265 d = p.v.u
266 d2 = d.get('ipynb') or {}
267 d2[key] = val
268 d['ipynb'] = d2
269 p.v.u = d
270 #@-others
271#@-others
272importer_dict = {
273 '@auto': [], # '@auto-jupyter', '@auto-ipynb',],
274 'class': Import_IPYNB,
275 'func': do_import,
276 'extensions': ['.ipynb',],
277}
278#@@language python
279#@@tabwidth -4
280#@-leo