##// END OF EJS Templates
Moved format date into common space
Jonathan Frederic -
Show More
@@ -1,518 +1,518 b''
1 """This module defines Exporter, a highly configurable converter
1 """This module defines Exporter, a highly configurable converter
2 that uses Jinja2 to export notebook files into different formats.
2 that uses Jinja2 to export notebook files into different formats.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 from __future__ import print_function, absolute_import
17 from __future__ import print_function, absolute_import
18
18
19 # Stdlib imports
19 # Stdlib imports
20 import io
20 import io
21 import os
21 import os
22 import inspect
22 import inspect
23 import copy
23 import copy
24 import collections
24 import collections
25 import datetime
25 import datetime
26
26
27 # other libs/dependencies
27 # other libs/dependencies
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
29
29
30 # IPython imports
30 # IPython imports
31 from IPython.config.configurable import LoggingConfigurable
31 from IPython.config.configurable import LoggingConfigurable
32 from IPython.config import Config
32 from IPython.config import Config
33 from IPython.nbformat import current as nbformat
33 from IPython.nbformat import current as nbformat
34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict, Any
34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict, Any
35 from IPython.utils.importstring import import_item
35 from IPython.utils.importstring import import_item
36 from IPython.utils.text import indent
36 from IPython.utils.text import indent, format_date
37 from IPython.utils import py3compat
37 from IPython.utils import py3compat
38
38
39 from IPython.nbconvert import preprocessors as nbpreprocessors
39 from IPython.nbconvert import preprocessors as nbpreprocessors
40 from IPython.nbconvert import filters
40 from IPython.nbconvert import filters
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Globals and constants
43 # Globals and constants
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 #Jinja2 extensions to load.
46 #Jinja2 extensions to load.
47 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
47 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
48
48
49 default_filters = {
49 default_filters = {
50 'indent': indent,
50 'indent': indent,
51 'markdown2html': filters.markdown2html,
51 'markdown2html': filters.markdown2html,
52 'ansi2html': filters.ansi2html,
52 'ansi2html': filters.ansi2html,
53 'filter_data_type': filters.DataTypeFilter,
53 'filter_data_type': filters.DataTypeFilter,
54 'get_lines': filters.get_lines,
54 'get_lines': filters.get_lines,
55 'highlight2html': filters.highlight2html,
55 'highlight2html': filters.highlight2html,
56 'highlight2latex': filters.highlight2latex,
56 'highlight2latex': filters.highlight2latex,
57 'ipython2python': filters.ipython2python,
57 'ipython2python': filters.ipython2python,
58 'posix_path': filters.posix_path,
58 'posix_path': filters.posix_path,
59 'markdown2latex': filters.markdown2latex,
59 'markdown2latex': filters.markdown2latex,
60 'markdown2rst': filters.markdown2rst,
60 'markdown2rst': filters.markdown2rst,
61 'comment_lines': filters.comment_lines,
61 'comment_lines': filters.comment_lines,
62 'strip_ansi': filters.strip_ansi,
62 'strip_ansi': filters.strip_ansi,
63 'strip_dollars': filters.strip_dollars,
63 'strip_dollars': filters.strip_dollars,
64 'strip_files_prefix': filters.strip_files_prefix,
64 'strip_files_prefix': filters.strip_files_prefix,
65 'html2text' : filters.html2text,
65 'html2text' : filters.html2text,
66 'add_anchor': filters.add_anchor,
66 'add_anchor': filters.add_anchor,
67 'ansi2latex': filters.ansi2latex,
67 'ansi2latex': filters.ansi2latex,
68 'strip_math_space': filters.strip_math_space,
68 'strip_math_space': filters.strip_math_space,
69 'wrap_text': filters.wrap_text,
69 'wrap_text': filters.wrap_text,
70 'escape_latex': filters.escape_latex,
70 'escape_latex': filters.escape_latex,
71 }
71 }
72
72
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74 # Class
74 # Class
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76
76
77 class ResourcesDict(collections.defaultdict):
77 class ResourcesDict(collections.defaultdict):
78 def __missing__(self, key):
78 def __missing__(self, key):
79 return ''
79 return ''
80
80
81
81
82 class Exporter(LoggingConfigurable):
82 class Exporter(LoggingConfigurable):
83 """
83 """
84 Exports notebooks into other file formats. Uses Jinja 2 templating engine
84 Exports notebooks into other file formats. Uses Jinja 2 templating engine
85 to output new formats. Inherit from this class if you are creating a new
85 to output new formats. Inherit from this class if you are creating a new
86 template type along with new filters/preprocessors. If the filters/
86 template type along with new filters/preprocessors. If the filters/
87 preprocessors provided by default suffice, there is no need to inherit from
87 preprocessors provided by default suffice, there is no need to inherit from
88 this class. Instead, override the template_file and file_extension
88 this class. Instead, override the template_file and file_extension
89 traits via a config file.
89 traits via a config file.
90
90
91 {filters}
91 {filters}
92 """
92 """
93
93
94 # finish the docstring
94 # finish the docstring
95 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
95 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
96
96
97
97
98 template_file = Unicode(u'default',
98 template_file = Unicode(u'default',
99 config=True,
99 config=True,
100 help="Name of the template file to use")
100 help="Name of the template file to use")
101 def _template_file_changed(self, name, old, new):
101 def _template_file_changed(self, name, old, new):
102 if new=='default':
102 if new=='default':
103 self.template_file = self.default_template
103 self.template_file = self.default_template
104 else:
104 else:
105 self.template_file = new
105 self.template_file = new
106 self.template = None
106 self.template = None
107 self._load_template()
107 self._load_template()
108
108
109 default_template = Unicode(u'')
109 default_template = Unicode(u'')
110 template = Any()
110 template = Any()
111 environment = Any()
111 environment = Any()
112
112
113 file_extension = Unicode(
113 file_extension = Unicode(
114 'txt', config=True,
114 'txt', config=True,
115 help="Extension of the file that should be written to disk"
115 help="Extension of the file that should be written to disk"
116 )
116 )
117
117
118 template_path = List(['.'], config=True)
118 template_path = List(['.'], config=True)
119 def _template_path_changed(self, name, old, new):
119 def _template_path_changed(self, name, old, new):
120 self._load_template()
120 self._load_template()
121
121
122 default_template_path = Unicode(
122 default_template_path = Unicode(
123 os.path.join("..", "templates"),
123 os.path.join("..", "templates"),
124 help="Path where the template files are located.")
124 help="Path where the template files are located.")
125
125
126 template_skeleton_path = Unicode(
126 template_skeleton_path = Unicode(
127 os.path.join("..", "templates", "skeleton"),
127 os.path.join("..", "templates", "skeleton"),
128 help="Path where the template skeleton files are located.")
128 help="Path where the template skeleton files are located.")
129
129
130 #Jinja block definitions
130 #Jinja block definitions
131 jinja_comment_block_start = Unicode("", config=True)
131 jinja_comment_block_start = Unicode("", config=True)
132 jinja_comment_block_end = Unicode("", config=True)
132 jinja_comment_block_end = Unicode("", config=True)
133 jinja_variable_block_start = Unicode("", config=True)
133 jinja_variable_block_start = Unicode("", config=True)
134 jinja_variable_block_end = Unicode("", config=True)
134 jinja_variable_block_end = Unicode("", config=True)
135 jinja_logic_block_start = Unicode("", config=True)
135 jinja_logic_block_start = Unicode("", config=True)
136 jinja_logic_block_end = Unicode("", config=True)
136 jinja_logic_block_end = Unicode("", config=True)
137
137
138 #Extension that the template files use.
138 #Extension that the template files use.
139 template_extension = Unicode(".tpl", config=True)
139 template_extension = Unicode(".tpl", config=True)
140
140
141 #Configurability, allows the user to easily add filters and preprocessors.
141 #Configurability, allows the user to easily add filters and preprocessors.
142 preprocessors = List(config=True,
142 preprocessors = List(config=True,
143 help="""List of preprocessors, by name or namespace, to enable.""")
143 help="""List of preprocessors, by name or namespace, to enable.""")
144
144
145 filters = Dict(config=True,
145 filters = Dict(config=True,
146 help="""Dictionary of filters, by name and namespace, to add to the Jinja
146 help="""Dictionary of filters, by name and namespace, to add to the Jinja
147 environment.""")
147 environment.""")
148
148
149 default_preprocessors = List([nbpreprocessors.coalesce_streams,
149 default_preprocessors = List([nbpreprocessors.coalesce_streams,
150 nbpreprocessors.SVG2PDFPreprocessor,
150 nbpreprocessors.SVG2PDFPreprocessor,
151 nbpreprocessors.ExtractOutputPreprocessor,
151 nbpreprocessors.ExtractOutputPreprocessor,
152 nbpreprocessors.CSSHTMLHeaderPreprocessor,
152 nbpreprocessors.CSSHTMLHeaderPreprocessor,
153 nbpreprocessors.RevealHelpPreprocessor,
153 nbpreprocessors.RevealHelpPreprocessor,
154 nbpreprocessors.LatexPreprocessor,
154 nbpreprocessors.LatexPreprocessor,
155 nbpreprocessors.SphinxPreprocessor],
155 nbpreprocessors.SphinxPreprocessor],
156 config=True,
156 config=True,
157 help="""List of preprocessors available by default, by name, namespace,
157 help="""List of preprocessors available by default, by name, namespace,
158 instance, or type.""")
158 instance, or type.""")
159
159
160
160
161 def __init__(self, config=None, extra_loaders=None, **kw):
161 def __init__(self, config=None, extra_loaders=None, **kw):
162 """
162 """
163 Public constructor
163 Public constructor
164
164
165 Parameters
165 Parameters
166 ----------
166 ----------
167 config : config
167 config : config
168 User configuration instance.
168 User configuration instance.
169 extra_loaders : list[of Jinja Loaders]
169 extra_loaders : list[of Jinja Loaders]
170 ordered list of Jinja loader to find templates. Will be tried in order
170 ordered list of Jinja loader to find templates. Will be tried in order
171 before the default FileSystem ones.
171 before the default FileSystem ones.
172 template : str (optional, kw arg)
172 template : str (optional, kw arg)
173 Template to use when exporting.
173 Template to use when exporting.
174 """
174 """
175 if not config:
175 if not config:
176 config = self.default_config
176 config = self.default_config
177
177
178 super(Exporter, self).__init__(config=config, **kw)
178 super(Exporter, self).__init__(config=config, **kw)
179
179
180 #Init
180 #Init
181 self._init_template()
181 self._init_template()
182 self._init_environment(extra_loaders=extra_loaders)
182 self._init_environment(extra_loaders=extra_loaders)
183 self._init_preprocessors()
183 self._init_preprocessors()
184 self._init_filters()
184 self._init_filters()
185
185
186
186
187 @property
187 @property
188 def default_config(self):
188 def default_config(self):
189 return Config()
189 return Config()
190
190
191 def _config_changed(self, name, old, new):
191 def _config_changed(self, name, old, new):
192 """When setting config, make sure to start with our default_config"""
192 """When setting config, make sure to start with our default_config"""
193 c = self.default_config
193 c = self.default_config
194 if new:
194 if new:
195 c.merge(new)
195 c.merge(new)
196 if c != old:
196 if c != old:
197 self.config = c
197 self.config = c
198 super(Exporter, self)._config_changed(name, old, c)
198 super(Exporter, self)._config_changed(name, old, c)
199
199
200
200
201 def _load_template(self):
201 def _load_template(self):
202 """Load the Jinja template object from the template file
202 """Load the Jinja template object from the template file
203
203
204 This is a no-op if the template attribute is already defined,
204 This is a no-op if the template attribute is already defined,
205 or the Jinja environment is not setup yet.
205 or the Jinja environment is not setup yet.
206
206
207 This is triggered by various trait changes that would change the template.
207 This is triggered by various trait changes that would change the template.
208 """
208 """
209 if self.template is not None:
209 if self.template is not None:
210 return
210 return
211 # called too early, do nothing
211 # called too early, do nothing
212 if self.environment is None:
212 if self.environment is None:
213 return
213 return
214 # Try different template names during conversion. First try to load the
214 # Try different template names during conversion. First try to load the
215 # template by name with extension added, then try loading the template
215 # template by name with extension added, then try loading the template
216 # as if the name is explicitly specified, then try the name as a
216 # as if the name is explicitly specified, then try the name as a
217 # 'flavor', and lastly just try to load the template by module name.
217 # 'flavor', and lastly just try to load the template by module name.
218 module_name = self.__module__.rsplit('.', 1)[-1]
218 module_name = self.__module__.rsplit('.', 1)[-1]
219 try_names = []
219 try_names = []
220 if self.template_file:
220 if self.template_file:
221 try_names.extend([
221 try_names.extend([
222 self.template_file + self.template_extension,
222 self.template_file + self.template_extension,
223 self.template_file,
223 self.template_file,
224 module_name + '_' + self.template_file + self.template_extension,
224 module_name + '_' + self.template_file + self.template_extension,
225 ])
225 ])
226 try_names.append(module_name + self.template_extension)
226 try_names.append(module_name + self.template_extension)
227 for try_name in try_names:
227 for try_name in try_names:
228 self.log.debug("Attempting to load template %s", try_name)
228 self.log.debug("Attempting to load template %s", try_name)
229 try:
229 try:
230 self.template = self.environment.get_template(try_name)
230 self.template = self.environment.get_template(try_name)
231 except (TemplateNotFound, IOError):
231 except (TemplateNotFound, IOError):
232 pass
232 pass
233 except Exception as e:
233 except Exception as e:
234 self.log.warn("Unexpected exception loading template: %s", try_name, exc_info=True)
234 self.log.warn("Unexpected exception loading template: %s", try_name, exc_info=True)
235 else:
235 else:
236 self.log.info("Loaded template %s", try_name)
236 self.log.info("Loaded template %s", try_name)
237 break
237 break
238
238
239 def from_notebook_node(self, nb, resources=None, **kw):
239 def from_notebook_node(self, nb, resources=None, **kw):
240 """
240 """
241 Convert a notebook from a notebook node instance.
241 Convert a notebook from a notebook node instance.
242
242
243 Parameters
243 Parameters
244 ----------
244 ----------
245 nb : Notebook node
245 nb : Notebook node
246 resources : dict (**kw)
246 resources : dict (**kw)
247 of additional resources that can be accessed read/write by
247 of additional resources that can be accessed read/write by
248 preprocessors and filters.
248 preprocessors and filters.
249 """
249 """
250 nb_copy = copy.deepcopy(nb)
250 nb_copy = copy.deepcopy(nb)
251 resources = self._init_resources(resources)
251 resources = self._init_resources(resources)
252
252
253 # Preprocess
253 # Preprocess
254 nb_copy, resources = self._preprocess(nb_copy, resources)
254 nb_copy, resources = self._preprocess(nb_copy, resources)
255
255
256 self._load_template()
256 self._load_template()
257
257
258 if self.template is not None:
258 if self.template is not None:
259 output = self.template.render(nb=nb_copy, resources=resources)
259 output = self.template.render(nb=nb_copy, resources=resources)
260 else:
260 else:
261 raise IOError('template file "%s" could not be found' % self.template_file)
261 raise IOError('template file "%s" could not be found' % self.template_file)
262 return output, resources
262 return output, resources
263
263
264
264
265 def from_filename(self, filename, resources=None, **kw):
265 def from_filename(self, filename, resources=None, **kw):
266 """
266 """
267 Convert a notebook from a notebook file.
267 Convert a notebook from a notebook file.
268
268
269 Parameters
269 Parameters
270 ----------
270 ----------
271 filename : str
271 filename : str
272 Full filename of the notebook file to open and convert.
272 Full filename of the notebook file to open and convert.
273 """
273 """
274
274
275 #Pull the metadata from the filesystem.
275 #Pull the metadata from the filesystem.
276 if resources is None:
276 if resources is None:
277 resources = ResourcesDict()
277 resources = ResourcesDict()
278 if not 'metadata' in resources or resources['metadata'] == '':
278 if not 'metadata' in resources or resources['metadata'] == '':
279 resources['metadata'] = ResourcesDict()
279 resources['metadata'] = ResourcesDict()
280 basename = os.path.basename(filename)
280 basename = os.path.basename(filename)
281 notebook_name = basename[:basename.rfind('.')]
281 notebook_name = basename[:basename.rfind('.')]
282 resources['metadata']['name'] = notebook_name
282 resources['metadata']['name'] = notebook_name
283
283
284 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
284 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
285 resources['metadata']['modified_date'] = modified_date.strftime("%B %d, %Y")
285 resources['metadata']['modified_date'] = format_date(modified_date)
286
286
287 with io.open(filename) as f:
287 with io.open(filename) as f:
288 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
288 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
289
289
290
290
291 def from_file(self, file_stream, resources=None, **kw):
291 def from_file(self, file_stream, resources=None, **kw):
292 """
292 """
293 Convert a notebook from a notebook file.
293 Convert a notebook from a notebook file.
294
294
295 Parameters
295 Parameters
296 ----------
296 ----------
297 file_stream : file-like object
297 file_stream : file-like object
298 Notebook file-like object to convert.
298 Notebook file-like object to convert.
299 """
299 """
300 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
300 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
301
301
302
302
303 def register_preprocessor(self, preprocessor, enabled=False):
303 def register_preprocessor(self, preprocessor, enabled=False):
304 """
304 """
305 Register a preprocessor.
305 Register a preprocessor.
306 Preprocessors are classes that act upon the notebook before it is
306 Preprocessors are classes that act upon the notebook before it is
307 passed into the Jinja templating engine. Preprocessors are also
307 passed into the Jinja templating engine. Preprocessors are also
308 capable of passing additional information to the Jinja
308 capable of passing additional information to the Jinja
309 templating engine.
309 templating engine.
310
310
311 Parameters
311 Parameters
312 ----------
312 ----------
313 preprocessor : preprocessor
313 preprocessor : preprocessor
314 """
314 """
315 if preprocessor is None:
315 if preprocessor is None:
316 raise TypeError('preprocessor')
316 raise TypeError('preprocessor')
317 isclass = isinstance(preprocessor, type)
317 isclass = isinstance(preprocessor, type)
318 constructed = not isclass
318 constructed = not isclass
319
319
320 #Handle preprocessor's registration based on it's type
320 #Handle preprocessor's registration based on it's type
321 if constructed and isinstance(preprocessor, py3compat.string_types):
321 if constructed and isinstance(preprocessor, py3compat.string_types):
322 #Preprocessor is a string, import the namespace and recursively call
322 #Preprocessor is a string, import the namespace and recursively call
323 #this register_preprocessor method
323 #this register_preprocessor method
324 preprocessor_cls = import_item(preprocessor)
324 preprocessor_cls = import_item(preprocessor)
325 return self.register_preprocessor(preprocessor_cls, enabled)
325 return self.register_preprocessor(preprocessor_cls, enabled)
326
326
327 if constructed and hasattr(preprocessor, '__call__'):
327 if constructed and hasattr(preprocessor, '__call__'):
328 #Preprocessor is a function, no need to construct it.
328 #Preprocessor is a function, no need to construct it.
329 #Register and return the preprocessor.
329 #Register and return the preprocessor.
330 if enabled:
330 if enabled:
331 preprocessor.enabled = True
331 preprocessor.enabled = True
332 self._preprocessors.append(preprocessor)
332 self._preprocessors.append(preprocessor)
333 return preprocessor
333 return preprocessor
334
334
335 elif isclass and isinstance(preprocessor, MetaHasTraits):
335 elif isclass and isinstance(preprocessor, MetaHasTraits):
336 #Preprocessor is configurable. Make sure to pass in new default for
336 #Preprocessor is configurable. Make sure to pass in new default for
337 #the enabled flag if one was specified.
337 #the enabled flag if one was specified.
338 self.register_preprocessor(preprocessor(parent=self), enabled)
338 self.register_preprocessor(preprocessor(parent=self), enabled)
339
339
340 elif isclass:
340 elif isclass:
341 #Preprocessor is not configurable, construct it
341 #Preprocessor is not configurable, construct it
342 self.register_preprocessor(preprocessor(), enabled)
342 self.register_preprocessor(preprocessor(), enabled)
343
343
344 else:
344 else:
345 #Preprocessor is an instance of something without a __call__
345 #Preprocessor is an instance of something without a __call__
346 #attribute.
346 #attribute.
347 raise TypeError('preprocessor')
347 raise TypeError('preprocessor')
348
348
349
349
350 def register_filter(self, name, jinja_filter):
350 def register_filter(self, name, jinja_filter):
351 """
351 """
352 Register a filter.
352 Register a filter.
353 A filter is a function that accepts and acts on one string.
353 A filter is a function that accepts and acts on one string.
354 The filters are accesible within the Jinja templating engine.
354 The filters are accesible within the Jinja templating engine.
355
355
356 Parameters
356 Parameters
357 ----------
357 ----------
358 name : str
358 name : str
359 name to give the filter in the Jinja engine
359 name to give the filter in the Jinja engine
360 filter : filter
360 filter : filter
361 """
361 """
362 if jinja_filter is None:
362 if jinja_filter is None:
363 raise TypeError('filter')
363 raise TypeError('filter')
364 isclass = isinstance(jinja_filter, type)
364 isclass = isinstance(jinja_filter, type)
365 constructed = not isclass
365 constructed = not isclass
366
366
367 #Handle filter's registration based on it's type
367 #Handle filter's registration based on it's type
368 if constructed and isinstance(jinja_filter, py3compat.string_types):
368 if constructed and isinstance(jinja_filter, py3compat.string_types):
369 #filter is a string, import the namespace and recursively call
369 #filter is a string, import the namespace and recursively call
370 #this register_filter method
370 #this register_filter method
371 filter_cls = import_item(jinja_filter)
371 filter_cls = import_item(jinja_filter)
372 return self.register_filter(name, filter_cls)
372 return self.register_filter(name, filter_cls)
373
373
374 if constructed and hasattr(jinja_filter, '__call__'):
374 if constructed and hasattr(jinja_filter, '__call__'):
375 #filter is a function, no need to construct it.
375 #filter is a function, no need to construct it.
376 self.environment.filters[name] = jinja_filter
376 self.environment.filters[name] = jinja_filter
377 return jinja_filter
377 return jinja_filter
378
378
379 elif isclass and isinstance(jinja_filter, MetaHasTraits):
379 elif isclass and isinstance(jinja_filter, MetaHasTraits):
380 #filter is configurable. Make sure to pass in new default for
380 #filter is configurable. Make sure to pass in new default for
381 #the enabled flag if one was specified.
381 #the enabled flag if one was specified.
382 filter_instance = jinja_filter(parent=self)
382 filter_instance = jinja_filter(parent=self)
383 self.register_filter(name, filter_instance )
383 self.register_filter(name, filter_instance )
384
384
385 elif isclass:
385 elif isclass:
386 #filter is not configurable, construct it
386 #filter is not configurable, construct it
387 filter_instance = jinja_filter()
387 filter_instance = jinja_filter()
388 self.register_filter(name, filter_instance)
388 self.register_filter(name, filter_instance)
389
389
390 else:
390 else:
391 #filter is an instance of something without a __call__
391 #filter is an instance of something without a __call__
392 #attribute.
392 #attribute.
393 raise TypeError('filter')
393 raise TypeError('filter')
394
394
395
395
396 def _init_template(self):
396 def _init_template(self):
397 """
397 """
398 Make sure a template name is specified. If one isn't specified, try to
398 Make sure a template name is specified. If one isn't specified, try to
399 build one from the information we know.
399 build one from the information we know.
400 """
400 """
401 self._template_file_changed('template_file', self.template_file, self.template_file)
401 self._template_file_changed('template_file', self.template_file, self.template_file)
402
402
403
403
404 def _init_environment(self, extra_loaders=None):
404 def _init_environment(self, extra_loaders=None):
405 """
405 """
406 Create the Jinja templating environment.
406 Create the Jinja templating environment.
407 """
407 """
408 here = os.path.dirname(os.path.realpath(__file__))
408 here = os.path.dirname(os.path.realpath(__file__))
409 loaders = []
409 loaders = []
410 if extra_loaders:
410 if extra_loaders:
411 loaders.extend(extra_loaders)
411 loaders.extend(extra_loaders)
412
412
413 paths = self.template_path
413 paths = self.template_path
414 paths.extend([os.path.join(here, self.default_template_path),
414 paths.extend([os.path.join(here, self.default_template_path),
415 os.path.join(here, self.template_skeleton_path)])
415 os.path.join(here, self.template_skeleton_path)])
416 loaders.append(FileSystemLoader(paths))
416 loaders.append(FileSystemLoader(paths))
417
417
418 self.environment = Environment(
418 self.environment = Environment(
419 loader= ChoiceLoader(loaders),
419 loader= ChoiceLoader(loaders),
420 extensions=JINJA_EXTENSIONS
420 extensions=JINJA_EXTENSIONS
421 )
421 )
422
422
423 #Set special Jinja2 syntax that will not conflict with latex.
423 #Set special Jinja2 syntax that will not conflict with latex.
424 if self.jinja_logic_block_start:
424 if self.jinja_logic_block_start:
425 self.environment.block_start_string = self.jinja_logic_block_start
425 self.environment.block_start_string = self.jinja_logic_block_start
426 if self.jinja_logic_block_end:
426 if self.jinja_logic_block_end:
427 self.environment.block_end_string = self.jinja_logic_block_end
427 self.environment.block_end_string = self.jinja_logic_block_end
428 if self.jinja_variable_block_start:
428 if self.jinja_variable_block_start:
429 self.environment.variable_start_string = self.jinja_variable_block_start
429 self.environment.variable_start_string = self.jinja_variable_block_start
430 if self.jinja_variable_block_end:
430 if self.jinja_variable_block_end:
431 self.environment.variable_end_string = self.jinja_variable_block_end
431 self.environment.variable_end_string = self.jinja_variable_block_end
432 if self.jinja_comment_block_start:
432 if self.jinja_comment_block_start:
433 self.environment.comment_start_string = self.jinja_comment_block_start
433 self.environment.comment_start_string = self.jinja_comment_block_start
434 if self.jinja_comment_block_end:
434 if self.jinja_comment_block_end:
435 self.environment.comment_end_string = self.jinja_comment_block_end
435 self.environment.comment_end_string = self.jinja_comment_block_end
436
436
437
437
438 def _init_preprocessors(self):
438 def _init_preprocessors(self):
439 """
439 """
440 Register all of the preprocessors needed for this exporter, disabled
440 Register all of the preprocessors needed for this exporter, disabled
441 unless specified explicitly.
441 unless specified explicitly.
442 """
442 """
443 self._preprocessors = []
443 self._preprocessors = []
444
444
445 #Load default preprocessors (not necessarly enabled by default).
445 #Load default preprocessors (not necessarly enabled by default).
446 if self.default_preprocessors:
446 if self.default_preprocessors:
447 for preprocessor in self.default_preprocessors:
447 for preprocessor in self.default_preprocessors:
448 self.register_preprocessor(preprocessor)
448 self.register_preprocessor(preprocessor)
449
449
450 #Load user preprocessors. Enable by default.
450 #Load user preprocessors. Enable by default.
451 if self.preprocessors:
451 if self.preprocessors:
452 for preprocessor in self.preprocessors:
452 for preprocessor in self.preprocessors:
453 self.register_preprocessor(preprocessor, enabled=True)
453 self.register_preprocessor(preprocessor, enabled=True)
454
454
455
455
456 def _init_filters(self):
456 def _init_filters(self):
457 """
457 """
458 Register all of the filters required for the exporter.
458 Register all of the filters required for the exporter.
459 """
459 """
460
460
461 #Add default filters to the Jinja2 environment
461 #Add default filters to the Jinja2 environment
462 for key, value in default_filters.items():
462 for key, value in default_filters.items():
463 self.register_filter(key, value)
463 self.register_filter(key, value)
464
464
465 #Load user filters. Overwrite existing filters if need be.
465 #Load user filters. Overwrite existing filters if need be.
466 if self.filters:
466 if self.filters:
467 for key, user_filter in self.filters.items():
467 for key, user_filter in self.filters.items():
468 self.register_filter(key, user_filter)
468 self.register_filter(key, user_filter)
469
469
470
470
471 def _init_resources(self, resources):
471 def _init_resources(self, resources):
472
472
473 #Make sure the resources dict is of ResourcesDict type.
473 #Make sure the resources dict is of ResourcesDict type.
474 if resources is None:
474 if resources is None:
475 resources = ResourcesDict()
475 resources = ResourcesDict()
476 if not isinstance(resources, ResourcesDict):
476 if not isinstance(resources, ResourcesDict):
477 new_resources = ResourcesDict()
477 new_resources = ResourcesDict()
478 new_resources.update(resources)
478 new_resources.update(resources)
479 resources = new_resources
479 resources = new_resources
480
480
481 #Make sure the metadata extension exists in resources
481 #Make sure the metadata extension exists in resources
482 if 'metadata' in resources:
482 if 'metadata' in resources:
483 if not isinstance(resources['metadata'], ResourcesDict):
483 if not isinstance(resources['metadata'], ResourcesDict):
484 resources['metadata'] = ResourcesDict(resources['metadata'])
484 resources['metadata'] = ResourcesDict(resources['metadata'])
485 else:
485 else:
486 resources['metadata'] = ResourcesDict()
486 resources['metadata'] = ResourcesDict()
487 if not resources['metadata']['name']:
487 if not resources['metadata']['name']:
488 resources['metadata']['name'] = 'Notebook'
488 resources['metadata']['name'] = 'Notebook'
489
489
490 #Set the output extension
490 #Set the output extension
491 resources['output_extension'] = self.file_extension
491 resources['output_extension'] = self.file_extension
492 return resources
492 return resources
493
493
494
494
495 def _preprocess(self, nb, resources):
495 def _preprocess(self, nb, resources):
496 """
496 """
497 Preprocess the notebook before passing it into the Jinja engine.
497 Preprocess the notebook before passing it into the Jinja engine.
498 To preprocess the notebook is to apply all of the
498 To preprocess the notebook is to apply all of the
499
499
500 Parameters
500 Parameters
501 ----------
501 ----------
502 nb : notebook node
502 nb : notebook node
503 notebook that is being exported.
503 notebook that is being exported.
504 resources : a dict of additional resources that
504 resources : a dict of additional resources that
505 can be accessed read/write by preprocessors
505 can be accessed read/write by preprocessors
506 and filters.
506 and filters.
507 """
507 """
508
508
509 # Do a copy.deepcopy first,
509 # Do a copy.deepcopy first,
510 # we are never safe enough with what the preprocessors could do.
510 # we are never safe enough with what the preprocessors could do.
511 nbc = copy.deepcopy(nb)
511 nbc = copy.deepcopy(nb)
512 resc = copy.deepcopy(resources)
512 resc = copy.deepcopy(resources)
513
513
514 #Run each preprocessor on the notebook. Carry the output along
514 #Run each preprocessor on the notebook. Carry the output along
515 #to each preprocessor
515 #to each preprocessor
516 for preprocessor in self._preprocessors:
516 for preprocessor in self._preprocessors:
517 nbc, resc = preprocessor(nbc, resc)
517 nbc, resc = preprocessor(nbc, resc)
518 return nbc, resc
518 return nbc, resc
@@ -1,270 +1,265 b''
1 """Module that allows custom Sphinx parameters to be set on the notebook and
1 """Module that allows custom Sphinx parameters to be set on the notebook and
2 on the 'other' object passed into Jinja. Called prior to Jinja conversion
2 on the 'other' object passed into Jinja. Called prior to Jinja conversion
3 process.
3 process.
4 """
4 """
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 from __future__ import print_function, absolute_import
17 from __future__ import print_function, absolute_import
18
18
19 # Stdlib imports
19 # Stdlib imports
20 import os.path
20 import os.path
21 import sys
21 import sys
22
22
23 # Used to set the default date to today's date
23 # Used to set the default date to today's date
24 from datetime import date
24 from datetime import date
25
25
26 # Third-party imports
26 # Third-party imports
27 # Needed for Pygments latex definitions.
27 # Needed for Pygments latex definitions.
28 from pygments.formatters import LatexFormatter
28 from pygments.formatters import LatexFormatter
29
29
30 # Our own imports
30 # Our own imports
31 # Configurable traitlets
31 # Configurable traitlets
32 from IPython.utils.traitlets import Unicode, Bool
32 from IPython.utils.traitlets import Unicode, Bool
33 from IPython.utils.text import format_date
33
34
34 # Needed to override preprocessor
35 # Needed to override preprocessor
35 from .base import (Preprocessor)
36 from .base import (Preprocessor)
36
37
37 from IPython.nbconvert.utils import console
38 from IPython.nbconvert.utils import console
38
39
39 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
40 # Classes and functions
41 # Classes and functions
41 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
42
43
43 class SphinxPreprocessor(Preprocessor):
44 class SphinxPreprocessor(Preprocessor):
44 """
45 """
45 Sphinx utility preprocessor.
46 Sphinx utility preprocessor.
46
47
47 This preprocessor is used to set variables needed by the latex to build
48 This preprocessor is used to set variables needed by the latex to build
48 Sphinx stylized templates.
49 Sphinx stylized templates.
49 """
50 """
50
51
51 interactive = Bool(False, config=True, help="""
52 interactive = Bool(False, config=True, help="""
52 Allows you to define whether or not the Sphinx exporter will prompt
53 Allows you to define whether or not the Sphinx exporter will prompt
53 you for input during the conversion process. If this is set to false,
54 you for input during the conversion process. If this is set to false,
54 the author, version, release, date, and chapter_style traits should
55 the author, version, release, date, and chapter_style traits should
55 be set.
56 be set.
56 """)
57 """)
57
58
58 author = Unicode("Unknown Author", config=True, help="Author name")
59 author = Unicode("Unknown Author", config=True, help="Author name")
59
60
60 version = Unicode("", config=True, help="""
61 version = Unicode("", config=True, help="""
61 Version number
62 Version number
62 You can leave this blank if you do not want to render a version number.
63 You can leave this blank if you do not want to render a version number.
63 Example: "1.0.0"
64 Example: "1.0.0"
64 """)
65 """)
65
66
66 release = Unicode("", config=True, help="""
67 release = Unicode("", config=True, help="""
67 Release name
68 Release name
68 You can leave this blank if you do not want to render a release name.
69 You can leave this blank if you do not want to render a release name.
69 Example: "Rough Draft"
70 Example: "Rough Draft"
70 """)
71 """)
71
72
72 publish_date = Unicode("", config=True, help="""
73 publish_date = Unicode("", config=True, help="""
73 Publish date
74 Publish date
74 This is the date to render on the document as the publish date.
75 This is the date to render on the document as the publish date.
75 Leave this blank to default to todays date.
76 Leave this blank to default to todays date.
76 Example: "June 12, 1990"
77 Example: "June 12, 1990"
77 """)
78 """)
78
79
79 chapter_style = Unicode("Bjarne", config=True, help="""
80 chapter_style = Unicode("Bjarne", config=True, help="""
80 Sphinx chapter style
81 Sphinx chapter style
81 This is the style to use for the chapter headers in the document.
82 This is the style to use for the chapter headers in the document.
82 You may choose one of the following:
83 You may choose one of the following:
83 "Bjarne" (default)
84 "Bjarne" (default)
84 "Lenny"
85 "Lenny"
85 "Glenn"
86 "Glenn"
86 "Conny"
87 "Conny"
87 "Rejne"
88 "Rejne"
88 "Sonny" (used for international documents)
89 "Sonny" (used for international documents)
89 """)
90 """)
90
91
91 output_style = Unicode("notebook", config=True, help="""
92 output_style = Unicode("notebook", config=True, help="""
92 Nbconvert Ipython
93 Nbconvert Ipython
93 notebook input/output formatting style.
94 notebook input/output formatting style.
94 You may choose one of the following:
95 You may choose one of the following:
95 "simple (recommended for long code segments)"
96 "simple (recommended for long code segments)"
96 "notebook" (default)
97 "notebook" (default)
97 """)
98 """)
98
99
99 center_output = Bool(False, config=True, help="""
100 center_output = Bool(False, config=True, help="""
100 Optional attempt to center all output. If this is false, no additional
101 Optional attempt to center all output. If this is false, no additional
101 formatting is applied.
102 formatting is applied.
102 """)
103 """)
103
104
104 use_headers = Bool(True, config=True, help="""
105 use_headers = Bool(True, config=True, help="""
105 Whether not a header should be added to the document.
106 Whether not a header should be added to the document.
106 """)
107 """)
107
108
108 #Allow the user to override the title of the notebook (useful for
109 #Allow the user to override the title of the notebook (useful for
109 #fancy document titles that the file system doesn't support.)
110 #fancy document titles that the file system doesn't support.)
110 overridetitle = Unicode("", config=True, help="")
111 overridetitle = Unicode("", config=True, help="")
111
112
112
113
113 def preprocess(self, nb, resources):
114 def preprocess(self, nb, resources):
114 """
115 """
115 Sphinx preprocessing to apply on each notebook.
116 Sphinx preprocessing to apply on each notebook.
116
117
117 Parameters
118 Parameters
118 ----------
119 ----------
119 nb : NotebookNode
120 nb : NotebookNode
120 Notebook being converted
121 Notebook being converted
121 resources : dictionary
122 resources : dictionary
122 Additional resources used in the conversion process. Allows
123 Additional resources used in the conversion process. Allows
123 preprocessors to pass variables into the Jinja engine.
124 preprocessors to pass variables into the Jinja engine.
124 """
125 """
125 # import sphinx here, so that sphinx is not a dependency when it's not used
126 # import sphinx here, so that sphinx is not a dependency when it's not used
126 import sphinx
127 import sphinx
127
128
128 # TODO: Add versatile method of additional notebook metadata. Include
129 # TODO: Add versatile method of additional notebook metadata. Include
129 # handling of multiple files. For now use a temporay namespace,
130 # handling of multiple files. For now use a temporay namespace,
130 # '_draft' to signify that this needs to change.
131 # '_draft' to signify that this needs to change.
131 if not isinstance(resources["sphinx"], dict):
132 if not isinstance(resources["sphinx"], dict):
132 resources["sphinx"] = {}
133 resources["sphinx"] = {}
133
134
134 if self.interactive:
135 if self.interactive:
135
136
136 # Prompt the user for additional meta data that doesn't exist currently
137 # Prompt the user for additional meta data that doesn't exist currently
137 # but would be usefull for Sphinx.
138 # but would be usefull for Sphinx.
138 resources["sphinx"]["author"] = self._prompt_author()
139 resources["sphinx"]["author"] = self._prompt_author()
139 resources["sphinx"]["version"] = self._prompt_version()
140 resources["sphinx"]["version"] = self._prompt_version()
140 resources["sphinx"]["release"] = self._prompt_release()
141 resources["sphinx"]["release"] = self._prompt_release()
141 resources["sphinx"]["date"] = self._prompt_date()
142 resources["sphinx"]["date"] = self._prompt_date()
142
143
143 # Prompt the user for the document style.
144 # Prompt the user for the document style.
144 resources["sphinx"]["chapterstyle"] = self._prompt_chapter_title_style()
145 resources["sphinx"]["chapterstyle"] = self._prompt_chapter_title_style()
145 resources["sphinx"]["outputstyle"] = self._prompt_output_style()
146 resources["sphinx"]["outputstyle"] = self._prompt_output_style()
146
147
147 # Small options
148 # Small options
148 resources["sphinx"]["centeroutput"] = console.prompt_boolean("Do you want to center the output? (false)", False)
149 resources["sphinx"]["centeroutput"] = console.prompt_boolean("Do you want to center the output? (false)", False)
149 resources["sphinx"]["header"] = console.prompt_boolean("Should a Sphinx document header be used? (true)", True)
150 resources["sphinx"]["header"] = console.prompt_boolean("Should a Sphinx document header be used? (true)", True)
150 else:
151 else:
151
152
152 # Try to use the traitlets.
153 # Try to use the traitlets.
153 resources["sphinx"]["author"] = self.author
154 resources["sphinx"]["author"] = self.author
154 resources["sphinx"]["version"] = self.version
155 resources["sphinx"]["version"] = self.version
155 resources["sphinx"]["release"] = self.release
156 resources["sphinx"]["release"] = self.release
156
157
157 # Use todays date if none is provided.
158 # Use todays date if none is provided.
158 if self.publish_date:
159 if self.publish_date:
159 resources["sphinx"]["date"] = self.publish_date
160 resources["sphinx"]["date"] = self.publish_date
160 elif len(resources['metadata']['modified_date'].strip()) == 0:
161 elif len(resources['metadata']['modified_date'].strip()) == 0:
161 if sys.platform == 'win32':
162 resources["sphinx"]["date"] = format_date(date.today())
162 resources["sphinx"]["date"] = date.today().strftime("%B %d, %Y")
163 else:
164 resources["sphinx"]["date"] = date.today().strftime("%B %-d, %Y")
165 else:
163 else:
166 resources["sphinx"]["date"] = resources['metadata']['modified_date']
164 resources["sphinx"]["date"] = resources['metadata']['modified_date']
167
165
168 # Sphinx traitlets.
166 # Sphinx traitlets.
169 resources["sphinx"]["chapterstyle"] = self.chapter_style
167 resources["sphinx"]["chapterstyle"] = self.chapter_style
170 resources["sphinx"]["outputstyle"] = self.output_style
168 resources["sphinx"]["outputstyle"] = self.output_style
171 resources["sphinx"]["centeroutput"] = self.center_output
169 resources["sphinx"]["centeroutput"] = self.center_output
172 resources["sphinx"]["header"] = self.use_headers
170 resources["sphinx"]["header"] = self.use_headers
173
171
174 # Find and pass in the path to the Sphinx dependencies.
172 # Find and pass in the path to the Sphinx dependencies.
175 resources["sphinx"]["texinputs"] = os.path.realpath(os.path.join(sphinx.package_dir, "texinputs"))
173 resources["sphinx"]["texinputs"] = os.path.realpath(os.path.join(sphinx.package_dir, "texinputs"))
176
174
177 # Generate Pygments definitions for Latex
175 # Generate Pygments definitions for Latex
178 resources["sphinx"]["pygment_definitions"] = self._generate_pygments_latex_def()
176 resources["sphinx"]["pygment_definitions"] = self._generate_pygments_latex_def()
179
177
180 if not (self.overridetitle == None or len(self.overridetitle.strip()) == 0):
178 if not (self.overridetitle == None or len(self.overridetitle.strip()) == 0):
181 resources['metadata']['name'] = self.overridetitle
179 resources['metadata']['name'] = self.overridetitle
182
180
183 # End
181 # End
184 return nb, resources
182 return nb, resources
185
183
186
184
187 def _generate_pygments_latex_def(self):
185 def _generate_pygments_latex_def(self):
188 """
186 """
189 Generate the pygments latex definitions that allows pygments
187 Generate the pygments latex definitions that allows pygments
190 to work in latex.
188 to work in latex.
191 """
189 """
192
190
193 return LatexFormatter().get_style_defs()
191 return LatexFormatter().get_style_defs()
194
192
195
193
196 def _prompt_author(self):
194 def _prompt_author(self):
197 """
195 """
198 Prompt the user to input an Author name
196 Prompt the user to input an Author name
199 """
197 """
200 return console.input("Author name: ")
198 return console.input("Author name: ")
201
199
202
200
203 def _prompt_version(self):
201 def _prompt_version(self):
204 """
202 """
205 prompt the user to enter a version number
203 prompt the user to enter a version number
206 """
204 """
207 return console.input("Version (ie ""1.0.0""): ")
205 return console.input("Version (ie ""1.0.0""): ")
208
206
209
207
210 def _prompt_release(self):
208 def _prompt_release(self):
211 """
209 """
212 Prompt the user to input a release name
210 Prompt the user to input a release name
213 """
211 """
214
212
215 return console.input("Release Name (ie ""Rough draft""): ")
213 return console.input("Release Name (ie ""Rough draft""): ")
216
214
217
215
218 def _prompt_date(self, resources):
216 def _prompt_date(self, resources):
219 """
217 """
220 Prompt the user to enter a date
218 Prompt the user to enter a date
221 """
219 """
222
220
223 if resources['metadata']['modified_date']:
221 if resources['metadata']['modified_date']:
224 default_date = resources['metadata']['modified_date']
222 default_date = resources['metadata']['modified_date']
225 else:
223 else:
226 if sys.platform == 'win32':
224 default_date = format_date(date.today())
227 default_date = date.today().strftime("%B %d, %Y")
228 else:
229 default_date = date.today().strftime("%B %-d, %Y")
230
225
231 user_date = console.input("Date (deafults to \"" + default_date + "\"): ")
226 user_date = console.input("Date (deafults to \"" + default_date + "\"): ")
232 if len(user_date.strip()) == 0:
227 if len(user_date.strip()) == 0:
233 user_date = default_date
228 user_date = default_date
234 return user_date
229 return user_date
235
230
236
231
237 def _prompt_output_style(self):
232 def _prompt_output_style(self):
238 """
233 """
239 Prompts the user to pick an IPython output style.
234 Prompts the user to pick an IPython output style.
240 """
235 """
241
236
242 # Dictionary of available output styles
237 # Dictionary of available output styles
243 styles = {1: "simple",
238 styles = {1: "simple",
244 2: "notebook"}
239 2: "notebook"}
245
240
246 #Append comments to the menu when displaying it to the user.
241 #Append comments to the menu when displaying it to the user.
247 comments = {1: "(recommended for long code segments)",
242 comments = {1: "(recommended for long code segments)",
248 2: "(default)"}
243 2: "(default)"}
249
244
250 return console.prompt_dictionary(styles, default_style=2, menu_comments=comments)
245 return console.prompt_dictionary(styles, default_style=2, menu_comments=comments)
251
246
252
247
253 def _prompt_chapter_title_style(self):
248 def _prompt_chapter_title_style(self):
254 """
249 """
255 Prompts the user to pick a Sphinx chapter style
250 Prompts the user to pick a Sphinx chapter style
256 """
251 """
257
252
258 # Dictionary of available Sphinx styles
253 # Dictionary of available Sphinx styles
259 styles = {1: "Bjarne",
254 styles = {1: "Bjarne",
260 2: "Lenny",
255 2: "Lenny",
261 3: "Glenn",
256 3: "Glenn",
262 4: "Conny",
257 4: "Conny",
263 5: "Rejne",
258 5: "Rejne",
264 6: "Sonny"}
259 6: "Sonny"}
265
260
266 #Append comments to the menu when displaying it to the user.
261 #Append comments to the menu when displaying it to the user.
267 comments = {1: "(default)",
262 comments = {1: "(default)",
268 6: "(for international documents)"}
263 6: "(for international documents)"}
269
264
270 return console.prompt_dictionary(styles, menu_comments=comments)
265 return console.prompt_dictionary(styles, menu_comments=comments)
@@ -1,713 +1,732 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for working with strings and text.
3 Utilities for working with strings and text.
4
4
5 Inheritance diagram:
5 Inheritance diagram:
6
6
7 .. inheritance-diagram:: IPython.utils.text
7 .. inheritance-diagram:: IPython.utils.text
8 :parts: 3
8 :parts: 3
9 """
9 """
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008-2011 The IPython Development Team
12 # Copyright (C) 2008-2011 The IPython Development Team
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 import os
22 import os
23 import re
23 import re
24 import sys
24 import textwrap
25 import textwrap
25 from string import Formatter
26 from string import Formatter
26
27
27 from IPython.external.path import path
28 from IPython.external.path import path
28 from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest
29 from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest
29 from IPython.utils import py3compat
30 from IPython.utils import py3compat
30
31
31 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
32 # Code
33 # Code
33 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
34
35
35 class LSString(str):
36 class LSString(str):
36 """String derivative with a special access attributes.
37 """String derivative with a special access attributes.
37
38
38 These are normal strings, but with the special attributes:
39 These are normal strings, but with the special attributes:
39
40
40 .l (or .list) : value as list (split on newlines).
41 .l (or .list) : value as list (split on newlines).
41 .n (or .nlstr): original value (the string itself).
42 .n (or .nlstr): original value (the string itself).
42 .s (or .spstr): value as whitespace-separated string.
43 .s (or .spstr): value as whitespace-separated string.
43 .p (or .paths): list of path objects
44 .p (or .paths): list of path objects
44
45
45 Any values which require transformations are computed only once and
46 Any values which require transformations are computed only once and
46 cached.
47 cached.
47
48
48 Such strings are very useful to efficiently interact with the shell, which
49 Such strings are very useful to efficiently interact with the shell, which
49 typically only understands whitespace-separated options for commands."""
50 typically only understands whitespace-separated options for commands."""
50
51
51 def get_list(self):
52 def get_list(self):
52 try:
53 try:
53 return self.__list
54 return self.__list
54 except AttributeError:
55 except AttributeError:
55 self.__list = self.split('\n')
56 self.__list = self.split('\n')
56 return self.__list
57 return self.__list
57
58
58 l = list = property(get_list)
59 l = list = property(get_list)
59
60
60 def get_spstr(self):
61 def get_spstr(self):
61 try:
62 try:
62 return self.__spstr
63 return self.__spstr
63 except AttributeError:
64 except AttributeError:
64 self.__spstr = self.replace('\n',' ')
65 self.__spstr = self.replace('\n',' ')
65 return self.__spstr
66 return self.__spstr
66
67
67 s = spstr = property(get_spstr)
68 s = spstr = property(get_spstr)
68
69
69 def get_nlstr(self):
70 def get_nlstr(self):
70 return self
71 return self
71
72
72 n = nlstr = property(get_nlstr)
73 n = nlstr = property(get_nlstr)
73
74
74 def get_paths(self):
75 def get_paths(self):
75 try:
76 try:
76 return self.__paths
77 return self.__paths
77 except AttributeError:
78 except AttributeError:
78 self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)]
79 self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)]
79 return self.__paths
80 return self.__paths
80
81
81 p = paths = property(get_paths)
82 p = paths = property(get_paths)
82
83
83 # FIXME: We need to reimplement type specific displayhook and then add this
84 # FIXME: We need to reimplement type specific displayhook and then add this
84 # back as a custom printer. This should also be moved outside utils into the
85 # back as a custom printer. This should also be moved outside utils into the
85 # core.
86 # core.
86
87
87 # def print_lsstring(arg):
88 # def print_lsstring(arg):
88 # """ Prettier (non-repr-like) and more informative printer for LSString """
89 # """ Prettier (non-repr-like) and more informative printer for LSString """
89 # print "LSString (.p, .n, .l, .s available). Value:"
90 # print "LSString (.p, .n, .l, .s available). Value:"
90 # print arg
91 # print arg
91 #
92 #
92 #
93 #
93 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
94 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
94
95
95
96
96 class SList(list):
97 class SList(list):
97 """List derivative with a special access attributes.
98 """List derivative with a special access attributes.
98
99
99 These are normal lists, but with the special attributes:
100 These are normal lists, but with the special attributes:
100
101
101 .l (or .list) : value as list (the list itself).
102 .l (or .list) : value as list (the list itself).
102 .n (or .nlstr): value as a string, joined on newlines.
103 .n (or .nlstr): value as a string, joined on newlines.
103 .s (or .spstr): value as a string, joined on spaces.
104 .s (or .spstr): value as a string, joined on spaces.
104 .p (or .paths): list of path objects
105 .p (or .paths): list of path objects
105
106
106 Any values which require transformations are computed only once and
107 Any values which require transformations are computed only once and
107 cached."""
108 cached."""
108
109
109 def get_list(self):
110 def get_list(self):
110 return self
111 return self
111
112
112 l = list = property(get_list)
113 l = list = property(get_list)
113
114
114 def get_spstr(self):
115 def get_spstr(self):
115 try:
116 try:
116 return self.__spstr
117 return self.__spstr
117 except AttributeError:
118 except AttributeError:
118 self.__spstr = ' '.join(self)
119 self.__spstr = ' '.join(self)
119 return self.__spstr
120 return self.__spstr
120
121
121 s = spstr = property(get_spstr)
122 s = spstr = property(get_spstr)
122
123
123 def get_nlstr(self):
124 def get_nlstr(self):
124 try:
125 try:
125 return self.__nlstr
126 return self.__nlstr
126 except AttributeError:
127 except AttributeError:
127 self.__nlstr = '\n'.join(self)
128 self.__nlstr = '\n'.join(self)
128 return self.__nlstr
129 return self.__nlstr
129
130
130 n = nlstr = property(get_nlstr)
131 n = nlstr = property(get_nlstr)
131
132
132 def get_paths(self):
133 def get_paths(self):
133 try:
134 try:
134 return self.__paths
135 return self.__paths
135 except AttributeError:
136 except AttributeError:
136 self.__paths = [path(p) for p in self if os.path.exists(p)]
137 self.__paths = [path(p) for p in self if os.path.exists(p)]
137 return self.__paths
138 return self.__paths
138
139
139 p = paths = property(get_paths)
140 p = paths = property(get_paths)
140
141
141 def grep(self, pattern, prune = False, field = None):
142 def grep(self, pattern, prune = False, field = None):
142 """ Return all strings matching 'pattern' (a regex or callable)
143 """ Return all strings matching 'pattern' (a regex or callable)
143
144
144 This is case-insensitive. If prune is true, return all items
145 This is case-insensitive. If prune is true, return all items
145 NOT matching the pattern.
146 NOT matching the pattern.
146
147
147 If field is specified, the match must occur in the specified
148 If field is specified, the match must occur in the specified
148 whitespace-separated field.
149 whitespace-separated field.
149
150
150 Examples::
151 Examples::
151
152
152 a.grep( lambda x: x.startswith('C') )
153 a.grep( lambda x: x.startswith('C') )
153 a.grep('Cha.*log', prune=1)
154 a.grep('Cha.*log', prune=1)
154 a.grep('chm', field=-1)
155 a.grep('chm', field=-1)
155 """
156 """
156
157
157 def match_target(s):
158 def match_target(s):
158 if field is None:
159 if field is None:
159 return s
160 return s
160 parts = s.split()
161 parts = s.split()
161 try:
162 try:
162 tgt = parts[field]
163 tgt = parts[field]
163 return tgt
164 return tgt
164 except IndexError:
165 except IndexError:
165 return ""
166 return ""
166
167
167 if isinstance(pattern, basestring):
168 if isinstance(pattern, basestring):
168 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
169 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
169 else:
170 else:
170 pred = pattern
171 pred = pattern
171 if not prune:
172 if not prune:
172 return SList([el for el in self if pred(match_target(el))])
173 return SList([el for el in self if pred(match_target(el))])
173 else:
174 else:
174 return SList([el for el in self if not pred(match_target(el))])
175 return SList([el for el in self if not pred(match_target(el))])
175
176
176 def fields(self, *fields):
177 def fields(self, *fields):
177 """ Collect whitespace-separated fields from string list
178 """ Collect whitespace-separated fields from string list
178
179
179 Allows quick awk-like usage of string lists.
180 Allows quick awk-like usage of string lists.
180
181
181 Example data (in var a, created by 'a = !ls -l')::
182 Example data (in var a, created by 'a = !ls -l')::
182 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
183 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
183 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
184 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
184
185
185 a.fields(0) is ['-rwxrwxrwx', 'drwxrwxrwx+']
186 a.fields(0) is ['-rwxrwxrwx', 'drwxrwxrwx+']
186 a.fields(1,0) is ['1 -rwxrwxrwx', '6 drwxrwxrwx+']
187 a.fields(1,0) is ['1 -rwxrwxrwx', '6 drwxrwxrwx+']
187 (note the joining by space).
188 (note the joining by space).
188 a.fields(-1) is ['ChangeLog', 'IPython']
189 a.fields(-1) is ['ChangeLog', 'IPython']
189
190
190 IndexErrors are ignored.
191 IndexErrors are ignored.
191
192
192 Without args, fields() just split()'s the strings.
193 Without args, fields() just split()'s the strings.
193 """
194 """
194 if len(fields) == 0:
195 if len(fields) == 0:
195 return [el.split() for el in self]
196 return [el.split() for el in self]
196
197
197 res = SList()
198 res = SList()
198 for el in [f.split() for f in self]:
199 for el in [f.split() for f in self]:
199 lineparts = []
200 lineparts = []
200
201
201 for fd in fields:
202 for fd in fields:
202 try:
203 try:
203 lineparts.append(el[fd])
204 lineparts.append(el[fd])
204 except IndexError:
205 except IndexError:
205 pass
206 pass
206 if lineparts:
207 if lineparts:
207 res.append(" ".join(lineparts))
208 res.append(" ".join(lineparts))
208
209
209 return res
210 return res
210
211
211 def sort(self,field= None, nums = False):
212 def sort(self,field= None, nums = False):
212 """ sort by specified fields (see fields())
213 """ sort by specified fields (see fields())
213
214
214 Example::
215 Example::
215 a.sort(1, nums = True)
216 a.sort(1, nums = True)
216
217
217 Sorts a by second field, in numerical order (so that 21 > 3)
218 Sorts a by second field, in numerical order (so that 21 > 3)
218
219
219 """
220 """
220
221
221 #decorate, sort, undecorate
222 #decorate, sort, undecorate
222 if field is not None:
223 if field is not None:
223 dsu = [[SList([line]).fields(field), line] for line in self]
224 dsu = [[SList([line]).fields(field), line] for line in self]
224 else:
225 else:
225 dsu = [[line, line] for line in self]
226 dsu = [[line, line] for line in self]
226 if nums:
227 if nums:
227 for i in range(len(dsu)):
228 for i in range(len(dsu)):
228 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
229 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
229 try:
230 try:
230 n = int(numstr)
231 n = int(numstr)
231 except ValueError:
232 except ValueError:
232 n = 0;
233 n = 0;
233 dsu[i][0] = n
234 dsu[i][0] = n
234
235
235
236
236 dsu.sort()
237 dsu.sort()
237 return SList([t[1] for t in dsu])
238 return SList([t[1] for t in dsu])
238
239
239
240
240 # FIXME: We need to reimplement type specific displayhook and then add this
241 # FIXME: We need to reimplement type specific displayhook and then add this
241 # back as a custom printer. This should also be moved outside utils into the
242 # back as a custom printer. This should also be moved outside utils into the
242 # core.
243 # core.
243
244
244 # def print_slist(arg):
245 # def print_slist(arg):
245 # """ Prettier (non-repr-like) and more informative printer for SList """
246 # """ Prettier (non-repr-like) and more informative printer for SList """
246 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
247 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
247 # if hasattr(arg, 'hideonce') and arg.hideonce:
248 # if hasattr(arg, 'hideonce') and arg.hideonce:
248 # arg.hideonce = False
249 # arg.hideonce = False
249 # return
250 # return
250 #
251 #
251 # nlprint(arg) # This was a nested list printer, now removed.
252 # nlprint(arg) # This was a nested list printer, now removed.
252 #
253 #
253 # print_slist = result_display.when_type(SList)(print_slist)
254 # print_slist = result_display.when_type(SList)(print_slist)
254
255
255
256
256 def indent(instr,nspaces=4, ntabs=0, flatten=False):
257 def indent(instr,nspaces=4, ntabs=0, flatten=False):
257 """Indent a string a given number of spaces or tabstops.
258 """Indent a string a given number of spaces or tabstops.
258
259
259 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
260 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
260
261
261 Parameters
262 Parameters
262 ----------
263 ----------
263
264
264 instr : basestring
265 instr : basestring
265 The string to be indented.
266 The string to be indented.
266 nspaces : int (default: 4)
267 nspaces : int (default: 4)
267 The number of spaces to be indented.
268 The number of spaces to be indented.
268 ntabs : int (default: 0)
269 ntabs : int (default: 0)
269 The number of tabs to be indented.
270 The number of tabs to be indented.
270 flatten : bool (default: False)
271 flatten : bool (default: False)
271 Whether to scrub existing indentation. If True, all lines will be
272 Whether to scrub existing indentation. If True, all lines will be
272 aligned to the same indentation. If False, existing indentation will
273 aligned to the same indentation. If False, existing indentation will
273 be strictly increased.
274 be strictly increased.
274
275
275 Returns
276 Returns
276 -------
277 -------
277
278
278 str|unicode : string indented by ntabs and nspaces.
279 str|unicode : string indented by ntabs and nspaces.
279
280
280 """
281 """
281 if instr is None:
282 if instr is None:
282 return
283 return
283 ind = '\t'*ntabs+' '*nspaces
284 ind = '\t'*ntabs+' '*nspaces
284 if flatten:
285 if flatten:
285 pat = re.compile(r'^\s*', re.MULTILINE)
286 pat = re.compile(r'^\s*', re.MULTILINE)
286 else:
287 else:
287 pat = re.compile(r'^', re.MULTILINE)
288 pat = re.compile(r'^', re.MULTILINE)
288 outstr = re.sub(pat, ind, instr)
289 outstr = re.sub(pat, ind, instr)
289 if outstr.endswith(os.linesep+ind):
290 if outstr.endswith(os.linesep+ind):
290 return outstr[:-len(ind)]
291 return outstr[:-len(ind)]
291 else:
292 else:
292 return outstr
293 return outstr
293
294
294
295
295 def list_strings(arg):
296 def list_strings(arg):
296 """Always return a list of strings, given a string or list of strings
297 """Always return a list of strings, given a string or list of strings
297 as input.
298 as input.
298
299
299 :Examples:
300 :Examples:
300
301
301 In [7]: list_strings('A single string')
302 In [7]: list_strings('A single string')
302 Out[7]: ['A single string']
303 Out[7]: ['A single string']
303
304
304 In [8]: list_strings(['A single string in a list'])
305 In [8]: list_strings(['A single string in a list'])
305 Out[8]: ['A single string in a list']
306 Out[8]: ['A single string in a list']
306
307
307 In [9]: list_strings(['A','list','of','strings'])
308 In [9]: list_strings(['A','list','of','strings'])
308 Out[9]: ['A', 'list', 'of', 'strings']
309 Out[9]: ['A', 'list', 'of', 'strings']
309 """
310 """
310
311
311 if isinstance(arg,basestring): return [arg]
312 if isinstance(arg,basestring): return [arg]
312 else: return arg
313 else: return arg
313
314
314
315
315 def marquee(txt='',width=78,mark='*'):
316 def marquee(txt='',width=78,mark='*'):
316 """Return the input string centered in a 'marquee'.
317 """Return the input string centered in a 'marquee'.
317
318
318 :Examples:
319 :Examples:
319
320
320 In [16]: marquee('A test',40)
321 In [16]: marquee('A test',40)
321 Out[16]: '**************** A test ****************'
322 Out[16]: '**************** A test ****************'
322
323
323 In [17]: marquee('A test',40,'-')
324 In [17]: marquee('A test',40,'-')
324 Out[17]: '---------------- A test ----------------'
325 Out[17]: '---------------- A test ----------------'
325
326
326 In [18]: marquee('A test',40,' ')
327 In [18]: marquee('A test',40,' ')
327 Out[18]: ' A test '
328 Out[18]: ' A test '
328
329
329 """
330 """
330 if not txt:
331 if not txt:
331 return (mark*width)[:width]
332 return (mark*width)[:width]
332 nmark = (width-len(txt)-2)//len(mark)//2
333 nmark = (width-len(txt)-2)//len(mark)//2
333 if nmark < 0: nmark =0
334 if nmark < 0: nmark =0
334 marks = mark*nmark
335 marks = mark*nmark
335 return '%s %s %s' % (marks,txt,marks)
336 return '%s %s %s' % (marks,txt,marks)
336
337
337
338
338 ini_spaces_re = re.compile(r'^(\s+)')
339 ini_spaces_re = re.compile(r'^(\s+)')
339
340
340 def num_ini_spaces(strng):
341 def num_ini_spaces(strng):
341 """Return the number of initial spaces in a string"""
342 """Return the number of initial spaces in a string"""
342
343
343 ini_spaces = ini_spaces_re.match(strng)
344 ini_spaces = ini_spaces_re.match(strng)
344 if ini_spaces:
345 if ini_spaces:
345 return ini_spaces.end()
346 return ini_spaces.end()
346 else:
347 else:
347 return 0
348 return 0
348
349
349
350
350 def format_screen(strng):
351 def format_screen(strng):
351 """Format a string for screen printing.
352 """Format a string for screen printing.
352
353
353 This removes some latex-type format codes."""
354 This removes some latex-type format codes."""
354 # Paragraph continue
355 # Paragraph continue
355 par_re = re.compile(r'\\$',re.MULTILINE)
356 par_re = re.compile(r'\\$',re.MULTILINE)
356 strng = par_re.sub('',strng)
357 strng = par_re.sub('',strng)
357 return strng
358 return strng
358
359
359
360
360 def dedent(text):
361 def dedent(text):
361 """Equivalent of textwrap.dedent that ignores unindented first line.
362 """Equivalent of textwrap.dedent that ignores unindented first line.
362
363
363 This means it will still dedent strings like:
364 This means it will still dedent strings like:
364 '''foo
365 '''foo
365 is a bar
366 is a bar
366 '''
367 '''
367
368
368 For use in wrap_paragraphs.
369 For use in wrap_paragraphs.
369 """
370 """
370
371
371 if text.startswith('\n'):
372 if text.startswith('\n'):
372 # text starts with blank line, don't ignore the first line
373 # text starts with blank line, don't ignore the first line
373 return textwrap.dedent(text)
374 return textwrap.dedent(text)
374
375
375 # split first line
376 # split first line
376 splits = text.split('\n',1)
377 splits = text.split('\n',1)
377 if len(splits) == 1:
378 if len(splits) == 1:
378 # only one line
379 # only one line
379 return textwrap.dedent(text)
380 return textwrap.dedent(text)
380
381
381 first, rest = splits
382 first, rest = splits
382 # dedent everything but the first line
383 # dedent everything but the first line
383 rest = textwrap.dedent(rest)
384 rest = textwrap.dedent(rest)
384 return '\n'.join([first, rest])
385 return '\n'.join([first, rest])
385
386
386
387
387 def wrap_paragraphs(text, ncols=80):
388 def wrap_paragraphs(text, ncols=80):
388 """Wrap multiple paragraphs to fit a specified width.
389 """Wrap multiple paragraphs to fit a specified width.
389
390
390 This is equivalent to textwrap.wrap, but with support for multiple
391 This is equivalent to textwrap.wrap, but with support for multiple
391 paragraphs, as separated by empty lines.
392 paragraphs, as separated by empty lines.
392
393
393 Returns
394 Returns
394 -------
395 -------
395
396
396 list of complete paragraphs, wrapped to fill `ncols` columns.
397 list of complete paragraphs, wrapped to fill `ncols` columns.
397 """
398 """
398 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
399 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
399 text = dedent(text).strip()
400 text = dedent(text).strip()
400 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
401 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
401 out_ps = []
402 out_ps = []
402 indent_re = re.compile(r'\n\s+', re.MULTILINE)
403 indent_re = re.compile(r'\n\s+', re.MULTILINE)
403 for p in paragraphs:
404 for p in paragraphs:
404 # presume indentation that survives dedent is meaningful formatting,
405 # presume indentation that survives dedent is meaningful formatting,
405 # so don't fill unless text is flush.
406 # so don't fill unless text is flush.
406 if indent_re.search(p) is None:
407 if indent_re.search(p) is None:
407 # wrap paragraph
408 # wrap paragraph
408 p = textwrap.fill(p, ncols)
409 p = textwrap.fill(p, ncols)
409 out_ps.append(p)
410 out_ps.append(p)
410 return out_ps
411 return out_ps
411
412
412
413
413 def long_substr(data):
414 def long_substr(data):
414 """Return the longest common substring in a list of strings.
415 """Return the longest common substring in a list of strings.
415
416
416 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
417 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
417 """
418 """
418 substr = ''
419 substr = ''
419 if len(data) > 1 and len(data[0]) > 0:
420 if len(data) > 1 and len(data[0]) > 0:
420 for i in range(len(data[0])):
421 for i in range(len(data[0])):
421 for j in range(len(data[0])-i+1):
422 for j in range(len(data[0])-i+1):
422 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
423 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
423 substr = data[0][i:i+j]
424 substr = data[0][i:i+j]
424 elif len(data) == 1:
425 elif len(data) == 1:
425 substr = data[0]
426 substr = data[0]
426 return substr
427 return substr
427
428
428
429
429 def strip_email_quotes(text):
430 def strip_email_quotes(text):
430 """Strip leading email quotation characters ('>').
431 """Strip leading email quotation characters ('>').
431
432
432 Removes any combination of leading '>' interspersed with whitespace that
433 Removes any combination of leading '>' interspersed with whitespace that
433 appears *identically* in all lines of the input text.
434 appears *identically* in all lines of the input text.
434
435
435 Parameters
436 Parameters
436 ----------
437 ----------
437 text : str
438 text : str
438
439
439 Examples
440 Examples
440 --------
441 --------
441
442
442 Simple uses::
443 Simple uses::
443
444
444 In [2]: strip_email_quotes('> > text')
445 In [2]: strip_email_quotes('> > text')
445 Out[2]: 'text'
446 Out[2]: 'text'
446
447
447 In [3]: strip_email_quotes('> > text\\n> > more')
448 In [3]: strip_email_quotes('> > text\\n> > more')
448 Out[3]: 'text\\nmore'
449 Out[3]: 'text\\nmore'
449
450
450 Note how only the common prefix that appears in all lines is stripped::
451 Note how only the common prefix that appears in all lines is stripped::
451
452
452 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
453 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
453 Out[4]: '> text\\n> more\\nmore...'
454 Out[4]: '> text\\n> more\\nmore...'
454
455
455 So if any line has no quote marks ('>') , then none are stripped from any
456 So if any line has no quote marks ('>') , then none are stripped from any
456 of them ::
457 of them ::
457
458
458 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
459 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
459 Out[5]: '> > text\\n> > more\\nlast different'
460 Out[5]: '> > text\\n> > more\\nlast different'
460 """
461 """
461 lines = text.splitlines()
462 lines = text.splitlines()
462 matches = set()
463 matches = set()
463 for line in lines:
464 for line in lines:
464 prefix = re.match(r'^(\s*>[ >]*)', line)
465 prefix = re.match(r'^(\s*>[ >]*)', line)
465 if prefix:
466 if prefix:
466 matches.add(prefix.group(1))
467 matches.add(prefix.group(1))
467 else:
468 else:
468 break
469 break
469 else:
470 else:
470 prefix = long_substr(list(matches))
471 prefix = long_substr(list(matches))
471 if prefix:
472 if prefix:
472 strip = len(prefix)
473 strip = len(prefix)
473 text = '\n'.join([ ln[strip:] for ln in lines])
474 text = '\n'.join([ ln[strip:] for ln in lines])
474 return text
475 return text
475
476
476
477
477 class EvalFormatter(Formatter):
478 class EvalFormatter(Formatter):
478 """A String Formatter that allows evaluation of simple expressions.
479 """A String Formatter that allows evaluation of simple expressions.
479
480
480 Note that this version interprets a : as specifying a format string (as per
481 Note that this version interprets a : as specifying a format string (as per
481 standard string formatting), so if slicing is required, you must explicitly
482 standard string formatting), so if slicing is required, you must explicitly
482 create a slice.
483 create a slice.
483
484
484 This is to be used in templating cases, such as the parallel batch
485 This is to be used in templating cases, such as the parallel batch
485 script templates, where simple arithmetic on arguments is useful.
486 script templates, where simple arithmetic on arguments is useful.
486
487
487 Examples
488 Examples
488 --------
489 --------
489
490
490 In [1]: f = EvalFormatter()
491 In [1]: f = EvalFormatter()
491 In [2]: f.format('{n//4}', n=8)
492 In [2]: f.format('{n//4}', n=8)
492 Out [2]: '2'
493 Out [2]: '2'
493
494
494 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
495 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
495 Out [3]: 'll'
496 Out [3]: 'll'
496 """
497 """
497 def get_field(self, name, args, kwargs):
498 def get_field(self, name, args, kwargs):
498 v = eval(name, kwargs)
499 v = eval(name, kwargs)
499 return v, name
500 return v, name
500
501
501
502
502 @skip_doctest_py3
503 @skip_doctest_py3
503 class FullEvalFormatter(Formatter):
504 class FullEvalFormatter(Formatter):
504 """A String Formatter that allows evaluation of simple expressions.
505 """A String Formatter that allows evaluation of simple expressions.
505
506
506 Any time a format key is not found in the kwargs,
507 Any time a format key is not found in the kwargs,
507 it will be tried as an expression in the kwargs namespace.
508 it will be tried as an expression in the kwargs namespace.
508
509
509 Note that this version allows slicing using [1:2], so you cannot specify
510 Note that this version allows slicing using [1:2], so you cannot specify
510 a format string. Use :class:`EvalFormatter` to permit format strings.
511 a format string. Use :class:`EvalFormatter` to permit format strings.
511
512
512 Examples
513 Examples
513 --------
514 --------
514
515
515 In [1]: f = FullEvalFormatter()
516 In [1]: f = FullEvalFormatter()
516 In [2]: f.format('{n//4}', n=8)
517 In [2]: f.format('{n//4}', n=8)
517 Out[2]: u'2'
518 Out[2]: u'2'
518
519
519 In [3]: f.format('{list(range(5))[2:4]}')
520 In [3]: f.format('{list(range(5))[2:4]}')
520 Out[3]: u'[2, 3]'
521 Out[3]: u'[2, 3]'
521
522
522 In [4]: f.format('{3*2}')
523 In [4]: f.format('{3*2}')
523 Out[4]: u'6'
524 Out[4]: u'6'
524 """
525 """
525 # copied from Formatter._vformat with minor changes to allow eval
526 # copied from Formatter._vformat with minor changes to allow eval
526 # and replace the format_spec code with slicing
527 # and replace the format_spec code with slicing
527 def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
528 def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
528 if recursion_depth < 0:
529 if recursion_depth < 0:
529 raise ValueError('Max string recursion exceeded')
530 raise ValueError('Max string recursion exceeded')
530 result = []
531 result = []
531 for literal_text, field_name, format_spec, conversion in \
532 for literal_text, field_name, format_spec, conversion in \
532 self.parse(format_string):
533 self.parse(format_string):
533
534
534 # output the literal text
535 # output the literal text
535 if literal_text:
536 if literal_text:
536 result.append(literal_text)
537 result.append(literal_text)
537
538
538 # if there's a field, output it
539 # if there's a field, output it
539 if field_name is not None:
540 if field_name is not None:
540 # this is some markup, find the object and do
541 # this is some markup, find the object and do
541 # the formatting
542 # the formatting
542
543
543 if format_spec:
544 if format_spec:
544 # override format spec, to allow slicing:
545 # override format spec, to allow slicing:
545 field_name = ':'.join([field_name, format_spec])
546 field_name = ':'.join([field_name, format_spec])
546
547
547 # eval the contents of the field for the object
548 # eval the contents of the field for the object
548 # to be formatted
549 # to be formatted
549 obj = eval(field_name, kwargs)
550 obj = eval(field_name, kwargs)
550
551
551 # do any conversion on the resulting object
552 # do any conversion on the resulting object
552 obj = self.convert_field(obj, conversion)
553 obj = self.convert_field(obj, conversion)
553
554
554 # format the object and append to the result
555 # format the object and append to the result
555 result.append(self.format_field(obj, ''))
556 result.append(self.format_field(obj, ''))
556
557
557 return u''.join(py3compat.cast_unicode(s) for s in result)
558 return u''.join(py3compat.cast_unicode(s) for s in result)
558
559
559
560
560 @skip_doctest_py3
561 @skip_doctest_py3
561 class DollarFormatter(FullEvalFormatter):
562 class DollarFormatter(FullEvalFormatter):
562 """Formatter allowing Itpl style $foo replacement, for names and attribute
563 """Formatter allowing Itpl style $foo replacement, for names and attribute
563 access only. Standard {foo} replacement also works, and allows full
564 access only. Standard {foo} replacement also works, and allows full
564 evaluation of its arguments.
565 evaluation of its arguments.
565
566
566 Examples
567 Examples
567 --------
568 --------
568 In [1]: f = DollarFormatter()
569 In [1]: f = DollarFormatter()
569 In [2]: f.format('{n//4}', n=8)
570 In [2]: f.format('{n//4}', n=8)
570 Out[2]: u'2'
571 Out[2]: u'2'
571
572
572 In [3]: f.format('23 * 76 is $result', result=23*76)
573 In [3]: f.format('23 * 76 is $result', result=23*76)
573 Out[3]: u'23 * 76 is 1748'
574 Out[3]: u'23 * 76 is 1748'
574
575
575 In [4]: f.format('$a or {b}', a=1, b=2)
576 In [4]: f.format('$a or {b}', a=1, b=2)
576 Out[4]: u'1 or 2'
577 Out[4]: u'1 or 2'
577 """
578 """
578 _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)")
579 _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)")
579 def parse(self, fmt_string):
580 def parse(self, fmt_string):
580 for literal_txt, field_name, format_spec, conversion \
581 for literal_txt, field_name, format_spec, conversion \
581 in Formatter.parse(self, fmt_string):
582 in Formatter.parse(self, fmt_string):
582
583
583 # Find $foo patterns in the literal text.
584 # Find $foo patterns in the literal text.
584 continue_from = 0
585 continue_from = 0
585 txt = ""
586 txt = ""
586 for m in self._dollar_pattern.finditer(literal_txt):
587 for m in self._dollar_pattern.finditer(literal_txt):
587 new_txt, new_field = m.group(1,2)
588 new_txt, new_field = m.group(1,2)
588 # $$foo --> $foo
589 # $$foo --> $foo
589 if new_field.startswith("$"):
590 if new_field.startswith("$"):
590 txt += new_txt + new_field
591 txt += new_txt + new_field
591 else:
592 else:
592 yield (txt + new_txt, new_field, "", None)
593 yield (txt + new_txt, new_field, "", None)
593 txt = ""
594 txt = ""
594 continue_from = m.end()
595 continue_from = m.end()
595
596
596 # Re-yield the {foo} style pattern
597 # Re-yield the {foo} style pattern
597 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
598 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
598
599
599 #-----------------------------------------------------------------------------
600 #-----------------------------------------------------------------------------
600 # Utils to columnize a list of string
601 # Utils to columnize a list of string
601 #-----------------------------------------------------------------------------
602 #-----------------------------------------------------------------------------
602
603
603 def _chunks(l, n):
604 def _chunks(l, n):
604 """Yield successive n-sized chunks from l."""
605 """Yield successive n-sized chunks from l."""
605 for i in xrange(0, len(l), n):
606 for i in xrange(0, len(l), n):
606 yield l[i:i+n]
607 yield l[i:i+n]
607
608
608
609
609 def _find_optimal(rlist , separator_size=2 , displaywidth=80):
610 def _find_optimal(rlist , separator_size=2 , displaywidth=80):
610 """Calculate optimal info to columnize a list of string"""
611 """Calculate optimal info to columnize a list of string"""
611 for nrow in range(1, len(rlist)+1) :
612 for nrow in range(1, len(rlist)+1) :
612 chk = map(max,_chunks(rlist, nrow))
613 chk = map(max,_chunks(rlist, nrow))
613 sumlength = sum(chk)
614 sumlength = sum(chk)
614 ncols = len(chk)
615 ncols = len(chk)
615 if sumlength+separator_size*(ncols-1) <= displaywidth :
616 if sumlength+separator_size*(ncols-1) <= displaywidth :
616 break;
617 break;
617 return {'columns_numbers' : ncols,
618 return {'columns_numbers' : ncols,
618 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0,
619 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0,
619 'rows_numbers' : nrow,
620 'rows_numbers' : nrow,
620 'columns_width' : chk
621 'columns_width' : chk
621 }
622 }
622
623
623
624
624 def _get_or_default(mylist, i, default=None):
625 def _get_or_default(mylist, i, default=None):
625 """return list item number, or default if don't exist"""
626 """return list item number, or default if don't exist"""
626 if i >= len(mylist):
627 if i >= len(mylist):
627 return default
628 return default
628 else :
629 else :
629 return mylist[i]
630 return mylist[i]
630
631
631
632
632 @skip_doctest
633 @skip_doctest
633 def compute_item_matrix(items, empty=None, *args, **kwargs) :
634 def compute_item_matrix(items, empty=None, *args, **kwargs) :
634 """Returns a nested list, and info to columnize items
635 """Returns a nested list, and info to columnize items
635
636
636 Parameters
637 Parameters
637 ----------
638 ----------
638
639
639 items :
640 items :
640 list of strings to columize
641 list of strings to columize
641 empty : (default None)
642 empty : (default None)
642 default value to fill list if needed
643 default value to fill list if needed
643 separator_size : int (default=2)
644 separator_size : int (default=2)
644 How much caracters will be used as a separation between each columns.
645 How much caracters will be used as a separation between each columns.
645 displaywidth : int (default=80)
646 displaywidth : int (default=80)
646 The width of the area onto wich the columns should enter
647 The width of the area onto wich the columns should enter
647
648
648 Returns
649 Returns
649 -------
650 -------
650
651
651 Returns a tuple of (strings_matrix, dict_info)
652 Returns a tuple of (strings_matrix, dict_info)
652
653
653 strings_matrix :
654 strings_matrix :
654
655
655 nested list of string, the outer most list contains as many list as
656 nested list of string, the outer most list contains as many list as
656 rows, the innermost lists have each as many element as colums. If the
657 rows, the innermost lists have each as many element as colums. If the
657 total number of elements in `items` does not equal the product of
658 total number of elements in `items` does not equal the product of
658 rows*columns, the last element of some lists are filled with `None`.
659 rows*columns, the last element of some lists are filled with `None`.
659
660
660 dict_info :
661 dict_info :
661 some info to make columnize easier:
662 some info to make columnize easier:
662
663
663 columns_numbers : number of columns
664 columns_numbers : number of columns
664 rows_numbers : number of rows
665 rows_numbers : number of rows
665 columns_width : list of with of each columns
666 columns_width : list of with of each columns
666 optimal_separator_width : best separator width between columns
667 optimal_separator_width : best separator width between columns
667
668
668 Examples
669 Examples
669 --------
670 --------
670
671
671 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
672 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
672 ...: compute_item_matrix(l,displaywidth=12)
673 ...: compute_item_matrix(l,displaywidth=12)
673 Out[1]:
674 Out[1]:
674 ([['aaa', 'f', 'k'],
675 ([['aaa', 'f', 'k'],
675 ['b', 'g', 'l'],
676 ['b', 'g', 'l'],
676 ['cc', 'h', None],
677 ['cc', 'h', None],
677 ['d', 'i', None],
678 ['d', 'i', None],
678 ['eeeee', 'j', None]],
679 ['eeeee', 'j', None]],
679 {'columns_numbers': 3,
680 {'columns_numbers': 3,
680 'columns_width': [5, 1, 1],
681 'columns_width': [5, 1, 1],
681 'optimal_separator_width': 2,
682 'optimal_separator_width': 2,
682 'rows_numbers': 5})
683 'rows_numbers': 5})
683
684
684 """
685 """
685 info = _find_optimal(map(len, items), *args, **kwargs)
686 info = _find_optimal(map(len, items), *args, **kwargs)
686 nrow, ncol = info['rows_numbers'], info['columns_numbers']
687 nrow, ncol = info['rows_numbers'], info['columns_numbers']
687 return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info)
688 return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info)
688
689
689
690
690 def columnize(items, separator=' ', displaywidth=80):
691 def columnize(items, separator=' ', displaywidth=80):
691 """ Transform a list of strings into a single string with columns.
692 """ Transform a list of strings into a single string with columns.
692
693
693 Parameters
694 Parameters
694 ----------
695 ----------
695 items : sequence of strings
696 items : sequence of strings
696 The strings to process.
697 The strings to process.
697
698
698 separator : str, optional [default is two spaces]
699 separator : str, optional [default is two spaces]
699 The string that separates columns.
700 The string that separates columns.
700
701
701 displaywidth : int, optional [default is 80]
702 displaywidth : int, optional [default is 80]
702 Width of the display in number of characters.
703 Width of the display in number of characters.
703
704
704 Returns
705 Returns
705 -------
706 -------
706 The formatted string.
707 The formatted string.
707 """
708 """
708 if not items :
709 if not items :
709 return '\n'
710 return '\n'
710 matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth)
711 matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth)
711 fmatrix = [filter(None, x) for x in matrix]
712 fmatrix = [filter(None, x) for x in matrix]
712 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])])
713 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])])
713 return '\n'.join(map(sjoin, fmatrix))+'\n'
714 return '\n'.join(map(sjoin, fmatrix))+'\n'
715
716
717 def format_date(date):
718 """ Format a datetime object as a string.
719
720 Parameters
721 ----------
722 date : datetime object
723 Date to be formatted.
724
725 Returns
726 -------
727 The formatted date string (ie "July 1, 1990")
728 """
729 if sys.platform == 'win32':
730 return date.strftime("%B {0}, %Y").format(date.day)
731 else:
732 return date.strftime("%B %-d, %Y") No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now