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