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

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

25 

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

83 

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. 

89 

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 

97 

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

147 

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 

196 

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