##// END OF EJS Templates
Changes after in person review with @ellisonbg including TODO tags
Jonathan Frederic -
Show More
@@ -1,220 +1,220
1 1 """
2 2 Module containing single call export functions.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15
16 16 from functools import wraps
17 17
18 18 from IPython.nbformat.v3.nbbase import NotebookNode
19 19 from IPython.config import Config
20 20
21 21 from .exporter import Exporter
22 22 from .basichtml import BasicHTMLExporter
23 23 from .fullhtml import FullHTMLExporter
24 24 from .latex import LatexExporter
25 25 from .markdown import MarkdownExporter
26 26 from .python import PythonExporter
27 27 from .reveal import RevealExporter
28 28 from .rst import RstExporter
29 29 from .sphinx_howto import SphinxHowtoExporter
30 30 from .sphinx_manual import SphinxManualExporter
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Classes
34 34 #-----------------------------------------------------------------------------
35 35
36 36 def DocDecorator(f):
37 37
38 38 #Set docstring of function
39 39 f.__doc__ = f.__doc__ + """
40 40 nb : Notebook node
41 41 config : config (optional, keyword arg)
42 42 User configuration instance.
43 43 resources : dict (optional, keyword arg)
44 44 Resources used in the conversion process.
45 45
46 46 Returns
47 47 ----------
48 48 tuple- output, resources, exporter_instance
49 49 output : str
50 50 Jinja 2 output. This is the resulting converted notebook.
51 51 resources : dictionary
52 52 Dictionary of resources used prior to and during the conversion
53 53 process.
54 54 exporter_instance : Exporter
55 55 Instance of the Exporter class used to export the document. Useful
56 56 to caller because it provides a 'file_extension' property which
57 57 specifies what extension the output should be saved as."""
58 58
59 59 @wraps(f)
60 60 def decorator(*args, **kwargs):
61 61 return f(*args, **kwargs)
62 62
63 63 return decorator
64 64
65 65
66 66 #-----------------------------------------------------------------------------
67 67 # Functions
68 68 #-----------------------------------------------------------------------------
69 69
70 70 __all__ = [
71 71 'export',
72 72 'export_sphinx_manual',
73 73 'export_sphinx_howto',
74 74 'export_basic_html',
75 75 'export_full_html',
76 76 'export_latex',
77 77 'export_markdown',
78 78 'export_python',
79 79 'export_reveal',
80 80 'export_rst',
81 81 'export_by_name',
82 82 'get_export_names'
83 83 ]
84 84
85 85 class ExporterNameError(NameError):
86 86 pass
87 87
88 88 @DocDecorator
89 89 def export(exporter_type, nb, **kw):
90 90 """
91 91 Export a notebook object using specific exporter class.
92 92
93 93 exporter_type : Exporter class type
94 94 Class type of the exporter that should be used. This method
95 95 will initialize it's own instance of the class. It is
96 96 ASSUMED that the class type provided exposes a
97 97 constructor (__init__) with the same signature as the
98 98 base Exporter class.}
99 99 """
100 100
101 101 #Check arguments
102 102 if exporter_type is None:
103 103 raise TypeError("Exporter is None")
104 104 elif not issubclass(exporter_type, Exporter):
105 105 raise TypeError("Exporter type does not inherit from Exporter (base)")
106 106 if nb is None:
107 107 raise TypeError("nb is None")
108 108
109 109 #Create the exporter
110 exporter_instance = exporter_type(config=kw.get('config', Config()))
110 resources = kw.pop('resources', None)
111 exporter_instance = exporter_type(**kw)
111 112
112 113 #Try to convert the notebook using the appropriate conversion function.
113 resources = kw.get('resources', {})
114 114 if isinstance(nb, NotebookNode):
115 115 output, resources = exporter_instance.from_notebook_node(nb, resources)
116 116 elif isinstance(nb, basestring):
117 117 output, resources = exporter_instance.from_filename(nb, resources)
118 118 else:
119 119 output, resources = exporter_instance.from_file(nb, resources)
120 120 return output, resources
121 121
122 122
123 123 @DocDecorator
124 124 def export_sphinx_manual(nb, **kw):
125 125 """
126 126 Export a notebook object to Sphinx Manual LaTeX
127 127 """
128 128 return export(SphinxManualExporter, nb, **kw)
129 129
130 130
131 131 @DocDecorator
132 132 def export_sphinx_howto(nb, **kw):
133 133 """
134 134 Export a notebook object to Sphinx HowTo LaTeX
135 135 """
136 136 return export(SphinxHowtoExporter, nb, **kw)
137 137
138 138
139 139 @DocDecorator
140 140 def export_basic_html(nb, **kw):
141 141 """
142 142 Export a notebook object to Basic HTML
143 143 """
144 144 return export(BasicHTMLExporter, nb, **kw)
145 145
146 146
147 147 @DocDecorator
148 148 def export_full_html(nb, **kw):
149 149 """
150 150 Export a notebook object to Full HTML
151 151 """
152 152 return export(FullHTMLExporter, nb, **kw)
153 153
154 154
155 155 @DocDecorator
156 156 def export_latex(nb, **kw):
157 157 """
158 158 Export a notebook object to LaTeX
159 159 """
160 160 return export(LatexExporter, nb, **kw)
161 161
162 162
163 163 @DocDecorator
164 164 def export_markdown(nb, **kw):
165 165 """
166 166 Export a notebook object to Markdown
167 167 """
168 168 return export(MarkdownExporter, nb, **kw)
169 169
170 170
171 171 @DocDecorator
172 172 def export_python(nb, **kw):
173 173 """
174 174 Export a notebook object to Python
175 175 """
176 176 return export(PythonExporter, nb, **kw)
177 177
178 178
179 179 @DocDecorator
180 180 def export_reveal(nb, **kw):
181 181 """
182 182 Export a notebook object to Reveal
183 183 """
184 184 return export(RevealExporter, nb, **kw)
185 185
186 186
187 187 @DocDecorator
188 188 def export_rst(nb, **kw):
189 189 """
190 190 Export a notebook object to RST
191 191 """
192 192 return export(RstExporter, nb, **kw)
193 193
194 194
195 195 @DocDecorator
196 196 def export_by_name(format_name, nb, **kw):
197 197 """
198 198 Export a notebook object to a template type by its name. Reflection
199 199 (Inspect) is used to find the template's corresponding explicit export
200 200 method defined in this module. That method is then called directly.
201 201
202 202 format_name : str
203 203 Name of the template style to export to.
204 204 """
205 205
206 206 function_name = "export_" + format_name.lower()
207 207
208 208 if function_name in globals():
209 209 return globals()[function_name](nb, **kw)
210 210 else:
211 211 raise ExporterNameError("template for `%s` not found" % function_name)
212 212
213 213 def get_export_names():
214 214 "Return a list of the currently supported export targets"
215 215 # grab everything after 'export_'
216 216 l = [x[len('export_'):] for x in __all__ if x.startswith('export_')]
217 217
218 218 # filter out the one method that is not a template
219 219 l = [x for x in l if 'by_name' not in x]
220 220 return sorted(l)
@@ -1,361 +1,360
1 1 """This module defines Exporter, a highly configurable converter
2 2 that uses Jinja2 to export notebook files into different formats.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 from __future__ import print_function, absolute_import
18 18
19 19 # Stdlib imports
20 20 import io
21 21 import os
22 22 import inspect
23 23 import types
24 24 from copy import deepcopy
25 25
26 26 # other libs/dependencies
27 27 from jinja2 import Environment, FileSystemLoader, ChoiceLoader
28 28
29 29 # IPython imports
30 30 from IPython.config.configurable import Configurable
31 31 from IPython.config import Config
32 32 from IPython.nbformat import current as nbformat
33 33 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict
34 34 from IPython.utils.importstring import import_item
35 35 from IPython.utils.text import indent
36 36
37 37 from IPython.nbconvert import filters
38 38 from IPython.nbconvert import transformers
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Globals and constants
42 42 #-----------------------------------------------------------------------------
43 43
44 44 #Jinja2 extensions to load.
45 45 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
46 46
47 47 default_filters = {
48 48 'indent': indent,
49 49 'markdown': filters.markdown2html,
50 50 'ansi2html': filters.ansi2html,
51 51 'filter_data_type': filters.DataTypeFilter,
52 52 'get_lines': filters.get_lines,
53 53 'highlight': filters.highlight,
54 54 'highlight2html': filters.highlight,
55 55 'highlight2latex': filters.highlight2latex,
56 56 'markdown2latex': filters.markdown2latex,
57 57 'markdown2rst': filters.markdown2rst,
58 58 'pycomment': filters.python_comment,
59 59 'rm_ansi': filters.remove_ansi,
60 60 'rm_dollars': filters.strip_dollars,
61 61 'rm_fake': filters.rm_fake,
62 62 'ansi2latex': filters.ansi2latex,
63 63 'rm_math_space': filters.rm_math_space,
64 64 'wrap': filters.wrap
65 65 }
66 66
67 67 #-----------------------------------------------------------------------------
68 68 # Class
69 69 #-----------------------------------------------------------------------------
70 70
71 71 class Exporter(Configurable):
72 72 """
73 73 Exports notebooks into other file formats. Uses Jinja 2 templating engine
74 74 to output new formats. Inherit from this class if you are creating a new
75 75 template type along with new filters/transformers. If the filters/
76 76 transformers provided by default suffice, there is no need to inherit from
77 77 this class. Instead, override the template_file and file_extension
78 78 traits via a config file.
79 79
80 80 {filters}
81 81 """
82 82
83 83 # finish the docstring
84 84 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
85 85
86 86
87 87 template_file = Unicode(
88 88 '', config=True,
89 89 help="Name of the template file to use")
90 90
91 91 file_extension = Unicode(
92 92 'txt', config=True,
93 93 help="Extension of the file that should be written to disk"
94 94 )
95 95
96 96 template_path = Unicode(
97 97 os.path.join("..", "templates"), config=True,
98 98 help="Path where the template files are located.")
99 99
100 100 template_skeleton_path = Unicode(
101 101 os.path.join("..", "templates", "skeleton"), config=True,
102 102 help="Path where the template skeleton files are located.")
103 103
104 104 #Jinja block definitions
105 105 jinja_comment_block_start = Unicode("", config=True)
106 106 jinja_comment_block_end = Unicode("", config=True)
107 107 jinja_variable_block_start = Unicode("", config=True)
108 108 jinja_variable_block_end = Unicode("", config=True)
109 109 jinja_logic_block_start = Unicode("", config=True)
110 110 jinja_logic_block_end = Unicode("", config=True)
111 111
112 112 #Extension that the template files use.
113 113 template_extension = Unicode(".tpl", config=True)
114 114
115 115 #Configurability, allows the user to easily add filters and transformers.
116 116 transformers = List(config=True,
117 117 help="""List of transformers, by name or namespace, to enable.""")
118 118 filters = Dict(config=True,
119 119 help="""Dictionary of filters, by name and namespace, to add to the Jinja
120 120 environment.""")
121 121
122 122 def __init__(self, config=None, extra_loaders=None, **kw):
123 123 """
124 124 Public constructor
125 125
126 126 Parameters
127 127 ----------
128 128 config : config
129 129 User configuration instance.
130 130 extra_loaders : list[of Jinja Loaders]
131 131 ordered list of Jinja loder to find templates. Will be tried in order
132 132 before the default FileSysteme ones.
133 133 """
134 134
135 135 #Call the base class constructor
136 136 c = self.default_config
137 137 if config:
138 138 c.merge(config)
139 139
140 140 super(Exporter, self).__init__(config=c, **kw)
141 141
142 142 #Standard environment
143 143 self._init_environment(extra_loaders=extra_loaders)
144 144
145 145 #Add transformers
146 146 self._transformers = []
147 147 self._register_transformers()
148 148
149 149 #Add filters to the Jinja2 environment
150 150 self._register_filters()
151 151
152 152 #Load user transformers. Enabled by default.
153 153 if self.transformers:
154 154 for transformer in self.transformers:
155 self.register_transformer(transformer, True)
155 self.register_transformer(transformer, enabled=True)
156 156
157 157 #Load user filters. Overwrite existing filters if need be.
158 158 if self.filters:
159 159 for key, user_filter in self.filters.iteritems():
160 160 self.register_filter(key, user_filter)
161 161
162 162
163 163 @property
164 164 def default_config(self):
165 165 return Config()
166 166
167 167
168 168 def from_notebook_node(self, nb, resources=None, **kw):
169 169 """
170 170 Convert a notebook from a notebook node instance.
171 171
172 172 Parameters
173 173 ----------
174 174 nb : Notebook node
175 175 resources : dict (**kw)
176 176 of additional resources that can be accessed read/write by
177 177 transformers and filters.
178 178 """
179 179 nb_copy = deepcopy(nb)
180 180
181 #Set output extension in resources dict
182 #TODO: init_resources
183 resources['output_extension'] = self.file_extension
184
181 185 #Preprocess
182 nb_copy, resources = self._preprocess(nb_copy, resources)
186 nb_copy, resources = self._transform(nb_copy, resources)
183 187
184 188 #Convert
185 189 self.template = self.environment.get_template(self.template_file + self.template_extension)
186 190 output = self.template.render(nb=nb_copy, resources=resources)
187
188 #Set output extension in resources dict
189 resources['output_extension'] = self.file_extension
190 191 return output, resources
191 192
192 193
193 def from_filename(self, filename, resources=None **kw):
194 def from_filename(self, filename, resources=None, **kw):
194 195 """
195 196 Convert a notebook from a notebook file.
196 197
197 198 Parameters
198 199 ----------
199 200 filename : str
200 201 Full filename of the notebook file to open and convert.
201 202 """
202 203
203 204 with io.open(filename) as f:
204 205 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
205 206
206 207
207 208 def from_file(self, file_stream, resources=None, **kw):
208 209 """
209 210 Convert a notebook from a notebook file.
210 211
211 212 Parameters
212 213 ----------
213 214 file_stream : file-like object
214 215 Notebook file-like object to convert.
215 216 """
216 217 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
217 218
218 219
219 220 def register_transformer(self, transformer, enabled=None):
220 221 """
221 222 Register a transformer.
222 223 Transformers are classes that act upon the notebook before it is
223 224 passed into the Jinja templating engine. Transformers are also
224 225 capable of passing additional information to the Jinja
225 226 templating engine.
226 227
227 228 Parameters
228 229 ----------
229 230 transformer : transformer
230 231 """
231 232
232 233 #Handle transformer's registration based on it's type
233 234 if inspect.isfunction(transformer):
234 235 #Transformer is a function, no need to construct it.
235 236 self._transformers.append(transformer)
236 237 return transformer
237 238
238 239 elif isinstance(transformer, types.StringTypes):
239 240 #Transformer is a string, import the namespace and recursively call
240 241 #this register_transformer method
241 242 transformer_cls = import_item(DottedObjectName(transformer))
242 243 return self.register_transformer(transformer_cls, enabled=None)
243 244
244 245 elif isinstance(transformer, MetaHasTraits):
245 246 #Transformer is configurable. Make sure to pass in new default for
246 247 #the enabled flag if one was specified.
247 c = Config()
248 if not enabled is None:
249 c = Config({transformer.__name__: {'enabled': enabled}})
250 c.merge(self.config)
251 transformer_instance = transformer(config=c)
248 transformer_instance = transformer(parent=self)
249 if enabled is not None:
250 transformer_instance.enabled = True
252 251
253 252 else:
254 253 #Transformer is not configurable, construct it
255 254 transformer_instance = transformer()
256 255
257 256 #Register and return the transformer.
258 257 self._transformers.append(transformer_instance)
259 258 return transformer_instance
260 259
261 260
262 261 def register_filter(self, name, filter):
263 262 """
264 263 Register a filter.
265 264 A filter is a function that accepts and acts on one string.
266 265 The filters are accesible within the Jinja templating engine.
267 266
268 267 Parameters
269 268 ----------
270 269 name : str
271 270 name to give the filter in the Jinja engine
272 271 filter : filter
273 272 """
274 273 if inspect.isfunction(filter):
275 274 self.environment.filters[name] = filter
276 275 elif isinstance(filter, types.StringTypes):
277 276 filter_cls = import_item(DottedObjectName(filter))
278 277 self.register_filter(name, filter_cls)
279 278 elif isinstance(filter, MetaHasTraits):
280 279 self.environment.filters[name] = filter(config=self.config)
281 280 else:
282 281 self.environment.filters[name] = filter()
283 282 return self.environment.filters[name]
284 283
285 284
286 285 def _register_transformers(self):
287 286 """
288 287 Register all of the transformers needed for this exporter, disabled
289 288 unless specified explicitly.
290 289 """
291 290
292 291 self.register_transformer(transformers.coalesce_streams)
293 292 self.register_transformer(transformers.ExtractFigureTransformer)
294 293
295 294
296 295 def _register_filters(self):
297 296 """
298 297 Register all of the filters required for the exporter.
299 298 """
300 299 for key, value in default_filters.iteritems():
301 300 self.register_filter(key, value)
302 301
303 302
304 303 def _init_environment(self, extra_loaders=None):
305 304 """
306 305 Create the Jinja templating environment.
307 306 """
308 307 here = os.path.dirname(os.path.realpath(__file__))
309 308 loaders = []
310 309 if extra_loaders:
311 310 loaders.extend(extra_loaders)
312 311
313 312 loaders.append(FileSystemLoader([
314 313 os.path.join(here, self.template_path),
315 314 os.path.join(here, self.template_skeleton_path),
316 315 ]))
317 316
318 317 self.environment = Environment(
319 318 loader= ChoiceLoader(loaders),
320 319 extensions=JINJA_EXTENSIONS
321 320 )
322 321
323 322 #Set special Jinja2 syntax that will not conflict with latex.
324 323 if self.jinja_logic_block_start:
325 324 self.environment.block_start_string = self.jinja_logic_block_start
326 325 if self.jinja_logic_block_end:
327 326 self.environment.block_end_string = self.jinja_logic_block_end
328 327 if self.jinja_variable_block_start:
329 328 self.environment.variable_start_string = self.jinja_variable_block_start
330 329 if self.jinja_variable_block_end:
331 330 self.environment.variable_end_string = self.jinja_variable_block_end
332 331 if self.jinja_comment_block_start:
333 332 self.environment.comment_start_string = self.jinja_comment_block_start
334 333 if self.jinja_comment_block_end:
335 334 self.environment.comment_end_string = self.jinja_comment_block_end
336 335
337 336
338 def _preprocess(self, nb, resources):
337 def _transform(self, nb, resources):
339 338 """
340 339 Preprocess the notebook before passing it into the Jinja engine.
341 340 To preprocess the notebook is to apply all of the
342 341
343 342 Parameters
344 343 ----------
345 344 nb : notebook node
346 345 notebook that is being exported.
347 346 resources : a dict of additional resources that
348 347 can be accessed read/write by transformers
349 348 and filters.
350 349 """
351 350
352 351 # Do a deepcopy first,
353 352 # we are never safe enough with what the transformers could do.
354 353 nbc = deepcopy(nb)
355 354 resc = deepcopy(resources)
356 355
357 356 #Run each transformer on the notebook. Carry the output along
358 357 #to each transformer
359 358 for transformer in self._transformers:
360 359 nbc, resc = transformer(nbc, resc)
361 360 return nbc, resc
@@ -1,111 +1,111
1 1 """
2 2 Exporter that allows Latex Jinja templates to work. Contains logic to
3 3 appropriately prepare IPYNB files for export to LaTeX. Including but
4 4 not limited to escaping LaTeX, fixing math region tags, using special
5 5 tags to circumvent Jinja/Latex syntax conflicts.
6 6 """
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (c) 2013, the IPython Development Team.
9 9 #
10 10 # Distributed under the terms of the Modified BSD License.
11 11 #
12 12 # The full license is in the file COPYING.txt, distributed with this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # Stdlib imports
20 20 import os
21 21
22 22 # IPython imports
23 23 from IPython.utils.traitlets import Unicode
24 24 from IPython.config import Config
25 25
26 26 from IPython.nbconvert import filters, transformers
27 27 from .exporter import Exporter
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Classes and functions
31 31 #-----------------------------------------------------------------------------
32 32
33 33 class LatexExporter(Exporter):
34 34 """
35 35 Exports to a Latex template. Inherit from this class if your template is
36 36 LaTeX based and you need custom tranformers/filters. Inherit from it if
37 37 you are writing your own HTML template and need custom tranformers/filters.
38 38 If you don't need custom tranformers/filters, just change the
39 39 'template_file' config option. Place your template in the special "/latex"
40 40 subfolder of the "../templates" folder.
41 41 """
42 42
43 43 file_extension = Unicode(
44 44 'tex', config=True,
45 45 help="Extension of the file that should be written to disk")
46 46
47 47 template_file = Unicode(
48 48 'base', config=True,
49 49 help="Name of the template file to use")
50 50
51 51 #Latex constants
52 52 template_path = Unicode(
53 53 os.path.join("..", "templates", "latex"), config=True,
54 54 help="Path where the template files are located.")
55 55
56 56 template_skeleton_path = Unicode(
57 57 os.path.join("..", "templates", "latex", "skeleton"), config=True,
58 58 help="Path where the template skeleton files are located.")
59 59
60 60 #Special Jinja2 syntax that will not conflict when exporting latex.
61 61 jinja_comment_block_start = Unicode("((=", config=True)
62 62 jinja_comment_block_end = Unicode("=))", config=True)
63 63 jinja_variable_block_start = Unicode("(((", config=True)
64 64 jinja_variable_block_end = Unicode(")))", config=True)
65 65 jinja_logic_block_start = Unicode("((*", config=True)
66 66 jinja_logic_block_end = Unicode("*))", config=True)
67 67
68 68 #Extension that the template files use.
69 69 template_extension = Unicode(".tplx", config=True)
70 70
71 71 def _register_filters(self):
72 72 """
73 73 Register all of the filters required for the exporter.
74 74 """
75 75
76 76 #Register the filters of the base class.
77 77 super(LatexExporter, self)._register_filters()
78 78
79 79 #Add latex filters to the Jinja2 environment
80 80 self.register_filter('escape_tex', filters.escape_latex)
81 81 self.register_filter('highlight', filters.highlight2latex)
82 82
83 83
84 84 def _register_transformers(self):
85 85 """
86 86 Register all of the transformers needed for this exporter.
87 87 """
88 88
89 89 #Register ConvertSvgTransformer before any other transformers!
90 90 #Important because it allows the conversion of svg->png BEFORE the
91 91 #extract figure transformer acts on the data.
92 92 self.register_transformer(transformers.ConvertSvgTransformer, True)
93 93
94 94 #Register transformers
95 95 super(LatexExporter, self)._register_transformers()
96 96 self.register_transformer(transformers.LatexTransformer, True)
97 97
98 98 @property
99 99 def default_config(self):
100 100 c = Config({
101 101 'GlobalConfigurable': {
102 'display_data_priority' : ['latex', 'png', 'jpg', 'jpeg', 'text']
102 'display_data_priority' : ['latex', 'png', 'jpg', 'pdf', 'jpeg', 'text']
103 103 },
104 104 'ExtractFigureTransformer': {
105 105 'enabled':True
106 106 }
107 107
108 108 })
109 109 c.merge(super(LatexExporter,self).default_config)
110 110 return c
111 111
@@ -1,179 +1,180
1 1 #!/usr/bin/env python
2 2 """NBConvert is a utility for conversion of IPYNB files.
3 3
4 4 Commandline interface for the NBConvert conversion utility. Read the
5 5 readme.rst for usage information
6 6 """
7 7 #-----------------------------------------------------------------------------
8 8 #Copyright (c) 2013, the IPython Development Team.
9 9 #
10 10 #Distributed under the terms of the Modified BSD License.
11 11 #
12 12 #The full license is in the file COPYING.txt, distributed with this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 #Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #Stdlib imports
20 20 from __future__ import print_function
21 21 import sys
22 22 import os
23 23 import glob
24 24
25 25 #From IPython
26 26 from IPython.core.application import BaseIPythonApplication
27 27 from IPython.config.application import catch_config_error
28 28 from IPython.utils.traitlets import Unicode, List, Instance, DottedObjectName, Type
29 29 from IPython.utils.importstring import import_item
30 30
31 31 from .exporters.export import export_by_name, get_export_names, ExporterNameError
32 32 from .exporters.exporter import Exporter
33 33 from .writers.base import WriterBase
34 34 from .utils.config import GlobalConfigurable
35 35
36 36 #-----------------------------------------------------------------------------
37 37 #Classes and functions
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class NbConvertApp(BaseIPythonApplication):
41 41 """Application used to convert to and from notebook file type (*.ipynb)"""
42 42
43 43
44 44 description = Unicode(
45 45 u"""This application is used to convert notebook files (*.ipynb).
46 46 An ipython config file can be used to batch convert notebooks in the
47 47 current directory.""")
48 48
49 49 examples = Unicode(u"""
50 50 Running `ipython nbconvert` will read the directory config file and then
51 51 apply it to one or more notebooks.
52 52
53 53 Multiple notebooks can be given at the command line in a couple of
54 54 different ways:
55 55
56 56 > ipython nbconvert notebook*.ipynb
57 57 > ipython nbconvert notebook1.ipynb notebook2.ipynb
58 58 > ipython nbconvert # this will use the config file to fill in the notebooks
59 59 """)
60 60
61 61 config_file_name = Unicode(u'ipython_nbconvert_config.py')
62 62
63 63 #Writer specific variables
64 64 writer = Instance('IPython.nbconvert.writers.base.WriterBase',
65 65 help="""Instance of the writer class used to write the
66 66 results of the conversion.""")
67 67 writer_class = DottedObjectName('FilesWriter', config=True,
68 68 help="""Writer class used to write the
69 69 results of the conversion""")
70 70 writer_aliases = {'FilesWriter': 'IPython.nbconvert.writers.files.FilesWriter',
71 71 'DebugWriter': 'IPython.nbconvert.writers.debug.DebugWriter',
72 72 'StdoutWriter': 'IPython.nbconvert.writers.stdout.StdoutWriter'}
73 73 writer_factory = Type()
74 74
75 75 def _writer_class_changed(self, name, old, new):
76 76 if new in self.writer_aliases:
77 77 new = self.writer_aliases[new]
78 78 self.writer_factory = import_item(new)
79 79
80 80
81 81 #Other configurable variables
82 82 export_format = Unicode(
83 83 "", config=True,
84 84 help="""If specified, nbconvert will convert the document(s) specified
85 85 using this format.""")
86 86
87 87 notebooks = List([], config=True, help="""List of notebooks to convert.
88 88 Search patterns are supported.""")
89 89
90 90 aliases = {'format':'NbConvertApp.export_format',
91 91 'notebooks':'NbConvertApp.notebooks',
92 92 'writer':'NbConvertApp.writer_class'}
93 93
94 94
95 95 @catch_config_error
96 96 def initialize(self, argv=None):
97 97 super(NbConvertApp, self).initialize(argv)
98 98
99 99 #Register class here to have help with help all
100 100 self.classes.insert(0, Exporter)
101 101 self.classes.insert(0, WriterBase)
102 102 self.classes.insert(0, GlobalConfigurable)
103 103
104 104 #Init
105 105 self.init_config(self.extra_args)
106 106 self.init_writer()
107 107
108 108
109 109 def init_config(self, extra_args):
110 110 """
111 111 Add notebooks to the config if needed. Glob each notebook to replace
112 112 notebook patterns with filenames.
113 113 """
114 114
115 115 #Get any additional notebook patterns from the commandline
116 116 if len(extra_args) > 0:
117 117 for pattern in extra_args:
118 118 self.notebooks.append(pattern)
119 119
120 120 #Use glob to replace all the notebook patterns with filenames.
121 121 filenames = []
122 122 for pattern in self.notebooks:
123 123 for filename in glob.glob(pattern):
124 124 if not filename in filenames:
125 125 filenames.append(filename)
126 126 self.notebooks = filenames
127 127
128 128
129 129 def init_writer(self):
130 130 """
131 131 Initialize the writer (which is stateless)
132 132 """
133 133 self._writer_class_changed(None, self.writer_class, self.writer_class)
134 134 self.writer = self.writer_factory(parent=self)
135 135
136 136
137 137 def start(self, argv=None):
138 138 """
139 139 Entrypoint of NbConvert application.
140 140 """
141 141
142 142 #Call base
143 143 super(NbConvertApp, self).start()
144 144
145 145 #Export each notebook
146 #TODO: Empty check
146 147 for notebook_filename in self.notebooks:
147 148
148 149 #Get a unique key for the notebook and set it in the resources object.
149 150 basename = os.path.basename(notebook_filename)
150 151 notebook_name = basename[:basename.rfind('.')]
151 152 resources = {}
152 153 resources['unique_key'] = notebook_name
153 154
154 155 #Try to export
155 156 try:
156 return_value = export_by_name(self.export_format,
157 output, resources = export_by_name(self.export_format,
157 158 notebook_filename,
158 159 resources=resources,
159 160 config=self.config)
160 161 except ExporterNameError as e:
161 162 print("Error: '%s' exporter not found." % self.export_format,
162 163 file=sys.stderr)
163 164 print("Known exporters are:",
164 165 "\n\t" + "\n\t".join(get_export_names()),
165 166 file=sys.stderr)
166 167 sys.exit(-1)
167 else:
168 output, resources = return_value
168 except Exception as e:
169 print("Error: could no export '%s'" % notebook_filename, file=sys.stderr)
170 print(e, file=sys.stderr)
169 171
170 172 #Write
171 173 self.writer.write(output, resources, notebook_name=notebook_name)
172 174
173 175
174 176 #-----------------------------------------------------------------------------
175 177 # Main entry point
176 178 #-----------------------------------------------------------------------------
177 179
178 180 launch_new_instance = NbConvertApp.launch_instance
179
@@ -1,74 +1,77
1 1 """Module containing a transformer that converts outputs in the notebook from
2 2 one format to another.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 #TODO: Come after extract
13
12 14 #-----------------------------------------------------------------------------
13 15 # Imports
14 16 #-----------------------------------------------------------------------------
15 17
16 18 from .activatable import ActivatableTransformer
17 19
18 20 #-----------------------------------------------------------------------------
19 21 # Classes
20 22 #-----------------------------------------------------------------------------
21 23
22 24 class ConvertFiguresTransformer(ActivatableTransformer):
23 25 """
24 26 Converts all of the outputs in a notebook from one format to another.
25 27 """
26 28
27 29
28 30 def __init__(self, from_formats, to_format, **kw):
29 31 """
30 32 Public constructor
31 33
32 34 Parameters
33 35 ----------
34 36 from_formats : list [of string]
35 37 Formats that the converter can convert from
36 38 to_format : string
37 39 Format that the converter converts to
38 40 config : Config
39 41 Configuration file structure
40 42 **kw : misc
41 43 Additional arguments
42 44 """
43 45 super(ConvertFiguresTransformer, self).__init__(**kw)
44 46
47 #TODO: Configurable, singular
45 48 self._from_formats = from_formats
46 49 self._to_format = to_format
47 50
48 51
49 52 def convert_figure(self, data_format, data):
50 53 raise NotImplementedError()
51 54
52 55
53 56 def transform_cell(self, cell, resources, cell_index):
54 57 """
55 58 Apply a transformation on each cell,
56 59
57 60 See base.py
58 61 """
59 62
60 63 #Loop through all of the datatypes of the outputs in the cell.
61 64 for index, cell_out in enumerate(cell.get('outputs', [])):
62 65 for data_type, data in cell_out.items():
63 66 self._convert_figure(cell_out, data_type, data)
64 67 return cell, resources
65 68
66 69
67 70 def _convert_figure(self, cell_out, data_type, data):
68 71 """
69 72 Convert a figure and output the results to the cell output
70 73 """
71 74
72 75 if not self._to_format in cell_out:
73 76 if data_type in self._from_formats:
74 77 cell_out[self._to_format] = self.convert_figure(data_type, data)
@@ -1,71 +1,72
1 1 """Module containing a transformer that converts outputs in the notebook from
2 2 one format to another.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15
16 16 import os
17 17 from IPython.utils.tempdir import TemporaryDirectory
18 18
19 19 from .convertfigures import ConvertFiguresTransformer
20 20
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Constants
24 24 #-----------------------------------------------------------------------------
25 25
26 26 INKSCAPE_COMMAND = "inkscape --without-gui --export-pdf=\"{to_filename}\" \"{from_filename}\""
27 27
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Classes
31 31 #-----------------------------------------------------------------------------
32 32
33 33 class ConvertSvgTransformer(ConvertFiguresTransformer):
34 34 """
35 35 Converts all of the outputs in a notebook from one format to another.
36 36 """
37 37
38 38
39 39 def __init__(self, **kw):
40 40 """
41 41 Constructor
42 42 """
43 43 super(ConvertSvgTransformer, self).__init__(['svg'], 'pdf', **kw)
44 44
45 45
46 46 def convert_figure(self, data_format, data):
47 47 """
48 48 Convert a single Svg figure. Returns converted data.
49 49 """
50 50
51 51 #Work in a temporary directory
52 52 with TemporaryDirectory() as tmpdir:
53 53
54 54 #Write fig to temp file
55 55 input_filename = os.path.join(tmpdir, 'figure.' + data_format)
56 56 with open(input_filename, 'w') as f:
57 57 f.write(data)
58 58
59 59 #Call conversion application
60 60 output_filename = os.path.join(tmpdir, 'figure.pdf')
61 61 shell = INKSCAPE_COMMAND.format(from_filename=input_filename,
62 62 to_filename=output_filename)
63 63 subprocess.call(shell, shell=True) #Shell=True okay since input is trusted.
64 64
65 65 #Read output from drive
66 66 if os.path.isfile(output_filename):
67 67 with open(output_filename, 'rb') as f:
68 68 return f.read().encode("base64") #PDF is a nb supported binary
69 69 #data type, so base64 encode.
70 70 else:
71 71 return TypeError("Inkscape svg to png conversion failed")
72 #TODO: ERROR: which notebook crashed exporter
@@ -1,100 +1,91
1 1 """Module containing a transformer that extracts all of the figures from the
2 2 notebook file. The extracted figures are returned in the 'resources' dictionary.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15
16 16 import sys
17 17 from IPython.utils.traitlets import Dict, Unicode
18 18 from .activatable import ActivatableTransformer
19 19
20 20 #-----------------------------------------------------------------------------
21 # Constants
22 #-----------------------------------------------------------------------------
23
24 # win64 is win32 for backwards compatability, for now. See
25 # http://mail.python.org/pipermail/patches/2000-May/000648.html
26 # for the original patch that this decision was made.
27 WINDOWS_PLATFORMS = ['win32']
28
29 #-----------------------------------------------------------------------------
30 21 # Classes
31 22 #-----------------------------------------------------------------------------
32 23
33 24 class ExtractFigureTransformer(ActivatableTransformer):
34 25 """
35 26 Extracts all of the figures from the notebook file. The extracted
36 27 figures are returned in the 'resources' dictionary.
37 28 """
38 29
39 30
40 31 figure_filename_template = Unicode(
41 32 "{unique_key}_{cell_index}_{index}.{extension}", config=True)
42 33
43 34
44 35 def transform_cell(self, cell, resources, cell_index):
45 36 """
46 37 Apply a transformation on each cell,
47 38
48 39 Parameters
49 40 ----------
50 41 cell : NotebookNode cell
51 42 Notebook cell being processed
52 43 resources : dictionary
53 44 Additional resources used in the conversion process. Allows
54 45 transformers to pass variables into the Jinja engine.
55 46 cell_index : int
56 47 Index of the cell being processed (see base.py)
57 48 """
58 49
59 50 #Get the unique key from the resource dict if it exists. If it does not
60 51 #exist, use 'figure' as the default.
61 52 unique_key = resources.get('unique_key', 'figure')
62 53
63 54 #Make sure figures key exists
64 55 if not 'figures' in resources:
65 56 resources['figures'] = {}
66 57
67 58 #Loop through all of the outputs in the cell
68 59 for index, out in enumerate(cell.get('outputs', [])):
69 60
70 61 #Get the output in data formats that the template is interested in.
71 62 for out_type in self.display_data_priority:
72 63 if out.hasattr(out_type):
73 64 data = out[out_type]
74 65
75 66 #Binary files are base64-encoded, SVG is already XML
76 67 if out_type in ('png', 'jpg', 'pdf'):
77 68 data = data.decode('base64')
78 elif sys.platform in WINDOWS_PLATFORMS:
69 elif sys.platform == 'win32':
79 70 data = data.replace('\n', '\r\n').encode("UTF-8")
80 71 else:
81 72 data = data.encode("UTF-8")
82 73
83 74 #Build a figure name
84 75 figure_name = self.figure_filename_template.format(
85 76 unique_key=unique_key,
86 77 cell_index=cell_index,
87 78 index=index,
88 79 extension=out_type)
89 80
90 81 #On the cell, make the figure available via
91 82 # cell.outputs[i].svg_filename ... etc (svg in example)
92 83 # Where
93 84 # cell.outputs[i].svg contains the data
94 85 out[out_type + '_filename'] = figure_name
95 86
96 87 #In the resources, make the figure available via
97 88 # resources['figures']['filename'] = data
98 89 resources['figures'][figure_name] = data
99 90
100 91 return cell, resources
@@ -1,261 +1,261
1 1 """Module that allows custom Sphinx parameters to be set on the notebook and
2 2 on the 'other' object passed into Jinja. Called prior to Jinja conversion
3 3 process.
4 4 """
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 from __future__ import print_function, absolute_import
18 18
19 19 # Stdlib imports
20 20 # Used to find Sphinx package location
21 21 import sphinx
22 22 import os.path
23 23
24 24 # Used to set the default date to today's date
25 25 from datetime import date
26 26
27 27 # Third-party imports
28 28 # Needed for Pygments latex definitions.
29 29 from pygments.formatters import LatexFormatter
30 30
31 31 # Our own imports
32 32 # Configurable traitlets
33 33 from IPython.utils.traitlets import Unicode, Bool
34 34
35 35 # Needed to override transformer
36 36 from .activatable import (ActivatableTransformer) #TODO
37 37
38 38 from IPython.nbconvert.utils import console
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Classes and functions
42 42 #-----------------------------------------------------------------------------
43 43
44 44 class SphinxTransformer(ActivatableTransformer):
45 45 """
46 46 Sphinx utility transformer.
47 47
48 48 This transformer is used to set variables needed by the latex to build
49 49 Sphinx stylized templates.
50 50 """
51 51
52 52 interactive = Bool(False, config=True, help="""
53 53 Allows you to define whether or not the Sphinx exporter will prompt
54 54 you for input during the conversion process. If this is set to false,
55 55 the author, version, release, date, and chapter_style traits should
56 56 be set.
57 57 """)
58 58
59 59 author = Unicode("Unknown Author", config=True, help="Author name")
60 60
61 61 version = Unicode("", config=True, help="""
62 62 Version number
63 63 You can leave this blank if you do not want to render a version number.
64 64 Example: "1.0.0"
65 65 """)
66 66
67 67 release = Unicode("", config=True, help="""
68 68 Release name
69 69 You can leave this blank if you do not want to render a release name.
70 70 Example: "Rough Draft"
71 71 """)
72 72
73 73 publish_date = Unicode("", config=True, help="""
74 74 Publish date
75 75 This is the date to render on the document as the publish date.
76 76 Leave this blank to default to todays date.
77 77 Example: "June 12, 1990"
78 78 """)
79 79
80 80 chapter_style = Unicode("Bjarne", config=True, help="""
81 81 Sphinx chapter style
82 82 This is the style to use for the chapter headers in the document.
83 83 You may choose one of the following:
84 84 "Bjarne" (default)
85 85 "Lenny"
86 86 "Glenn"
87 87 "Conny"
88 88 "Rejne"
89 89 "Sonny" (used for international documents)
90 90 """)
91 91
92 92 output_style = Unicode("notebook", config=True, help="""
93 93 Nbconvert Ipython
94 94 notebook input/output formatting style.
95 95 You may choose one of the following:
96 96 "simple (recommended for long code segments)"
97 97 "notebook" (default)
98 98 """)
99 99
100 100 center_output = Bool(False, config=True, help="""
101 101 Optional attempt to center all output. If this is false, no additional
102 102 formatting is applied.
103 103 """)
104 104
105 105 use_headers = Bool(True, config=True, help="""
106 106 Whether not a header should be added to the document.
107 107 """)
108 108
109 109 #Allow the user to override the title of the notebook (useful for
110 110 #fancy document titles that the file system doesn't support.)
111 111 overridetitle = Unicode("", config=True, help="")
112 112
113 113
114 114 def call(self, nb, resources):
115 115 """
116 116 Sphinx transformation to apply on each notebook.
117 117
118 118 Parameters
119 119 ----------
120 120 nb : NotebookNode
121 121 Notebook being converted
122 122 resources : dictionary
123 123 Additional resources used in the conversion process. Allows
124 124 transformers to pass variables into the Jinja engine.
125 125 """
126 126
127 127 # TODO: Add versatile method of additional notebook metadata. Include
128 128 # handling of multiple files. For now use a temporay namespace,
129 129 # '_draft' to signify that this needs to change.
130 130 if not "_draft" in nb.metadata:
131 131 nb.metadata._draft = {}
132
132 #TODO: Remove draft, and nb
133 133 if not "sphinx" in resources:
134 134 resources["sphinx"] = {}
135 135
136 136 if self.interactive:
137 137
138 138 # Prompt the user for additional meta data that doesn't exist currently
139 139 # but would be usefull for Sphinx.
140 140 nb.metadata._draft["author"] = self._prompt_author()
141 141 nb.metadata._draft["version"] = self._prompt_version()
142 142 nb.metadata._draft["release"] = self._prompt_release()
143 143 nb.metadata._draft["date"] = self._prompt_date()
144 144
145 145 # Prompt the user for the document style.
146 146 resources["sphinx"]["chapterstyle"] = self._prompt_chapter_title_style()
147 147 resources["sphinx"]["outputstyle"] = self._prompt_output_style()
148 148
149 149 # Small options
150 150 resources["sphinx"]["centeroutput"] = console.prompt_boolean("Do you want to center the output? (false)", False)
151 151 resources["sphinx"]["header"] = console.prompt_boolean("Should a Sphinx document header be used? (true)", True)
152 152 else:
153 153
154 154 # Try to use the traitlets.
155 155 nb.metadata._draft["author"] = self.author
156 156 nb.metadata._draft["version"] = self.version
157 157 nb.metadata._draft["release"] = self.release
158 158
159 159 # Use todays date if none is provided.
160 160 if len(self.publish_date.strip()) == 0:
161 161 nb.metadata._draft["date"] = date.today().strftime("%B %d, %Y")
162 162 else:
163 163 nb.metadata._draft["date"] = self.publish_date
164 164
165 165 # Sphinx traitlets.
166 166 resources["sphinx"]["chapterstyle"] = self.chapter_style
167 167 resources["sphinx"]["outputstyle"] = self.output_style
168 168 resources["sphinx"]["centeroutput"] = self.center_output
169 169 resources["sphinx"]["header"] = self.use_headers
170 170
171 171 # Find and pass in the path to the Sphinx dependencies.
172 172 resources["sphinx"]["texinputs"] = os.path.realpath(os.path.join(sphinx.__file__, "..", "texinputs"))
173 173
174 174 # Generate Pygments definitions for Latex
175 175 resources["sphinx"]["pygment_definitions"] = self._generate_pygments_latex_def()
176 176
177 177 if not (self.overridetitle == None or len(self.overridetitle.strip()) == 0):
178 178 nb.metadata.name = self.overridetitle
179 179
180 180 # End
181 181 return nb, resources
182 182
183 183
184 184 def _generate_pygments_latex_def(self):
185 185 """
186 186 Generate the pygments latex definitions that allows pygments
187 187 to work in latex.
188 188 """
189 189
190 190 return LatexFormatter().get_style_defs()
191 191
192 192
193 193 def _prompt_author(self):
194 194 """
195 195 Prompt the user to input an Author name
196 196 """
197 197 return console.input("Author name: ")
198 198
199 199
200 200 def _prompt_version(self):
201 201 """
202 202 prompt the user to enter a version number
203 203 """
204 204 return console.input("Version (ie ""1.0.0""): ")
205 205
206 206
207 207 def _prompt_release(self):
208 208 """
209 209 Prompt the user to input a release name
210 210 """
211 211
212 212 return console.input("Release Name (ie ""Rough draft""): ")
213 213
214 214
215 215 def _prompt_date(self):
216 216 """
217 217 Prompt the user to enter a date
218 218 """
219 219
220 220 default_date = date.today().strftime("%B %d, %Y")
221 221 user_date = console.input("Date (deafults to \"" + default_date + "\"): ")
222 222 if len(user_date.strip()) == 0:
223 223 user_date = default_date
224 224 return user_date
225 225
226 226
227 227 def _prompt_output_style(self):
228 228 """
229 229 Prompts the user to pick an IPython output style.
230 230 """
231 231
232 232 # Dictionary of available output styles
233 233 styles = {1: "simple",
234 234 2: "notebook"}
235 235
236 236 #Append comments to the menu when displaying it to the user.
237 237 comments = {1: "(recommended for long code segments)",
238 238 2: "(default)"}
239 239
240 240 return console.prompt_dictionary(styles, default_style=2, menu_comments=comments)
241 241
242 242
243 243 def _prompt_chapter_title_style(self):
244 244 """
245 245 Prompts the user to pick a Sphinx chapter style
246 246 """
247 247
248 248 # Dictionary of available Sphinx styles
249 249 styles = {1: "Bjarne",
250 250 2: "Lenny",
251 251 3: "Glenn",
252 252 4: "Conny",
253 253 5: "Rejne",
254 254 6: "Sonny"}
255 255
256 256 #Append comments to the menu when displaying it to the user.
257 257 comments = {1: "(default)",
258 258 6: "(for international documents)"}
259 259
260 260 return console.prompt_dictionary(styles, menu_comments=comments)
261 261
General Comments 0
You need to be logged in to leave comments. Login now