##// END OF EJS Templates
Use @DocInherit to put docstrings on render dispatch functions in base class.
Anton I. Sipos -
Show More
@@ -0,0 +1,68 b''
1 """
2 doc_inherit decorator
3
4 Usage:
5
6 class Foo(object):
7 def foo(self):
8 "Frobber"
9 pass
10
11 class Bar(Foo):
12 @doc_inherit
13 def foo(self):
14 pass
15
16 Now, Bar.foo.__doc__ == Bar().foo.__doc__ == Foo.foo.__doc__ == "Frobber"
17 See: http://stackoverflow.com/questions/2025562/inherit-docstrings-in-python-class-inheritance
18 """
19
20 from functools import wraps
21
22
23 class DocInherit(object):
24 """
25 Docstring inheriting method descriptor
26
27 The class itself is also used as a decorator
28 """
29
30 def __init__(self, mthd):
31 self.mthd = mthd
32 self.name = mthd.__name__
33
34 def __get__(self, obj, cls):
35 if obj:
36 return self.get_with_inst(obj, cls)
37 else:
38 return self.get_no_inst(cls)
39
40 def get_with_inst(self, obj, cls):
41
42 overridden = getattr(super(cls, obj), self.name, None)
43
44 @wraps(self.mthd, assigned=('__name__', '__module__'))
45 def f(*args, **kwargs):
46 return self.mthd(obj, *args, **kwargs)
47
48 return self.use_parent_doc(f, overridden)
49
50 def get_no_inst(self, cls):
51
52 for parent in cls.__mro__[1:]:
53 overridden = getattr(parent, self.name, None)
54 if overridden: break
55
56 @wraps(self.mthd, assigned=('__name__', '__module__'))
57 def f(*args, **kwargs):
58 return self.mthd(*args, **kwargs)
59
60 return self.use_parent_doc(f, overridden)
61
62 def use_parent_doc(self, func, source):
63 if source is None:
64 raise NameError, ("Can't find '%s' in parents" % self.name)
65 func.__doc__ = source.__doc__
66 return func
67
68 doc_inherit = DocInherit
@@ -1,271 +1,278 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """A really simple notebook to rst/html exporter.
2 """A really simple notebook to rst/html exporter.
3
3
4 Usage
4 Usage
5
5
6 ./nb2html.py file.ipynb
6 ./nb2html.py file.ipynb
7
7
8 Produces 'file.rst' and 'file.html', along with auto-generated figure files
8 Produces 'file.rst' and 'file.html', along with auto-generated figure files
9 called nb_figure_NN.png.
9 called nb_figure_NN.png.
10
10
11 """
11 """
12
12
13 import os
13 import os
14 import subprocess
14 import subprocess
15 import sys
15 import sys
16 from IPython.external import argparse
16 from IPython.external import argparse
17 from IPython.nbformat import current as nbformat
17 from IPython.nbformat import current as nbformat
18 from IPython.utils.text import indent
18 from IPython.utils.text import indent
19
19 from decorators import DocInherit
20
20
21 # Cell converters
21 # Cell converters
22
22
23 def unknown_cell(cell):
23 def unknown_cell(cell):
24 """Default converter for cells of unknown type.
24 """Default converter for cells of unknown type.
25 """
25 """
26
26
27 return rst_directive('.. warning:: Unknown cell') + \
27 return rst_directive('.. warning:: Unknown cell') + \
28 [repr(cell)]
28 [repr(cell)]
29
29
30
30
31 def rst_directive(directive, text=''):
31 def rst_directive(directive, text=''):
32 out = [directive, '']
32 out = [directive, '']
33 if text:
33 if text:
34 out.extend([indent(text), ''])
34 out.extend([indent(text), ''])
35 return out
35 return out
36
36
37 # Converters for parts of a cell.
37 # Converters for parts of a cell.
38
38
39
39
40 class ConversionException(Exception):
40 class ConversionException(Exception):
41 pass
41 pass
42
42
43
43
44 class Converter(object):
44 class Converter(object):
45 default_encoding = 'utf-8'
45 default_encoding = 'utf-8'
46
46
47 def __init__(self, infile):
47 def __init__(self, infile):
48 self.infile = infile
48 self.infile = infile
49 self.dirpath = os.path.dirname(infile)
49 self.dirpath = os.path.dirname(infile)
50
50
51 @property
51 @property
52 def extension(self):
52 def extension(self):
53 raise ConversionException("""extension must be defined in Converter
53 raise ConversionException("""extension must be defined in Converter
54 subclass""")
54 subclass""")
55
55
56 def dispatch(self, cell_type):
56 def dispatch(self, cell_type):
57 """return cell_type dependent render method, for example render_code
57 """return cell_type dependent render method, for example render_code
58 """
58 """
59 return getattr(self, 'render_' + cell_type, unknown_cell)
59 return getattr(self, 'render_' + cell_type, unknown_cell)
60
60
61 def convert(self):
61 def convert(self):
62 lines = []
62 lines = []
63 for cell in self.nb.worksheets[0].cells:
63 for cell in self.nb.worksheets[0].cells:
64 conv_fn = self.dispatch(cell.cell_type)
64 conv_fn = self.dispatch(cell.cell_type)
65 lines.extend(conv_fn(cell))
65 lines.extend(conv_fn(cell))
66 lines.append('')
66 lines.append('')
67 return '\n'.join(lines)
67 return '\n'.join(lines)
68
68
69 def render(self):
69 def render(self):
70 "read, convert, and save self.infile"
70 "read, convert, and save self.infile"
71 self.read()
71 self.read()
72 self.output = self.convert()
72 self.output = self.convert()
73 return self.save()
73 return self.save()
74
74
75 def read(self):
75 def read(self):
76 "read and parse notebook into NotebookNode called self.nb"
76 "read and parse notebook into NotebookNode called self.nb"
77 with open(self.infile) as f:
77 with open(self.infile) as f:
78 self.nb = nbformat.read(f, 'json')
78 self.nb = nbformat.read(f, 'json')
79
79
80 def save(self, infile=None, encoding=None):
80 def save(self, infile=None, encoding=None):
81 "read and parse notebook into self.nb"
81 "read and parse notebook into self.nb"
82 if infile is None:
82 if infile is None:
83 infile = os.path.splitext(self.infile)[0] + '.' + self.extension
83 infile = os.path.splitext(self.infile)[0] + '.' + self.extension
84 if encoding is None:
84 if encoding is None:
85 encoding = self.default_encoding
85 encoding = self.default_encoding
86 with open(infile, 'w') as f:
86 with open(infile, 'w') as f:
87 f.write(self.output.encode(encoding))
87 f.write(self.output.encode(encoding))
88 return infile
88 return infile
89
89
90 def render_heading(self, cell):
90 def render_heading(self, cell):
91 """convert a heading cell
92
93 Returns list."""
91 raise NotImplementedError
94 raise NotImplementedError
92
95
93 def render_code(self, cell):
96 def render_code(self, cell):
97 """Convert a code cell
98
99 Returns list."""
94 raise NotImplementedError
100 raise NotImplementedError
95
101
96 def render_markdown(self, cell):
102 def render_markdown(self, cell):
103 """convert a markdown cell
104
105 Returns list."""
97 raise NotImplementedError
106 raise NotImplementedError
98
107
99 def render_pyout(self, cell):
108 def render_pyout(self, cell):
109 """convert pyout part of a code cell
110
111 Returns list."""
100 raise NotImplementedError
112 raise NotImplementedError
101
113
102 def render_display_data(self, cell):
114 def render_display_data(self, cell):
115 """convert display data from the output of a code cell
116
117 Returns list.
118 """
103 raise NotImplementedError
119 raise NotImplementedError
104
120
105 def render_stream(self, cell):
121 def render_stream(self, cell):
122 """convert stream part of a code cell
123
124 Returns list."""
125 raise NotImplementedError
126
127 def render_plaintext(self, cell):
128 """convert plain text
129
130 Returns list."""
106 raise NotImplementedError
131 raise NotImplementedError
107
132
108
133
109 class ConverterRST(Converter):
134 class ConverterRST(Converter):
110 extension = 'rst'
135 extension = 'rst'
111 figures_counter = 0
136 figures_counter = 0
112 heading_level = {1: '=', 2: '-', 3: '`', 4: '\'', 5: '.', 6: '~'}
137 heading_level = {1: '=', 2: '-', 3: '`', 4: '\'', 5: '.', 6: '~'}
113
138
139 @DocInherit
114 def render_heading(self, cell):
140 def render_heading(self, cell):
115 """convert a heading cell to rst
116
117 Returns list."""
118 marker = self.heading_level[cell.level]
141 marker = self.heading_level[cell.level]
119 return ['{0}\n{1}\n'.format(cell.source, marker * len(cell.source))]
142 return ['{0}\n{1}\n'.format(cell.source, marker * len(cell.source))]
120
143
144 @DocInherit
121 def render_code(self, cell):
145 def render_code(self, cell):
122 """Convert a code cell to rst
123
124 Returns list."""
125
126 if not cell.input:
146 if not cell.input:
127 return []
147 return []
128
148
129 lines = ['In[%s]:' % cell.prompt_number, '']
149 lines = ['In[%s]:' % cell.prompt_number, '']
130 lines.extend(rst_directive('.. code:: python', cell.input))
150 lines.extend(rst_directive('.. code:: python', cell.input))
131
151
132 for output in cell.outputs:
152 for output in cell.outputs:
133 conv_fn = self.dispatch(output.output_type)
153 conv_fn = self.dispatch(output.output_type)
134 lines.extend(conv_fn(output))
154 lines.extend(conv_fn(output))
135
155
136 return lines
156 return lines
137
157
158 @DocInherit
138 def render_markdown(self, cell):
159 def render_markdown(self, cell):
139 """convert a markdown cell to rst
140
141 Returns list."""
142 return [cell.source]
160 return [cell.source]
143
161
162 @DocInherit
144 def render_plaintext(self, cell):
163 def render_plaintext(self, cell):
145 """convert plain text to rst
146
147 Returns list."""
148 return [cell.source]
164 return [cell.source]
149
165
166 @DocInherit
150 def render_pyout(self, output):
167 def render_pyout(self, output):
151 """convert pyout part of a code cell to rst
152
153 Returns list."""
154
155 lines = ['Out[%s]:' % output.prompt_number, '']
168 lines = ['Out[%s]:' % output.prompt_number, '']
156
169
157 # output is a dictionary like object with type as a key
170 # output is a dictionary like object with type as a key
158 if 'latex' in output:
171 if 'latex' in output:
159 lines.extend(rst_directive('.. math::', output.latex))
172 lines.extend(rst_directive('.. math::', output.latex))
160
173
161 if 'text' in output:
174 if 'text' in output:
162 lines.extend(rst_directive('.. parsed-literal::', output.text))
175 lines.extend(rst_directive('.. parsed-literal::', output.text))
163
176
164 return lines
177 return lines
165
178
179 @DocInherit
166 def render_display_data(self, output):
180 def render_display_data(self, output):
167 """convert display data from the output of a code cell to rst.
168
169 Returns list.
170 """
171 lines = []
181 lines = []
172
182
173 if 'png' in output:
183 if 'png' in output:
174 infile = 'nb_figure_%s.png' % self.figures_counter
184 infile = 'nb_figure_%s.png' % self.figures_counter
175 fullname = os.path.join(self.dirpath, infile)
185 fullname = os.path.join(self.dirpath, infile)
176 with open(fullname, 'w') as f:
186 with open(fullname, 'w') as f:
177 f.write(output.png.decode('base64'))
187 f.write(output.png.decode('base64'))
178
188
179 self.figures_counter += 1
189 self.figures_counter += 1
180 lines.append('.. image:: %s' % infile)
190 lines.append('.. image:: %s' % infile)
181 lines.append('')
191 lines.append('')
182
192
183 return lines
193 return lines
184
194
195 @DocInherit
185 def render_stream(self, output):
196 def render_stream(self, output):
186 """convert stream part of a code cell to rst
187
188 Returns list."""
189
190 lines = []
197 lines = []
191
198
192 if 'text' in output:
199 if 'text' in output:
193 lines.extend(rst_directive('.. parsed-literal::', output.text))
200 lines.extend(rst_directive('.. parsed-literal::', output.text))
194
201
195 return lines
202 return lines
196
203
197
204
198 def rst2simplehtml(infile):
205 def rst2simplehtml(infile):
199 """Convert a rst file to simplified html suitable for blogger.
206 """Convert a rst file to simplified html suitable for blogger.
200
207
201 This just runs rst2html with certain parameters to produce really simple
208 This just runs rst2html with certain parameters to produce really simple
202 html and strips the document header, so the resulting file can be easily
209 html and strips the document header, so the resulting file can be easily
203 pasted into a blogger edit window.
210 pasted into a blogger edit window.
204 """
211 """
205
212
206 # This is the template for the rst2html call that produces the cleanest,
213 # This is the template for the rst2html call that produces the cleanest,
207 # simplest html I could find. This should help in making it easier to
214 # simplest html I could find. This should help in making it easier to
208 # paste into the blogspot html window, though I'm still having problems
215 # paste into the blogspot html window, though I'm still having problems
209 # with linebreaks there...
216 # with linebreaks there...
210 cmd_template = ("rst2html --link-stylesheet --no-xml-declaration "
217 cmd_template = ("rst2html --link-stylesheet --no-xml-declaration "
211 "--no-generator --no-datestamp --no-source-link "
218 "--no-generator --no-datestamp --no-source-link "
212 "--no-toc-backlinks --no-section-numbering "
219 "--no-toc-backlinks --no-section-numbering "
213 "--strip-comments ")
220 "--strip-comments ")
214
221
215 cmd = "%s %s" % (cmd_template, infile)
222 cmd = "%s %s" % (cmd_template, infile)
216 proc = subprocess.Popen(cmd,
223 proc = subprocess.Popen(cmd,
217 stdout=subprocess.PIPE,
224 stdout=subprocess.PIPE,
218 stderr=subprocess.PIPE,
225 stderr=subprocess.PIPE,
219 shell=True)
226 shell=True)
220 html, stderr = proc.communicate()
227 html, stderr = proc.communicate()
221 if stderr:
228 if stderr:
222 raise IOError(stderr)
229 raise IOError(stderr)
223
230
224 # Make an iterator so breaking out holds state. Our implementation of
231 # Make an iterator so breaking out holds state. Our implementation of
225 # searching for the html body below is basically a trivial little state
232 # searching for the html body below is basically a trivial little state
226 # machine, so we need this.
233 # machine, so we need this.
227 walker = iter(html.splitlines())
234 walker = iter(html.splitlines())
228
235
229 # Find start of main text, break out to then print until we find end /div.
236 # Find start of main text, break out to then print until we find end /div.
230 # This may only work if there's a real title defined so we get a 'div class'
237 # This may only work if there's a real title defined so we get a 'div class'
231 # tag, I haven't really tried.
238 # tag, I haven't really tried.
232 for line in walker:
239 for line in walker:
233 if line.startswith('<body>'):
240 if line.startswith('<body>'):
234 break
241 break
235
242
236 newfname = os.path.splitext(infile)[0] + '.html'
243 newfname = os.path.splitext(infile)[0] + '.html'
237 with open(newfname, 'w') as f:
244 with open(newfname, 'w') as f:
238 for line in walker:
245 for line in walker:
239 if line.startswith('</body>'):
246 if line.startswith('</body>'):
240 break
247 break
241 f.write(line)
248 f.write(line)
242 f.write('\n')
249 f.write('\n')
243
250
244 return newfname
251 return newfname
245
252
246
253
247 def main(infile, format='rst'):
254 def main(infile, format='rst'):
248 """Convert a notebook to html in one step"""
255 """Convert a notebook to html in one step"""
249 if format == 'rst':
256 if format == 'rst':
250 converter = ConverterRST(infile)
257 converter = ConverterRST(infile)
251 converter.render()
258 converter.render()
252 elif format == 'html':
259 elif format == 'html':
253 #Currently, conversion to html is a 2 step process, nb->rst->html
260 #Currently, conversion to html is a 2 step process, nb->rst->html
254 converter = ConverterRST(infile)
261 converter = ConverterRST(infile)
255 rstfname = converter.render()
262 rstfname = converter.render()
256 rst2simplehtml(rstfname)
263 rst2simplehtml(rstfname)
257
264
258
265
259 if __name__ == '__main__':
266 if __name__ == '__main__':
260 parser = argparse.ArgumentParser(description='nbconvert: Convert IPython notebooks to other formats')
267 parser = argparse.ArgumentParser(description='nbconvert: Convert IPython notebooks to other formats')
261
268
262 # TODO: consider passing file like object around, rather than filenames
269 # TODO: consider passing file like object around, rather than filenames
263 # would allow us to process stdin, or even http streams
270 # would allow us to process stdin, or even http streams
264 #parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), default=sys.stdin)
271 #parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), default=sys.stdin)
265
272
266 #Require a filename as a positional argument
273 #Require a filename as a positional argument
267 parser.add_argument('infile', nargs=1)
274 parser.add_argument('infile', nargs=1)
268 parser.add_argument('-f', '--format', default='rst',
275 parser.add_argument('-f', '--format', default='rst',
269 help='Output format. Supported formats: rst (default), html.')
276 help='Output format. Supported formats: rst (default), html.')
270 args = parser.parse_args()
277 args = parser.parse_args()
271 main(infile=args.infile[0], format=args.format)
278 main(infile=args.infile[0], format=args.format)
General Comments 0
You need to be logged in to leave comments. Login now