##// END OF EJS Templates
add posix_path filter...
MinRK -
Show More
@@ -1,517 +1,518 b''
1 """This module defines Exporter, a highly configurable converter
1 """This module defines Exporter, a highly configurable converter
2 that uses Jinja2 to export notebook files into different formats.
2 that uses Jinja2 to export notebook files into different formats.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 from __future__ import print_function, absolute_import
17 from __future__ import print_function, absolute_import
18
18
19 # Stdlib imports
19 # Stdlib imports
20 import io
20 import io
21 import os
21 import os
22 import inspect
22 import inspect
23 import copy
23 import copy
24 import collections
24 import collections
25 import datetime
25 import datetime
26
26
27 # other libs/dependencies
27 # other libs/dependencies
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
29
29
30 # IPython imports
30 # IPython imports
31 from IPython.config.configurable import LoggingConfigurable
31 from IPython.config.configurable import LoggingConfigurable
32 from IPython.config import Config
32 from IPython.config import Config
33 from IPython.nbformat import current as nbformat
33 from IPython.nbformat import current as nbformat
34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict, Any
34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict, Any
35 from IPython.utils.importstring import import_item
35 from IPython.utils.importstring import import_item
36 from IPython.utils.text import indent
36 from IPython.utils.text import indent
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': 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 'markdown2latex': filters.markdown2latex,
59 'markdown2latex': filters.markdown2latex,
59 'markdown2rst': filters.markdown2rst,
60 'markdown2rst': filters.markdown2rst,
60 'comment_lines': filters.comment_lines,
61 'comment_lines': filters.comment_lines,
61 'strip_ansi': filters.strip_ansi,
62 'strip_ansi': filters.strip_ansi,
62 'strip_dollars': filters.strip_dollars,
63 'strip_dollars': filters.strip_dollars,
63 'strip_files_prefix': filters.strip_files_prefix,
64 'strip_files_prefix': filters.strip_files_prefix,
64 'html2text' : filters.html2text,
65 'html2text' : filters.html2text,
65 'add_anchor': filters.add_anchor,
66 'add_anchor': filters.add_anchor,
66 'ansi2latex': filters.ansi2latex,
67 'ansi2latex': filters.ansi2latex,
67 'strip_math_space': filters.strip_math_space,
68 'strip_math_space': filters.strip_math_space,
68 'wrap_text': filters.wrap_text,
69 'wrap_text': filters.wrap_text,
69 'escape_latex': filters.escape_latex
70 'escape_latex': filters.escape_latex,
70 }
71 }
71
72
72 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
73 # Class
74 # Class
74 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
75
76
76 class ResourcesDict(collections.defaultdict):
77 class ResourcesDict(collections.defaultdict):
77 def __missing__(self, key):
78 def __missing__(self, key):
78 return ''
79 return ''
79
80
80
81
81 class Exporter(LoggingConfigurable):
82 class Exporter(LoggingConfigurable):
82 """
83 """
83 Exports notebooks into other file formats. Uses Jinja 2 templating engine
84 Exports notebooks into other file formats. Uses Jinja 2 templating engine
84 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
85 template type along with new filters/transformers. If the filters/
86 template type along with new filters/transformers. If the filters/
86 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
87 this class. Instead, override the template_file and file_extension
88 this class. Instead, override the template_file and file_extension
88 traits via a config file.
89 traits via a config file.
89
90
90 {filters}
91 {filters}
91 """
92 """
92
93
93 # finish the docstring
94 # finish the docstring
94 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
95 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
95
96
96
97
97 template_file = Unicode(u'default',
98 template_file = Unicode(u'default',
98 config=True,
99 config=True,
99 help="Name of the template file to use")
100 help="Name of the template file to use")
100 def _template_file_changed(self, name, old, new):
101 def _template_file_changed(self, name, old, new):
101 if new=='default':
102 if new=='default':
102 self.template_file = self.default_template
103 self.template_file = self.default_template
103 else:
104 else:
104 self.template_file = new
105 self.template_file = new
105 self.template = None
106 self.template = None
106 self._load_template()
107 self._load_template()
107
108
108 default_template = Unicode(u'')
109 default_template = Unicode(u'')
109 template = Any()
110 template = Any()
110 environment = Any()
111 environment = Any()
111
112
112 file_extension = Unicode(
113 file_extension = Unicode(
113 'txt', config=True,
114 'txt', config=True,
114 help="Extension of the file that should be written to disk"
115 help="Extension of the file that should be written to disk"
115 )
116 )
116
117
117 template_path = List(['.'], config=True)
118 template_path = List(['.'], config=True)
118 def _template_path_changed(self, name, old, new):
119 def _template_path_changed(self, name, old, new):
119 self._load_template()
120 self._load_template()
120
121
121 default_template_path = Unicode(
122 default_template_path = Unicode(
122 os.path.join("..", "templates"),
123 os.path.join("..", "templates"),
123 help="Path where the template files are located.")
124 help="Path where the template files are located.")
124
125
125 template_skeleton_path = Unicode(
126 template_skeleton_path = Unicode(
126 os.path.join("..", "templates", "skeleton"),
127 os.path.join("..", "templates", "skeleton"),
127 help="Path where the template skeleton files are located.")
128 help="Path where the template skeleton files are located.")
128
129
129 #Jinja block definitions
130 #Jinja block definitions
130 jinja_comment_block_start = Unicode("", config=True)
131 jinja_comment_block_start = Unicode("", config=True)
131 jinja_comment_block_end = Unicode("", config=True)
132 jinja_comment_block_end = Unicode("", config=True)
132 jinja_variable_block_start = Unicode("", config=True)
133 jinja_variable_block_start = Unicode("", config=True)
133 jinja_variable_block_end = Unicode("", config=True)
134 jinja_variable_block_end = Unicode("", config=True)
134 jinja_logic_block_start = Unicode("", config=True)
135 jinja_logic_block_start = Unicode("", config=True)
135 jinja_logic_block_end = Unicode("", config=True)
136 jinja_logic_block_end = Unicode("", config=True)
136
137
137 #Extension that the template files use.
138 #Extension that the template files use.
138 template_extension = Unicode(".tpl", config=True)
139 template_extension = Unicode(".tpl", config=True)
139
140
140 #Configurability, allows the user to easily add filters and transformers.
141 #Configurability, allows the user to easily add filters and transformers.
141 transformers = List(config=True,
142 transformers = List(config=True,
142 help="""List of transformers, by name or namespace, to enable.""")
143 help="""List of transformers, by name or namespace, to enable.""")
143
144
144 filters = Dict(config=True,
145 filters = Dict(config=True,
145 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
146 environment.""")
147 environment.""")
147
148
148 default_transformers = List([nbtransformers.coalesce_streams,
149 default_transformers = List([nbtransformers.coalesce_streams,
149 nbtransformers.SVG2PDFTransformer,
150 nbtransformers.SVG2PDFTransformer,
150 nbtransformers.ExtractOutputTransformer,
151 nbtransformers.ExtractOutputTransformer,
151 nbtransformers.CSSHTMLHeaderTransformer,
152 nbtransformers.CSSHTMLHeaderTransformer,
152 nbtransformers.RevealHelpTransformer,
153 nbtransformers.RevealHelpTransformer,
153 nbtransformers.LatexTransformer,
154 nbtransformers.LatexTransformer,
154 nbtransformers.SphinxTransformer],
155 nbtransformers.SphinxTransformer],
155 config=True,
156 config=True,
156 help="""List of transformers available by default, by name, namespace,
157 help="""List of transformers available by default, by name, namespace,
157 instance, or type.""")
158 instance, or type.""")
158
159
159
160
160 def __init__(self, config=None, extra_loaders=None, **kw):
161 def __init__(self, config=None, extra_loaders=None, **kw):
161 """
162 """
162 Public constructor
163 Public constructor
163
164
164 Parameters
165 Parameters
165 ----------
166 ----------
166 config : config
167 config : config
167 User configuration instance.
168 User configuration instance.
168 extra_loaders : list[of Jinja Loaders]
169 extra_loaders : list[of Jinja Loaders]
169 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
170 before the default FileSystem ones.
171 before the default FileSystem ones.
171 template : str (optional, kw arg)
172 template : str (optional, kw arg)
172 Template to use when exporting.
173 Template to use when exporting.
173 """
174 """
174 if not config:
175 if not config:
175 config = self.default_config
176 config = self.default_config
176
177
177 super(Exporter, self).__init__(config=config, **kw)
178 super(Exporter, self).__init__(config=config, **kw)
178
179
179 #Init
180 #Init
180 self._init_template()
181 self._init_template()
181 self._init_environment(extra_loaders=extra_loaders)
182 self._init_environment(extra_loaders=extra_loaders)
182 self._init_transformers()
183 self._init_transformers()
183 self._init_filters()
184 self._init_filters()
184
185
185
186
186 @property
187 @property
187 def default_config(self):
188 def default_config(self):
188 return Config()
189 return Config()
189
190
190 def _config_changed(self, name, old, new):
191 def _config_changed(self, name, old, new):
191 """When setting config, make sure to start with our default_config"""
192 """When setting config, make sure to start with our default_config"""
192 c = self.default_config
193 c = self.default_config
193 if new:
194 if new:
194 c.merge(new)
195 c.merge(new)
195 if c != old:
196 if c != old:
196 self.config = c
197 self.config = c
197 super(Exporter, self)._config_changed(name, old, c)
198 super(Exporter, self)._config_changed(name, old, c)
198
199
199
200
200 def _load_template(self):
201 def _load_template(self):
201 """Load the Jinja template object from the template file
202 """Load the Jinja template object from the template file
202
203
203 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,
204 or the Jinja environment is not setup yet.
205 or the Jinja environment is not setup yet.
205
206
206 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.
207 """
208 """
208 if self.template is not None:
209 if self.template is not None:
209 return
210 return
210 # called too early, do nothing
211 # called too early, do nothing
211 if self.environment is None:
212 if self.environment is None:
212 return
213 return
213 # Try different template names during conversion. First try to load the
214 # Try different template names during conversion. First try to load the
214 # template by name with extension added, then try loading the template
215 # template by name with extension added, then try loading the template
215 # 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
216 # '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.
217 module_name = self.__module__.rsplit('.', 1)[-1]
218 module_name = self.__module__.rsplit('.', 1)[-1]
218 try_names = []
219 try_names = []
219 if self.template_file:
220 if self.template_file:
220 try_names.extend([
221 try_names.extend([
221 self.template_file + self.template_extension,
222 self.template_file + self.template_extension,
222 self.template_file,
223 self.template_file,
223 module_name + '_' + self.template_file + self.template_extension,
224 module_name + '_' + self.template_file + self.template_extension,
224 ])
225 ])
225 try_names.append(module_name + self.template_extension)
226 try_names.append(module_name + self.template_extension)
226 for try_name in try_names:
227 for try_name in try_names:
227 self.log.debug("Attempting to load template %s", try_name)
228 self.log.debug("Attempting to load template %s", try_name)
228 try:
229 try:
229 self.template = self.environment.get_template(try_name)
230 self.template = self.environment.get_template(try_name)
230 except (TemplateNotFound, IOError):
231 except (TemplateNotFound, IOError):
231 pass
232 pass
232 except Exception as e:
233 except Exception as e:
233 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)
234 else:
235 else:
235 self.log.info("Loaded template %s", try_name)
236 self.log.info("Loaded template %s", try_name)
236 break
237 break
237
238
238 def from_notebook_node(self, nb, resources=None, **kw):
239 def from_notebook_node(self, nb, resources=None, **kw):
239 """
240 """
240 Convert a notebook from a notebook node instance.
241 Convert a notebook from a notebook node instance.
241
242
242 Parameters
243 Parameters
243 ----------
244 ----------
244 nb : Notebook node
245 nb : Notebook node
245 resources : dict (**kw)
246 resources : dict (**kw)
246 of additional resources that can be accessed read/write by
247 of additional resources that can be accessed read/write by
247 transformers and filters.
248 transformers and filters.
248 """
249 """
249 nb_copy = copy.deepcopy(nb)
250 nb_copy = copy.deepcopy(nb)
250 resources = self._init_resources(resources)
251 resources = self._init_resources(resources)
251
252
252 # Preprocess
253 # Preprocess
253 nb_copy, resources = self._transform(nb_copy, resources)
254 nb_copy, resources = self._transform(nb_copy, resources)
254
255
255 self._load_template()
256 self._load_template()
256
257
257 if self.template is not None:
258 if self.template is not None:
258 output = self.template.render(nb=nb_copy, resources=resources)
259 output = self.template.render(nb=nb_copy, resources=resources)
259 else:
260 else:
260 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)
261 return output, resources
262 return output, resources
262
263
263
264
264 def from_filename(self, filename, resources=None, **kw):
265 def from_filename(self, filename, resources=None, **kw):
265 """
266 """
266 Convert a notebook from a notebook file.
267 Convert a notebook from a notebook file.
267
268
268 Parameters
269 Parameters
269 ----------
270 ----------
270 filename : str
271 filename : str
271 Full filename of the notebook file to open and convert.
272 Full filename of the notebook file to open and convert.
272 """
273 """
273
274
274 #Pull the metadata from the filesystem.
275 #Pull the metadata from the filesystem.
275 if resources is None:
276 if resources is None:
276 resources = ResourcesDict()
277 resources = ResourcesDict()
277 if not 'metadata' in resources or resources['metadata'] == '':
278 if not 'metadata' in resources or resources['metadata'] == '':
278 resources['metadata'] = ResourcesDict()
279 resources['metadata'] = ResourcesDict()
279 basename = os.path.basename(filename)
280 basename = os.path.basename(filename)
280 notebook_name = basename[:basename.rfind('.')]
281 notebook_name = basename[:basename.rfind('.')]
281 resources['metadata']['name'] = notebook_name
282 resources['metadata']['name'] = notebook_name
282
283
283 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
284 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
284 resources['metadata']['modified_date'] = modified_date.strftime("%B %d, %Y")
285 resources['metadata']['modified_date'] = modified_date.strftime("%B %d, %Y")
285
286
286 with io.open(filename) as f:
287 with io.open(filename) as f:
287 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)
288
289
289
290
290 def from_file(self, file_stream, resources=None, **kw):
291 def from_file(self, file_stream, resources=None, **kw):
291 """
292 """
292 Convert a notebook from a notebook file.
293 Convert a notebook from a notebook file.
293
294
294 Parameters
295 Parameters
295 ----------
296 ----------
296 file_stream : file-like object
297 file_stream : file-like object
297 Notebook file-like object to convert.
298 Notebook file-like object to convert.
298 """
299 """
299 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)
300
301
301
302
302 def register_transformer(self, transformer, enabled=False):
303 def register_transformer(self, transformer, enabled=False):
303 """
304 """
304 Register a transformer.
305 Register a transformer.
305 Transformers are classes that act upon the notebook before it is
306 Transformers are classes that act upon the notebook before it is
306 passed into the Jinja templating engine. Transformers are also
307 passed into the Jinja templating engine. Transformers are also
307 capable of passing additional information to the Jinja
308 capable of passing additional information to the Jinja
308 templating engine.
309 templating engine.
309
310
310 Parameters
311 Parameters
311 ----------
312 ----------
312 transformer : transformer
313 transformer : transformer
313 """
314 """
314 if transformer is None:
315 if transformer is None:
315 raise TypeError('transformer')
316 raise TypeError('transformer')
316 isclass = isinstance(transformer, type)
317 isclass = isinstance(transformer, type)
317 constructed = not isclass
318 constructed = not isclass
318
319
319 #Handle transformer's registration based on it's type
320 #Handle transformer's registration based on it's type
320 if constructed and isinstance(transformer, py3compat.string_types):
321 if constructed and isinstance(transformer, py3compat.string_types):
321 #Transformer is a string, import the namespace and recursively call
322 #Transformer is a string, import the namespace and recursively call
322 #this register_transformer method
323 #this register_transformer method
323 transformer_cls = import_item(transformer)
324 transformer_cls = import_item(transformer)
324 return self.register_transformer(transformer_cls, enabled)
325 return self.register_transformer(transformer_cls, enabled)
325
326
326 if constructed and hasattr(transformer, '__call__'):
327 if constructed and hasattr(transformer, '__call__'):
327 #Transformer is a function, no need to construct it.
328 #Transformer is a function, no need to construct it.
328 #Register and return the transformer.
329 #Register and return the transformer.
329 if enabled:
330 if enabled:
330 transformer.enabled = True
331 transformer.enabled = True
331 self._transformers.append(transformer)
332 self._transformers.append(transformer)
332 return transformer
333 return transformer
333
334
334 elif isclass and isinstance(transformer, MetaHasTraits):
335 elif isclass and isinstance(transformer, MetaHasTraits):
335 #Transformer is configurable. Make sure to pass in new default for
336 #Transformer is configurable. Make sure to pass in new default for
336 #the enabled flag if one was specified.
337 #the enabled flag if one was specified.
337 self.register_transformer(transformer(parent=self), enabled)
338 self.register_transformer(transformer(parent=self), enabled)
338
339
339 elif isclass:
340 elif isclass:
340 #Transformer is not configurable, construct it
341 #Transformer is not configurable, construct it
341 self.register_transformer(transformer(), enabled)
342 self.register_transformer(transformer(), enabled)
342
343
343 else:
344 else:
344 #Transformer is an instance of something without a __call__
345 #Transformer is an instance of something without a __call__
345 #attribute.
346 #attribute.
346 raise TypeError('transformer')
347 raise TypeError('transformer')
347
348
348
349
349 def register_filter(self, name, jinja_filter):
350 def register_filter(self, name, jinja_filter):
350 """
351 """
351 Register a filter.
352 Register a filter.
352 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.
353 The filters are accesible within the Jinja templating engine.
354 The filters are accesible within the Jinja templating engine.
354
355
355 Parameters
356 Parameters
356 ----------
357 ----------
357 name : str
358 name : str
358 name to give the filter in the Jinja engine
359 name to give the filter in the Jinja engine
359 filter : filter
360 filter : filter
360 """
361 """
361 if jinja_filter is None:
362 if jinja_filter is None:
362 raise TypeError('filter')
363 raise TypeError('filter')
363 isclass = isinstance(jinja_filter, type)
364 isclass = isinstance(jinja_filter, type)
364 constructed = not isclass
365 constructed = not isclass
365
366
366 #Handle filter's registration based on it's type
367 #Handle filter's registration based on it's type
367 if constructed and isinstance(jinja_filter, py3compat.string_types):
368 if constructed and isinstance(jinja_filter, py3compat.string_types):
368 #filter is a string, import the namespace and recursively call
369 #filter is a string, import the namespace and recursively call
369 #this register_filter method
370 #this register_filter method
370 filter_cls = import_item(jinja_filter)
371 filter_cls = import_item(jinja_filter)
371 return self.register_filter(name, filter_cls)
372 return self.register_filter(name, filter_cls)
372
373
373 if constructed and hasattr(jinja_filter, '__call__'):
374 if constructed and hasattr(jinja_filter, '__call__'):
374 #filter is a function, no need to construct it.
375 #filter is a function, no need to construct it.
375 self.environment.filters[name] = jinja_filter
376 self.environment.filters[name] = jinja_filter
376 return jinja_filter
377 return jinja_filter
377
378
378 elif isclass and isinstance(jinja_filter, MetaHasTraits):
379 elif isclass and isinstance(jinja_filter, MetaHasTraits):
379 #filter is configurable. Make sure to pass in new default for
380 #filter is configurable. Make sure to pass in new default for
380 #the enabled flag if one was specified.
381 #the enabled flag if one was specified.
381 filter_instance = jinja_filter(parent=self)
382 filter_instance = jinja_filter(parent=self)
382 self.register_filter(name, filter_instance )
383 self.register_filter(name, filter_instance )
383
384
384 elif isclass:
385 elif isclass:
385 #filter is not configurable, construct it
386 #filter is not configurable, construct it
386 filter_instance = jinja_filter()
387 filter_instance = jinja_filter()
387 self.register_filter(name, filter_instance)
388 self.register_filter(name, filter_instance)
388
389
389 else:
390 else:
390 #filter is an instance of something without a __call__
391 #filter is an instance of something without a __call__
391 #attribute.
392 #attribute.
392 raise TypeError('filter')
393 raise TypeError('filter')
393
394
394
395
395 def _init_template(self):
396 def _init_template(self):
396 """
397 """
397 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
398 build one from the information we know.
399 build one from the information we know.
399 """
400 """
400 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)
401
402
402
403
403 def _init_environment(self, extra_loaders=None):
404 def _init_environment(self, extra_loaders=None):
404 """
405 """
405 Create the Jinja templating environment.
406 Create the Jinja templating environment.
406 """
407 """
407 here = os.path.dirname(os.path.realpath(__file__))
408 here = os.path.dirname(os.path.realpath(__file__))
408 loaders = []
409 loaders = []
409 if extra_loaders:
410 if extra_loaders:
410 loaders.extend(extra_loaders)
411 loaders.extend(extra_loaders)
411
412
412 paths = self.template_path
413 paths = self.template_path
413 paths.extend([os.path.join(here, self.default_template_path),
414 paths.extend([os.path.join(here, self.default_template_path),
414 os.path.join(here, self.template_skeleton_path)])
415 os.path.join(here, self.template_skeleton_path)])
415 loaders.append(FileSystemLoader(paths))
416 loaders.append(FileSystemLoader(paths))
416
417
417 self.environment = Environment(
418 self.environment = Environment(
418 loader= ChoiceLoader(loaders),
419 loader= ChoiceLoader(loaders),
419 extensions=JINJA_EXTENSIONS
420 extensions=JINJA_EXTENSIONS
420 )
421 )
421
422
422 #Set special Jinja2 syntax that will not conflict with latex.
423 #Set special Jinja2 syntax that will not conflict with latex.
423 if self.jinja_logic_block_start:
424 if self.jinja_logic_block_start:
424 self.environment.block_start_string = self.jinja_logic_block_start
425 self.environment.block_start_string = self.jinja_logic_block_start
425 if self.jinja_logic_block_end:
426 if self.jinja_logic_block_end:
426 self.environment.block_end_string = self.jinja_logic_block_end
427 self.environment.block_end_string = self.jinja_logic_block_end
427 if self.jinja_variable_block_start:
428 if self.jinja_variable_block_start:
428 self.environment.variable_start_string = self.jinja_variable_block_start
429 self.environment.variable_start_string = self.jinja_variable_block_start
429 if self.jinja_variable_block_end:
430 if self.jinja_variable_block_end:
430 self.environment.variable_end_string = self.jinja_variable_block_end
431 self.environment.variable_end_string = self.jinja_variable_block_end
431 if self.jinja_comment_block_start:
432 if self.jinja_comment_block_start:
432 self.environment.comment_start_string = self.jinja_comment_block_start
433 self.environment.comment_start_string = self.jinja_comment_block_start
433 if self.jinja_comment_block_end:
434 if self.jinja_comment_block_end:
434 self.environment.comment_end_string = self.jinja_comment_block_end
435 self.environment.comment_end_string = self.jinja_comment_block_end
435
436
436
437
437 def _init_transformers(self):
438 def _init_transformers(self):
438 """
439 """
439 Register all of the transformers needed for this exporter, disabled
440 Register all of the transformers needed for this exporter, disabled
440 unless specified explicitly.
441 unless specified explicitly.
441 """
442 """
442 self._transformers = []
443 self._transformers = []
443
444
444 #Load default transformers (not necessarly enabled by default).
445 #Load default transformers (not necessarly enabled by default).
445 if self.default_transformers:
446 if self.default_transformers:
446 for transformer in self.default_transformers:
447 for transformer in self.default_transformers:
447 self.register_transformer(transformer)
448 self.register_transformer(transformer)
448
449
449 #Load user transformers. Enable by default.
450 #Load user transformers. Enable by default.
450 if self.transformers:
451 if self.transformers:
451 for transformer in self.transformers:
452 for transformer in self.transformers:
452 self.register_transformer(transformer, enabled=True)
453 self.register_transformer(transformer, enabled=True)
453
454
454
455
455 def _init_filters(self):
456 def _init_filters(self):
456 """
457 """
457 Register all of the filters required for the exporter.
458 Register all of the filters required for the exporter.
458 """
459 """
459
460
460 #Add default filters to the Jinja2 environment
461 #Add default filters to the Jinja2 environment
461 for key, value in default_filters.items():
462 for key, value in default_filters.items():
462 self.register_filter(key, value)
463 self.register_filter(key, value)
463
464
464 #Load user filters. Overwrite existing filters if need be.
465 #Load user filters. Overwrite existing filters if need be.
465 if self.filters:
466 if self.filters:
466 for key, user_filter in self.filters.items():
467 for key, user_filter in self.filters.items():
467 self.register_filter(key, user_filter)
468 self.register_filter(key, user_filter)
468
469
469
470
470 def _init_resources(self, resources):
471 def _init_resources(self, resources):
471
472
472 #Make sure the resources dict is of ResourcesDict type.
473 #Make sure the resources dict is of ResourcesDict type.
473 if resources is None:
474 if resources is None:
474 resources = ResourcesDict()
475 resources = ResourcesDict()
475 if not isinstance(resources, ResourcesDict):
476 if not isinstance(resources, ResourcesDict):
476 new_resources = ResourcesDict()
477 new_resources = ResourcesDict()
477 new_resources.update(resources)
478 new_resources.update(resources)
478 resources = new_resources
479 resources = new_resources
479
480
480 #Make sure the metadata extension exists in resources
481 #Make sure the metadata extension exists in resources
481 if 'metadata' in resources:
482 if 'metadata' in resources:
482 if not isinstance(resources['metadata'], ResourcesDict):
483 if not isinstance(resources['metadata'], ResourcesDict):
483 resources['metadata'] = ResourcesDict(resources['metadata'])
484 resources['metadata'] = ResourcesDict(resources['metadata'])
484 else:
485 else:
485 resources['metadata'] = ResourcesDict()
486 resources['metadata'] = ResourcesDict()
486 if not resources['metadata']['name']:
487 if not resources['metadata']['name']:
487 resources['metadata']['name'] = 'Notebook'
488 resources['metadata']['name'] = 'Notebook'
488
489
489 #Set the output extension
490 #Set the output extension
490 resources['output_extension'] = self.file_extension
491 resources['output_extension'] = self.file_extension
491 return resources
492 return resources
492
493
493
494
494 def _transform(self, nb, resources):
495 def _transform(self, nb, resources):
495 """
496 """
496 Preprocess the notebook before passing it into the Jinja engine.
497 Preprocess the notebook before passing it into the Jinja engine.
497 To preprocess the notebook is to apply all of the
498 To preprocess the notebook is to apply all of the
498
499
499 Parameters
500 Parameters
500 ----------
501 ----------
501 nb : notebook node
502 nb : notebook node
502 notebook that is being exported.
503 notebook that is being exported.
503 resources : a dict of additional resources that
504 resources : a dict of additional resources that
504 can be accessed read/write by transformers
505 can be accessed read/write by transformers
505 and filters.
506 and filters.
506 """
507 """
507
508
508 # Do a copy.deepcopy first,
509 # Do a copy.deepcopy first,
509 # we are never safe enough with what the transformers could do.
510 # we are never safe enough with what the transformers could do.
510 nbc = copy.deepcopy(nb)
511 nbc = copy.deepcopy(nb)
511 resc = copy.deepcopy(resources)
512 resc = copy.deepcopy(resources)
512
513
513 #Run each transformer on the notebook. Carry the output along
514 #Run each transformer on the notebook. Carry the output along
514 #to each transformer
515 #to each transformer
515 for transformer in self._transformers:
516 for transformer in self._transformers:
516 nbc, resc = transformer(nbc, resc)
517 nbc, resc = transformer(nbc, resc)
517 return nbc, resc
518 return nbc, resc
@@ -1,171 +1,183 b''
1 # coding: utf-8
1 # coding: utf-8
2 """String filters.
2 """String filters.
3
3
4 Contains a collection of useful string manipulation filters for use in Jinja
4 Contains a collection of useful string manipulation filters for use in Jinja
5 templates.
5 templates.
6 """
6 """
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (c) 2013, the IPython Development Team.
8 # Copyright (c) 2013, the IPython Development Team.
9 #
9 #
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11 #
11 #
12 # The full license is in the file COPYING.txt, distributed with this software.
12 # The full license is in the file COPYING.txt, distributed with this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import re
20 import re
20 import textwrap
21 import textwrap
21 from xml.etree import ElementTree
22 from xml.etree import ElementTree
22
23
23 from IPython.core.interactiveshell import InteractiveShell
24 from IPython.core.interactiveshell import InteractiveShell
24 from IPython.utils import py3compat
25 from IPython.utils import py3compat
25
26
26 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
27 # Functions
28 # Functions
28 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
29
30
30 __all__ = [
31 __all__ = [
31 'wrap_text',
32 'wrap_text',
32 'html2text',
33 'html2text',
33 'add_anchor',
34 'add_anchor',
34 'strip_dollars',
35 'strip_dollars',
35 'strip_files_prefix',
36 'strip_files_prefix',
36 'comment_lines',
37 'comment_lines',
37 'get_lines',
38 'get_lines',
38 'ipython2python',
39 'ipython2python',
40 'posix_path',
39 ]
41 ]
40
42
41
43
42 def wrap_text(text, width=100):
44 def wrap_text(text, width=100):
43 """
45 """
44 Intelligently wrap text.
46 Intelligently wrap text.
45 Wrap text without breaking words if possible.
47 Wrap text without breaking words if possible.
46
48
47 Parameters
49 Parameters
48 ----------
50 ----------
49 text : str
51 text : str
50 Text to wrap.
52 Text to wrap.
51 width : int, optional
53 width : int, optional
52 Number of characters to wrap to, default 100.
54 Number of characters to wrap to, default 100.
53 """
55 """
54
56
55 split_text = text.split('\n')
57 split_text = text.split('\n')
56 wrp = map(lambda x:textwrap.wrap(x,width), split_text)
58 wrp = map(lambda x:textwrap.wrap(x,width), split_text)
57 wrpd = map('\n'.join, wrp)
59 wrpd = map('\n'.join, wrp)
58 return '\n'.join(wrpd)
60 return '\n'.join(wrpd)
59
61
60
62
61 def html2text(element):
63 def html2text(element):
62 """extract inner text from html
64 """extract inner text from html
63
65
64 Analog of jQuery's $(element).text()
66 Analog of jQuery's $(element).text()
65 """
67 """
66 if isinstance(element, py3compat.string_types):
68 if isinstance(element, py3compat.string_types):
67 element = ElementTree.fromstring(element)
69 element = ElementTree.fromstring(element)
68
70
69 text = element.text or ""
71 text = element.text or ""
70 for child in element:
72 for child in element:
71 text += html2text(child)
73 text += html2text(child)
72 text += (element.tail or "")
74 text += (element.tail or "")
73 return text
75 return text
74
76
75
77
76 def add_anchor(html):
78 def add_anchor(html):
77 """Add an anchor-link to an html header tag
79 """Add an anchor-link to an html header tag
78
80
79 For use in heading cells
81 For use in heading cells
80 """
82 """
81 h = ElementTree.fromstring(py3compat.cast_bytes_py2(html))
83 h = ElementTree.fromstring(py3compat.cast_bytes_py2(html))
82 link = html2text(h).replace(' ', '-')
84 link = html2text(h).replace(' ', '-')
83 h.set('id', link)
85 h.set('id', link)
84 a = ElementTree.Element("a", {"class" : "anchor-link", "href" : "#" + link})
86 a = ElementTree.Element("a", {"class" : "anchor-link", "href" : "#" + link})
85 a.text = u'ΒΆ'
87 a.text = u'ΒΆ'
86 h.append(a)
88 h.append(a)
87
89
88 # Known issue of Python3.x, ElementTree.tostring() returns a byte string
90 # Known issue of Python3.x, ElementTree.tostring() returns a byte string
89 # instead of a text string. See issue http://bugs.python.org/issue10942
91 # instead of a text string. See issue http://bugs.python.org/issue10942
90 # Workaround is to make sure the bytes are casted to a string.
92 # Workaround is to make sure the bytes are casted to a string.
91 return py3compat.decode(ElementTree.tostring(h), 'utf-8')
93 return py3compat.decode(ElementTree.tostring(h), 'utf-8')
92
94
93
95
94 def strip_dollars(text):
96 def strip_dollars(text):
95 """
97 """
96 Remove all dollar symbols from text
98 Remove all dollar symbols from text
97
99
98 Parameters
100 Parameters
99 ----------
101 ----------
100 text : str
102 text : str
101 Text to remove dollars from
103 Text to remove dollars from
102 """
104 """
103
105
104 return text.strip('$')
106 return text.strip('$')
105
107
106
108
107 files_url_pattern = re.compile(r'(src|href)\=([\'"]?)files/')
109 files_url_pattern = re.compile(r'(src|href)\=([\'"]?)files/')
108
110
109 def strip_files_prefix(text):
111 def strip_files_prefix(text):
110 """
112 """
111 Fix all fake URLs that start with `files/`,
113 Fix all fake URLs that start with `files/`,
112 stripping out the `files/` prefix.
114 stripping out the `files/` prefix.
113
115
114 Parameters
116 Parameters
115 ----------
117 ----------
116 text : str
118 text : str
117 Text in which to replace 'src="files/real...' with 'src="real...'
119 Text in which to replace 'src="files/real...' with 'src="real...'
118 """
120 """
119 return files_url_pattern.sub(r"\1=\2", text)
121 return files_url_pattern.sub(r"\1=\2", text)
120
122
121
123
122 def comment_lines(text, prefix='# '):
124 def comment_lines(text, prefix='# '):
123 """
125 """
124 Build a Python comment line from input text.
126 Build a Python comment line from input text.
125
127
126 Parameters
128 Parameters
127 ----------
129 ----------
128 text : str
130 text : str
129 Text to comment out.
131 Text to comment out.
130 prefix : str
132 prefix : str
131 Character to append to the start of each line.
133 Character to append to the start of each line.
132 """
134 """
133
135
134 #Replace line breaks with line breaks and comment symbols.
136 #Replace line breaks with line breaks and comment symbols.
135 #Also add a comment symbol at the beginning to comment out
137 #Also add a comment symbol at the beginning to comment out
136 #the first line.
138 #the first line.
137 return prefix + ('\n'+prefix).join(text.split('\n'))
139 return prefix + ('\n'+prefix).join(text.split('\n'))
138
140
139
141
140 def get_lines(text, start=None,end=None):
142 def get_lines(text, start=None,end=None):
141 """
143 """
142 Split the input text into separate lines and then return the
144 Split the input text into separate lines and then return the
143 lines that the caller is interested in.
145 lines that the caller is interested in.
144
146
145 Parameters
147 Parameters
146 ----------
148 ----------
147 text : str
149 text : str
148 Text to parse lines from.
150 Text to parse lines from.
149 start : int, optional
151 start : int, optional
150 First line to grab from.
152 First line to grab from.
151 end : int, optional
153 end : int, optional
152 Last line to grab from.
154 Last line to grab from.
153 """
155 """
154
156
155 # Split the input into lines.
157 # Split the input into lines.
156 lines = text.split("\n")
158 lines = text.split("\n")
157
159
158 # Return the right lines.
160 # Return the right lines.
159 return "\n".join(lines[start:end]) #re-join
161 return "\n".join(lines[start:end]) #re-join
160
162
161 def ipython2python(code):
163 def ipython2python(code):
162 """Transform IPython syntax to pure Python syntax
164 """Transform IPython syntax to pure Python syntax
163
165
164 Parameters
166 Parameters
165 ----------
167 ----------
166
168
167 code : str
169 code : str
168 IPython code, to be transformed to pure Python
170 IPython code, to be transformed to pure Python
169 """
171 """
170 shell = InteractiveShell.instance()
172 shell = InteractiveShell.instance()
171 return shell.input_transformer_manager.transform_cell(code)
173 return shell.input_transformer_manager.transform_cell(code)
174
175 def posix_path(path):
176 """Turn a path into posix-style path/to/etc
177
178 Mainly for use in latex on Windows,
179 where native Windows paths are not allowed.
180 """
181 if os.path.sep != '/':
182 return path.replace(os.path.sep, '/')
183 return path
@@ -1,119 +1,127 b''
1 """
1 """
2 Module with tests for Strings
2 Module with tests for Strings
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 IPython.testing import decorators as dec
17 from IPython.testing import decorators as dec
18 from ...tests.base import TestsBase
18 from ...tests.base import TestsBase
19 from ..strings import (wrap_text, html2text, add_anchor, strip_dollars,
19 from ..strings import (wrap_text, html2text, add_anchor, strip_dollars,
20 strip_files_prefix, get_lines, comment_lines, ipython2python)
20 strip_files_prefix, get_lines, comment_lines, ipython2python, posix_path,
21 )
21
22
22
23
23 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
24 # Class
25 # Class
25 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
26
27
27 class TestStrings(TestsBase):
28 class TestStrings(TestsBase):
28
29
29 def test_wrap_text(self):
30 def test_wrap_text(self):
30 """wrap_text test"""
31 """wrap_text test"""
31 test_text = """
32 test_text = """
32 Tush! never tell me; I take it much unkindly
33 Tush! never tell me; I take it much unkindly
33 That thou, Iago, who hast had my purse
34 That thou, Iago, who hast had my purse
34 As if the strings were thine, shouldst know of this.
35 As if the strings were thine, shouldst know of this.
35 """
36 """
36 for length in [30,5,1]:
37 for length in [30,5,1]:
37 yield self._confirm_wrap_text(test_text, length)
38 yield self._confirm_wrap_text(test_text, length)
38
39
39
40
40 def _confirm_wrap_text(self, text, length):
41 def _confirm_wrap_text(self, text, length):
41 for line in wrap_text(text, length).split('\n'):
42 for line in wrap_text(text, length).split('\n'):
42 assert len(line) <= length
43 assert len(line) <= length
43
44
44
45
45 def test_html2text(self):
46 def test_html2text(self):
46 """html2text test"""
47 """html2text test"""
47 #TODO: More tests
48 #TODO: More tests
48 self.assertEqual(html2text('<name>joe</name>'), 'joe')
49 self.assertEqual(html2text('<name>joe</name>'), 'joe')
49
50
50
51
51 def test_add_anchor(self):
52 def test_add_anchor(self):
52 """add_anchor test"""
53 """add_anchor test"""
53 #TODO: More tests
54 #TODO: More tests
54 results = add_anchor('<b>Hello World!</b>')
55 results = add_anchor('<b>Hello World!</b>')
55 assert 'Hello World!' in results
56 assert 'Hello World!' in results
56 assert 'id="' in results
57 assert 'id="' in results
57 assert 'class="anchor-link"' in results
58 assert 'class="anchor-link"' in results
58 assert '<b' in results
59 assert '<b' in results
59 assert '</b>' in results
60 assert '</b>' in results
60
61
61
62
62 def test_strip_dollars(self):
63 def test_strip_dollars(self):
63 """strip_dollars test"""
64 """strip_dollars test"""
64 tests = [
65 tests = [
65 ('', ''),
66 ('', ''),
66 ('$$', ''),
67 ('$$', ''),
67 ('$H$', 'H'),
68 ('$H$', 'H'),
68 ('$He', 'He'),
69 ('$He', 'He'),
69 ('H$el', 'H$el'),
70 ('H$el', 'H$el'),
70 ('Hell$', 'Hell'),
71 ('Hell$', 'Hell'),
71 ('Hello', 'Hello'),
72 ('Hello', 'Hello'),
72 ('W$o$rld', 'W$o$rld')]
73 ('W$o$rld', 'W$o$rld')]
73 for test in tests:
74 for test in tests:
74 yield self._try_strip_dollars(test[0], test[1])
75 yield self._try_strip_dollars(test[0], test[1])
75
76
76
77
77 def _try_strip_dollars(self, test, result):
78 def _try_strip_dollars(self, test, result):
78 self.assertEqual(strip_dollars(test), result)
79 self.assertEqual(strip_dollars(test), result)
79
80
80
81
81 def test_strip_files_prefix(self):
82 def test_strip_files_prefix(self):
82 """strip_files_prefix test"""
83 """strip_files_prefix test"""
83 tests = [
84 tests = [
84 ('', ''),
85 ('', ''),
85 ('/files', '/files'),
86 ('/files', '/files'),
86 ('test="/files"', 'test="/files"'),
87 ('test="/files"', 'test="/files"'),
87 ('My files are in `files/`', 'My files are in `files/`'),
88 ('My files are in `files/`', 'My files are in `files/`'),
88 ('<a href="files/test.html">files/test.html</a>', '<a href="test.html">files/test.html</a>')]
89 ('<a href="files/test.html">files/test.html</a>', '<a href="test.html">files/test.html</a>')]
89 for test in tests:
90 for test in tests:
90 yield self._try_files_prefix(test[0], test[1])
91 yield self._try_files_prefix(test[0], test[1])
91
92
92
93
93 def _try_files_prefix(self, test, result):
94 def _try_files_prefix(self, test, result):
94 self.assertEqual(strip_files_prefix(test), result)
95 self.assertEqual(strip_files_prefix(test), result)
95
96
96
97
97 def test_comment_lines(self):
98 def test_comment_lines(self):
98 """comment_lines test"""
99 """comment_lines test"""
99 for line in comment_lines('hello\nworld\n!').split('\n'):
100 for line in comment_lines('hello\nworld\n!').split('\n'):
100 assert line.startswith('# ')
101 assert line.startswith('# ')
101 for line in comment_lines('hello\nworld\n!', 'beep').split('\n'):
102 for line in comment_lines('hello\nworld\n!', 'beep').split('\n'):
102 assert line.startswith('beep')
103 assert line.startswith('beep')
103
104
104
105
105 def test_get_lines(self):
106 def test_get_lines(self):
106 """get_lines test"""
107 """get_lines test"""
107 text = "hello\nworld\n!"
108 text = "hello\nworld\n!"
108 self.assertEqual(get_lines(text, start=1), "world\n!")
109 self.assertEqual(get_lines(text, start=1), "world\n!")
109 self.assertEqual(get_lines(text, end=2), "hello\nworld")
110 self.assertEqual(get_lines(text, end=2), "hello\nworld")
110 self.assertEqual(get_lines(text, start=2, end=5), "!")
111 self.assertEqual(get_lines(text, start=2, end=5), "!")
111 self.assertEqual(get_lines(text, start=-2), "world\n!")
112 self.assertEqual(get_lines(text, start=-2), "world\n!")
112
113
113
114
114 def test_ipython2python(self):
115 def test_ipython2python(self):
115 """ipython2python test"""
116 """ipython2python test"""
116 #TODO: More tests
117 #TODO: More tests
117 results = ipython2python(u'%%pylab\nprint("Hello-World")').replace("u'", "'")
118 results = ipython2python(u'%%pylab\nprint("Hello-World")').replace("u'", "'")
118 self.fuzzy_compare(results, u"get_ipython().run_cell_magic('pylab', '', 'print(\"Hello-World\")')",
119 self.fuzzy_compare(results, u"get_ipython().run_cell_magic('pylab', '', 'print(\"Hello-World\")')",
119 ignore_spaces=True, ignore_newlines=True)
120 ignore_spaces=True, ignore_newlines=True)
121
122 def test_posix_path(self):
123 path_list = ['foo', 'bar']
124 expected = '/'.join(path_list)
125 native = os.path.join(path_list)
126 filtered = posix_path(native)
127 self.assertEqual(filtered, expected)
General Comments 0
You need to be logged in to leave comments. Login now