Coverage for C:\Repos\leo-editor\leo\plugins\writers\ipynb.py: 19%
124 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.20160412101901.1: * @file ../plugins/writers/ipynb.py
3"""The @auto write code for jupyter (.ipynb) files."""
4import json
5import re
6import sys
7from leo.core import leoGlobals as g
8import leo.plugins.writers.basewriter as basewriter
10#@+others
11#@+node:ekr.20160412101845.2: ** class Export_IPYNB
12class Export_IPYNB(basewriter.BaseWriter):
13 """A class to export outlines to .ipynb files."""
15 def __init__(self, c):
16 """Ctor for Import_IPYNB class."""
17 super().__init__(c)
18 self.c = c # Commander of present outline.
19 self.root = None # The root of the outline.
21 #@+others
22 #@+node:ekr.20160412114852.1: *3* ipy_w.Entries
23 #@+node:ekr.20160412101845.4: *4* ipy_w.export_outline
24 def export_outline(self, root, fn=None):
25 """
26 Entry point for export-jupyter-notebook
27 Export the given .ipynb file.
28 """
29 self.root = root
30 if not fn:
31 fn = self.get_file_name()
32 if not fn:
33 return False
34 try:
35 nb = self.make_notebook()
36 s = self.convert_notebook(nb)
37 except Exception:
38 g.es_exception()
39 return False
40 if not s:
41 return False
42 s = g.toEncodedString(s, encoding='utf-8', reportErrors=True)
43 try:
44 with open(fn, 'wb') as f:
45 f.write(s)
46 g.es_print('wrote: %s' % fn)
47 except IOError:
48 g.es_print('can not open: %s' % fn)
49 return True
50 #@+node:ekr.20160412114239.1: *4* ipy_w.write: @auto entry
51 def write(self, root):
52 """
53 Export_IPYNB: entry point for @auto writes.
54 Signature must match signature of BaseWriter.write().
55 """
56 if not root:
57 g.trace('can not happen: no root')
58 return False
59 self.root = root
60 try:
61 nb = self.make_notebook()
62 s = self.convert_notebook(nb)
63 except Exception:
64 g.es_exception()
65 return False
66 if not s:
67 return False
68 self.put(g.toUnicode(s))
69 return True
70 #@+node:ekr.20180409081735.1: *3* ipy_w.cell_type
71 def cell_type(self, p):
72 """Return the Jupyter cell type of p.b, honoring ancestor directives."""
73 c = self.c
74 language = g.getLanguageAtPosition(c, p)
75 return 'code' if language == 'python' else 'markdown'
76 #@+node:ekr.20180410045324.1: *3* ipy_w.clean_headline
77 def clean_headline(self, s):
78 """
79 Return a cleaned version of a headline.
81 Used to clean section names and the [ ] part of markdown links.
82 """
83 aList = [ch for ch in s if ch in '-: ' or ch.isalnum()]
84 return ''.join(aList).rstrip('-').strip()
85 #@+node:ekr.20180407191227.1: *3* ipy_w.convert_notebook
86 def convert_notebook(self, nb):
87 """Convert the notebook to a string."""
88 # Do *not* catch exceptions here.
89 s = json.dumps(nb,
90 sort_keys=True,
91 indent=4, separators=(',', ': '))
92 return g.toUnicode(s)
93 #@+node:ekr.20160412101845.21: *3* ipy_w.default_metadata
94 def default_metadata(self):
95 """Return the default top-level metadata."""
96 n1, n2 = sys.version_info[0], sys.version_info[1]
97 version = n1
98 long_version = '%s.%s' % (n1, n2)
99 return {
100 "metadata": {
101 "kernelspec": {
102 "display_name": "Python %s" % version,
103 "language": "python",
104 "name": "python%s" % version,
105 },
106 "language_info": {
107 "codemirror_mode": {
108 "name": "ipython",
109 "version": version,
110 },
111 "file_extension": ".py",
112 "mimetype": "text/x-python",
113 "name": "python",
114 "nbconvert_exporter": "python",
115 "pygments_lexer": "ipython3",
116 "version": long_version,
117 }
118 },
119 "nbformat": 4,
120 "nbformat_minor": 0
121 }
122 #@+node:ekr.20180408103729.1: *3* ipy_w.get_file_name
123 def get_file_name(self):
124 """Open a dialog to write a Jupyter (.ipynb) file."""
125 c = self.c
126 fn = g.app.gui.runSaveFileDialog(
127 c,
128 defaultextension=".ipynb",
129 filetypes=[
130 ("Jupyter files", "*.ipynb"),
131 ("All files", "*"),
132 ],
133 title="Export To Jupyter File",
134 )
135 c.bringToFront()
136 return fn
137 #@+node:ekr.20180407193222.1: *3* ipy_w.get_ua
138 def get_ua(self, p, key=None):
139 """Return the ipynb uA. If key is given, return the inner dict."""
140 d = p.v.u.get('ipynb')
141 if not d:
142 return {}
143 if key:
144 return d.get(key)
145 return d
146 #@+node:ekr.20180407191219.1: *3* ipy_w.make_notebook
147 def make_notebook(self):
148 """Create a JSON notebook"""
149 root = self.root
150 nb = self.get_ua(root, key='prefix') or self.default_metadata()
151 # Write the expansion status of the root.
152 meta = nb.get('metadata') or {}
153 meta['collapsed'] = not root.isExpanded()
154 nb['metadata'] = meta
155 # Put all the cells.
156 nb['cells'] = [self.put_body(p) for p in root.subtree()]
157 return nb
158 #@+node:ekr.20180407195341.1: *3* ipy_w.put_body & helpers
159 def put_body(self, p):
160 """Put the body text of p, as an element of dict d."""
161 cell = self.get_ua(p, 'cell') or {}
162 meta = cell.get('metadata') or {}
163 self.update_cell_properties(cell, meta, p)
164 self.update_cell_body(cell, meta, p)
165 # g.printObj(meta, tag='metadata')
166 # g.printObj(cell, tag='cell')
167 return cell
168 #@+node:ekr.20180409120613.1: *4* ipy_w.update_cell_body
169 pat1 = re.compile(r'^.*<[hH]([123456])>(.*)</[hH]([123456])>')
170 pat2 = re.compile(r'^\s*([#]+)')
172 def update_cell_body(self, cell, meta, p):
173 """Create a new body text, depending on kind."""
175 def clean(lines):
176 lines = [z for z in lines if not g.isDirective(z)]
177 s = ''.join(lines).strip() + '\n'
178 return g.splitLines(s)
180 kind = self.cell_type(p)
181 lines = g.splitLines(p.b)
182 level = p.level() - self.root.level()
183 if kind == 'markdown':
184 # Remove all header markup lines.
185 lines = [z for z in lines if
186 not self.pat1.match(z) and not self.pat2.match(z)]
187 lines = clean(lines)
188 # Insert a new header markup line.
189 if level > 0:
190 lines.insert(0, '%s %s\n' % ('#' * level, self.clean_headline(p.h)))
191 else:
192 # Remember the level for the importer.
193 meta['leo_level'] = level
194 lines = clean(lines)
195 # Remove leading whitespace lines inserted during import.
196 cell['source'] = lines
197 #@+node:ekr.20180409120454.1: *4* ipy_w.update_cell_properties
198 def update_cell_properties(self, cell, meta, p):
199 """Update cell properties."""
200 # Update the metadata.
201 meta['leo_headline'] = p.h
202 meta['collapsed'] = not p.isExpanded()
203 # "cell_type" should not be in the metadata.
204 if meta.get('cell_type'):
205 del meta['cell_type']
206 cell['metadata'] = meta
207 # Update required properties.
208 cell['cell_type'] = kind = self.cell_type(p)
209 if kind == 'code':
210 if cell.get('outputs') is None:
211 cell['outputs'] = []
212 if cell.get('execution_count') is None:
213 cell['execution_count'] = 0
214 else:
215 # These properties are invalid!
216 for prop in ('execution_count', 'outputs'):
217 if cell.get(prop) is not None:
218 del cell[prop]
219 return kind
221 #@-others
222#@-others
223writer_dict = {
224 '@auto': ['@auto-jupyter', '@auto-ipynb',],
225 'class': Export_IPYNB,
226 'extensions': ['.ipynb',],
227}
228#@@language python
229#@@tabwidth -4
230#@-leo