Coverage for C:\leo.repo\leo-editor\leo\plugins\importers\ipynb.py : 14%

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