Hide keyboard shortcuts

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.""" 

19 

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. 

29 

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*([#]+)') 

87 

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. 

93 

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 

101 

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): 

151 

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 

200 

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