##// END OF EJS Templates
full inkscape path on OS X...
Matthias BUSSONNIER -
Show More
@@ -1,705 +1,711 b''
1 1 #!/usr/bin/env python
2 2 """Convert IPython notebooks to other formats, such as ReST, and HTML.
3 3
4 4 Example:
5 5 ./nbconvert.py --format html file.ipynb
6 6
7 7 Produces 'file.rst' and 'file.html', along with auto-generated figure files
8 8 called nb_figure_NN.png. To avoid the two-step process, ipynb -> rst -> html,
9 9 use '--format quick-html' which will do ipynb -> html, but won't look as
10 10 pretty.
11 11 """
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from __future__ import print_function
16 16
17 17 # Stdlib
18 18 import codecs
19 19 import logging
20 20 import os
21 21 import pprint
22 22 import re
23 23 import subprocess
24 24 import sys
25 25
26 inkscape = 'inkscape'
27 if sys.platform == 'darwin':
28 inkscape = '/Applications/Inkscape.app/Contents/Resources/bin/inkscape'
29 if not os.path.exists(inkscape):
30 inkscape = None
31
26 32 # From IPython
27 33 from IPython.external import argparse
28 34 from IPython.nbformat import current as nbformat
29 35 from IPython.utils.text import indent
30 36 from decorators import DocInherit
31 37
32 38 #-----------------------------------------------------------------------------
33 39 # Utility functions
34 40 #-----------------------------------------------------------------------------
35 41
36 42 def remove_fake_files_url(cell):
37 43 """Remove from the cell source the /files/ pseudo-path we use.
38 44 """
39 45 src = cell.source
40 46 cell.source = src.replace('/files/', '')
41 47
42 48
43 49 def remove_ansi(src):
44 50 """Strip all ANSI color escape sequences from input string.
45 51
46 52 Parameters
47 53 ----------
48 54 src : string
49 55
50 56 Returns
51 57 -------
52 58 string
53 59 """
54 60 return re.sub(r'\033\[(0|\d;\d\d)m', '', src)
55 61
56 62
57 63 # Pandoc-dependent code
58 64 def markdown2latex(src):
59 65 """Convert a markdown string to LaTeX via pandoc.
60 66
61 67 This function will raise an error if pandoc is not installed.
62 68
63 69 Any error messages generated by pandoc are printed to stderr.
64 70
65 71 Parameters
66 72 ----------
67 73 src : string
68 74 Input string, assumed to be valid markdown.
69 75
70 76 Returns
71 77 -------
72 78 out : string
73 79 Output as returned by pandoc.
74 80 """
75 81 p = subprocess.Popen('pandoc -f markdown -t latex'.split(),
76 82 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
77 83 out, err = p.communicate(src)
78 84 if err:
79 85 print(err, file=sys.stderr)
80 86 #print('*'*20+'\n', out, '\n'+'*'*20) # dbg
81 87 return out
82 88
83 89
84 90 def rst_directive(directive, text=''):
85 91 out = [directive, '']
86 92 if text:
87 93 out.extend([indent(text), ''])
88 94 return out
89 95
90 96 #-----------------------------------------------------------------------------
91 97 # Class declarations
92 98 #-----------------------------------------------------------------------------
93 99
94 100 class ConversionException(Exception):
95 101 pass
96 102
97 103
98 104 class Converter(object):
99 105 default_encoding = 'utf-8'
100 106 extension = str()
101 107 figures_counter = 0
102 108 infile = str()
103 109 infile_dir = str()
104 110 infile_root = str()
105 111 files_dir = str()
106 112 with_preamble = True
107 113 user_preamble = None
108 114 output = str()
109 115 raw_as_verbatim = False
110 116
111 117 def __init__(self, infile):
112 118 self.infile = infile
113 119 self.infile_dir = os.path.dirname(infile)
114 120 infile_root = os.path.splitext(infile)[0]
115 121 files_dir = infile_root + '_files'
116 122 if not os.path.isdir(files_dir):
117 123 os.mkdir(files_dir)
118 124 self.infile_root = infile_root
119 125 self.files_dir = files_dir
120 126
121 127 def dispatch(self, cell_type):
122 128 """return cell_type dependent render method, for example render_code
123 129 """
124 130 return getattr(self, 'render_' + cell_type, self.render_unknown)
125 131
126 132 def convert(self):
127 133 lines = []
128 134 lines.extend(self.optional_header())
129 135 for worksheet in self.nb.worksheets:
130 136 for cell in worksheet.cells:
131 137 #print(cell.cell_type) # dbg
132 138 conv_fn = self.dispatch(cell.cell_type)
133 139 if cell.cell_type in ('markdown', 'raw'):
134 140 remove_fake_files_url(cell)
135 141 lines.extend(conv_fn(cell))
136 142 lines.append('')
137 143 lines.extend(self.optional_footer())
138 144 return '\n'.join(lines)
139 145
140 146 def render(self):
141 147 "read, convert, and save self.infile"
142 148 self.read()
143 149 self.output = self.convert()
144 150 return self.save()
145 151
146 152 def read(self):
147 153 "read and parse notebook into NotebookNode called self.nb"
148 154 with open(self.infile) as f:
149 155 self.nb = nbformat.read(f, 'json')
150 156
151 157 def save(self, infile=None, encoding=None):
152 158 "read and parse notebook into self.nb"
153 159 if infile is None:
154 160 infile = os.path.splitext(self.infile)[0] + '.' + self.extension
155 161 if encoding is None:
156 162 encoding = self.default_encoding
157 163 with open(infile, 'w') as f:
158 164 f.write(self.output.encode(encoding))
159 165 return infile
160 166
161 167 def optional_header(self):
162 168 return []
163 169
164 170 def optional_footer(self):
165 171 return []
166 172
167 173 def _new_figure(self, data, fmt):
168 174 """Create a new figure file in the given format.
169 175
170 176 Returns a path relative to the input file.
171 177 """
172 178 figname = '%s_fig_%02i.%s' % (self.infile_root,
173 179 self.figures_counter, fmt)
174 180 self.figures_counter += 1
175 181 fullname = os.path.join(self.files_dir, figname)
176 182
177 183 # Binary files are base64-encoded, SVG is already XML
178 184 if fmt in ('png', 'jpg', 'pdf'):
179 185 data = data.decode('base64')
180 186 fopen = lambda fname: open(fname, 'wb')
181 187 else:
182 188 fopen = lambda fname: codecs.open(fname, 'wb', self.default_encoding)
183 189
184 190 with fopen(fullname) as f:
185 191 f.write(data)
186 192
187 193 return fullname
188 194
189 195 def render_heading(self, cell):
190 196 """convert a heading cell
191 197
192 198 Returns list."""
193 199 raise NotImplementedError
194 200
195 201 def render_code(self, cell):
196 202 """Convert a code cell
197 203
198 204 Returns list."""
199 205 raise NotImplementedError
200 206
201 207 def render_markdown(self, cell):
202 208 """convert a markdown cell
203 209
204 210 Returns list."""
205 211 raise NotImplementedError
206 212
207 213 def render_pyout(self, output):
208 214 """convert pyout part of a code cell
209 215
210 216 Returns list."""
211 217 raise NotImplementedError
212 218
213 219
214 220 def render_pyerr(self, output):
215 221 """convert pyerr part of a code cell
216 222
217 223 Returns list."""
218 224 raise NotImplementedError
219 225
220 226 def _img_lines(self, img_file):
221 227 """Return list of lines to include an image file."""
222 228 # Note: subclasses may choose to implement format-specific _FMT_lines
223 229 # methods if they so choose (FMT in {png, svg, jpg, pdf}).
224 230 raise NotImplementedError
225 231
226 232 def render_display_data(self, output):
227 233 """convert display data from the output of a code cell
228 234
229 235 Returns list.
230 236 """
231 237 lines = []
232 238
233 239 for fmt in ['png', 'svg', 'jpg', 'pdf']:
234 240 if fmt in output:
235 241 img_file = self._new_figure(output[fmt], fmt)
236 242 # Subclasses can have format-specific render functions (e.g.,
237 243 # latex has to auto-convert all SVG to PDF first).
238 244 lines_fun = getattr(self, '_%s_lines' % fmt, None)
239 245 if not lines_fun:
240 246 lines_fun = self._img_lines
241 247 lines.extend(lines_fun(img_file))
242 248
243 249 return lines
244 250
245 251 def render_stream(self, cell):
246 252 """convert stream part of a code cell
247 253
248 254 Returns list."""
249 255 raise NotImplementedError
250 256
251 257 def render_raw(self, cell):
252 258 """convert a cell with raw text
253 259
254 260 Returns list."""
255 261 raise NotImplementedError
256 262
257 263 def render_unknown(self, cell):
258 264 """Render cells of unkown type
259 265
260 266 Returns list."""
261 267 data = pprint.pformat(cell)
262 268 logging.warning('Unknown cell:\n%s' % data)
263 269 return self._unknown_lines(data)
264 270
265 271 def _unknown_lines(self, data):
266 272 """Return list of lines for an unknown cell.
267 273
268 274 Parameters
269 275 ----------
270 276 data : str
271 277 The content of the unknown data as a single string.
272 278 """
273 279 raise NotImplementedError
274 280
275 281
276 282 class ConverterRST(Converter):
277 283 extension = 'rst'
278 284 heading_level = {1: '=', 2: '-', 3: '`', 4: '\'', 5: '.', 6: '~'}
279 285
280 286 @DocInherit
281 287 def render_heading(self, cell):
282 288 marker = self.heading_level[cell.level]
283 289 return ['{0}\n{1}\n'.format(cell.source, marker * len(cell.source))]
284 290
285 291 @DocInherit
286 292 def render_code(self, cell):
287 293 if not cell.input:
288 294 return []
289 295
290 296 lines = ['In[%s]:' % cell.prompt_number, '']
291 297 lines.extend(rst_directive('.. code:: python', cell.input))
292 298
293 299 for output in cell.outputs:
294 300 conv_fn = self.dispatch(output.output_type)
295 301 lines.extend(conv_fn(output))
296 302
297 303 return lines
298 304
299 305 @DocInherit
300 306 def render_markdown(self, cell):
301 307 return [cell.source]
302 308
303 309 @DocInherit
304 310 def render_raw(self, cell):
305 311 if self.raw_as_verbatim:
306 312 return ['::', '', indent(cell.source), '']
307 313 else:
308 314 return [cell.source]
309 315
310 316 @DocInherit
311 317 def render_pyout(self, output):
312 318 lines = ['Out[%s]:' % output.prompt_number, '']
313 319
314 320 # output is a dictionary like object with type as a key
315 321 if 'latex' in output:
316 322 lines.extend(rst_directive('.. math::', output.latex))
317 323
318 324 if 'text' in output:
319 325 lines.extend(rst_directive('.. parsed-literal::', output.text))
320 326
321 327 return lines
322 328
323 329 @DocInherit
324 330 def render_pyerr(self, output):
325 331 # Note: a traceback is a *list* of frames.
326 332 return ['::', '', indent(remove_ansi('\n'.join(output.traceback))), '']
327 333
328 334 @DocInherit
329 335 def _img_lines(self, img_file):
330 336 return ['.. image:: %s' % img_file, '']
331 337
332 338 @DocInherit
333 339 def render_stream(self, output):
334 340 lines = []
335 341
336 342 if 'text' in output:
337 343 lines.extend(rst_directive('.. parsed-literal::', output.text))
338 344
339 345 return lines
340 346
341 347 @DocInherit
342 348 def _unknown_lines(self, data):
343 349 return rst_directive('.. warning:: Unknown cell') + [data]
344 350
345 351
346 352 class ConverterQuickHTML(Converter):
347 353 extension = 'html'
348 354
349 355 def in_tag(self, tag, src):
350 356 """Return a list of elements bracketed by the given tag"""
351 357 return ['<%s>' % tag, src, '</%s>' % tag]
352 358
353 359 def optional_header(self):
354 360 # XXX: inject the IPython standard CSS into here
355 361 s = """<html>
356 362 <head>
357 363 </head>
358 364
359 365 <body>
360 366 """
361 367 return s.splitlines()
362 368
363 369 def optional_footer(self):
364 370 s = """</body>
365 371 </html>
366 372 """
367 373 return s.splitlines()
368 374
369 375 @DocInherit
370 376 def render_heading(self, cell):
371 377 marker = cell.level
372 378 return ['<h{1}>\n {0}\n</h{1}>'.format(cell.source, marker)]
373 379
374 380 @DocInherit
375 381 def render_code(self, cell):
376 382 if not cell.input:
377 383 return []
378 384
379 385 lines = ['<table>']
380 386 lines.append('<tr><td><tt>In [<b>%s</b>]:</tt></td><td><tt>' % cell.prompt_number)
381 387 lines.append("<br>\n".join(cell.input.splitlines()))
382 388 lines.append('</tt></td></tr>')
383 389
384 390 for output in cell.outputs:
385 391 lines.append('<tr><td></td><td>')
386 392 conv_fn = self.dispatch(output.output_type)
387 393 lines.extend(conv_fn(output))
388 394 lines.append('</td></tr>')
389 395
390 396 lines.append('</table>')
391 397 return lines
392 398
393 399 @DocInherit
394 400 def render_markdown(self, cell):
395 401 return self.in_tag('pre', cell.source)
396 402
397 403 @DocInherit
398 404 def render_raw(self, cell):
399 405 if self.raw_as_verbatim:
400 406 return self.in_tag('pre', cell.source)
401 407 else:
402 408 return [cell.source]
403 409
404 410 @DocInherit
405 411 def render_pyout(self, output):
406 412 lines = ['<tr><td><tt>Out[<b>%s</b>]:</tt></td></tr>' %
407 413 output.prompt_number, '<td>']
408 414
409 415 # output is a dictionary like object with type as a key
410 416 for out_type in ('text', 'latex'):
411 417 if out_type in output:
412 418 lines.extend(self.in_tag('pre', indent(output[out_type])))
413 419
414 420 return lines
415 421
416 422 @DocInherit
417 423 def render_pyerr(self, output):
418 424 # Note: a traceback is a *list* of frames.
419 425 return self.in_tag('pre', remove_ansi('\n'.join(output.traceback)))
420 426
421 427 @DocInherit
422 428 def _img_lines(self, img_file):
423 429 return ['<img src="%s">' % img_file, '']
424 430
425 431 @DocInherit
426 432 def render_stream(self, output):
427 433 lines = []
428 434
429 435 if 'text' in output:
430 436 lines.append(output.text)
431 437
432 438 return lines
433 439
434 440 @DocInherit
435 441 def _unknown_lines(self, data):
436 442 return ['<h2>Warning:: Unknown cell</h2>'] + self.in_tag('pre', data)
437 443
438 444
439 445 class ConverterLaTeX(Converter):
440 446 """Converts a notebook to a .tex file suitable for pdflatex.
441 447
442 448 Note: this converter *needs*:
443 449
444 450 - `pandoc`: for all conversion of markdown cells. If your notebook only
445 451 has Raw cells, pandoc will not be needed.
446 452
447 453 - `inkscape`: if your notebook has SVG figures. These need to be
448 454 converted to PDF before inclusion in the TeX file, as LaTeX doesn't
449 455 understand SVG natively.
450 456
451 457 You will in general obtain much better final PDF results if you configure
452 458 the matplotlib backend to create SVG output with
453 459
454 460 %config InlineBackend.figure_format = 'svg'
455 461
456 462 (or set the equivalent flag at startup or in your configuration profile).
457 463 """
458 464 extension = 'tex'
459 465 documentclass = 'article'
460 466 documentclass_options = '11pt,english'
461 467 heading_map = {1: r'\section',
462 468 2: r'\subsection',
463 469 3: r'\subsubsection',
464 470 4: r'\paragraph',
465 471 5: r'\subparagraph',
466 472 6: r'\subparagraph'}
467 473
468 474 def in_env(self, environment, lines):
469 475 """Return list of environment lines for input lines
470 476
471 477 Parameters
472 478 ----------
473 479 env : string
474 480 Name of the environment to bracket with begin/end.
475 481
476 482 lines: """
477 483 out = [r'\begin{%s}' % environment]
478 484 if isinstance(lines, basestring):
479 485 out.append(lines)
480 486 else: # list
481 487 out.extend(lines)
482 488 out.append(r'\end{%s}' % environment)
483 489 return out
484 490
485 491 def convert(self):
486 492 # The main body is done by the logic in the parent class, and that's
487 493 # all we need if preamble support has been turned off.
488 494 body = super(ConverterLaTeX, self).convert()
489 495 if not self.with_preamble:
490 496 return body
491 497 # But if preamble is on, then we need to construct a proper, standalone
492 498 # tex file.
493 499
494 500 # Tag the document at the top and set latex class
495 501 final = [ r'%% This file was auto-generated by IPython, do NOT edit',
496 502 r'%% Conversion from the original notebook file:',
497 503 r'%% {0}'.format(self.infile),
498 504 r'%%',
499 505 r'\documentclass[%s]{%s}' % (self.documentclass_options,
500 506 self.documentclass),
501 507 '',
502 508 ]
503 509 # Load our own preamble, which is stored next to the main file. We
504 510 # need to be careful in case the script entry point is a symlink
505 511 myfile = __file__ if not os.path.islink(__file__) else \
506 512 os.readlink(__file__)
507 513 with open(os.path.join(os.path.dirname(myfile), 'preamble.tex')) as f:
508 514 final.append(f.read())
509 515
510 516 # Load any additional user-supplied preamble
511 517 if self.user_preamble:
512 518 final.extend(['', '%% Adding user preamble from file:',
513 519 '%% {0}'.format(self.user_preamble), ''])
514 520 with open(self.user_preamble) as f:
515 521 final.append(f.read())
516 522
517 523 # Include document body
518 524 final.extend([ r'\begin{document}', '',
519 525 body,
520 526 r'\end{document}', ''])
521 527 # Retun value must be a string
522 528 return '\n'.join(final)
523 529
524 530 @DocInherit
525 531 def render_heading(self, cell):
526 532 marker = self.heading_map[cell.level]
527 533 return ['%s{%s}' % (marker, cell.source) ]
528 534
529 535 @DocInherit
530 536 def render_code(self, cell):
531 537 if not cell.input:
532 538 return []
533 539
534 540 # Cell codes first carry input code, we use lstlisting for that
535 541 lines = [r'\begin{codecell}']
536 542
537 543 lines.extend(self.in_env('codeinput',
538 544 self.in_env('lstlisting', cell.input)))
539 545
540 546 outlines = []
541 547 for output in cell.outputs:
542 548 conv_fn = self.dispatch(output.output_type)
543 549 outlines.extend(conv_fn(output))
544 550
545 551 # And then output of many possible types; use a frame for all of it.
546 552 if outlines:
547 553 lines.extend(self.in_env('codeoutput', outlines))
548 554
549 555 lines.append(r'\end{codecell}')
550 556
551 557 return lines
552 558
553 559
554 560 @DocInherit
555 561 def _img_lines(self, img_file):
556 562 return self.in_env('center',
557 563 [r'\includegraphics[width=3in]{%s}' % img_file, r'\par'])
558 564
559 565 def _svg_lines(self, img_file):
560 566 base_file = os.path.splitext(img_file)[0]
561 567 pdf_file = base_file + '.pdf'
562 subprocess.check_call(['inkscape', '--export-pdf=%s' % pdf_file,
568 subprocess.check_call([ inkscape, '--export-pdf=%s' % pdf_file,
563 569 img_file])
564 570 return self._img_lines(pdf_file)
565 571
566 572 @DocInherit
567 573 def render_stream(self, output):
568 574 lines = []
569 575
570 576 if 'text' in output:
571 577 lines.extend(self.in_env('verbatim', output.text.strip()))
572 578
573 579 return lines
574 580
575 581 @DocInherit
576 582 def render_markdown(self, cell):
577 583 return [markdown2latex(cell.source)]
578 584
579 585 @DocInherit
580 586 def render_pyout(self, output):
581 587 lines = []
582 588
583 589 # output is a dictionary like object with type as a key
584 590 if 'latex' in output:
585 591 lines.extend(output.latex)
586 592
587 593 if 'text' in output:
588 594 lines.extend(self.in_env('verbatim', output.text))
589 595
590 596 return lines
591 597
592 598 @DocInherit
593 599 def render_pyerr(self, output):
594 600 # Note: a traceback is a *list* of frames.
595 601 return self.in_env('traceback',
596 602 self.in_env('verbatim',
597 603 remove_ansi('\n'.join(output.traceback))))
598 604
599 605 @DocInherit
600 606 def render_raw(self, cell):
601 607 if self.raw_as_verbatim:
602 608 return self.in_env('verbatim', cell.source)
603 609 else:
604 610 return [cell.source]
605 611
606 612 @DocInherit
607 613 def _unknown_lines(self, data):
608 614 return [r'{\vspace{5mm}\bf WARNING:: unknown cell:}'] + \
609 615 self.in_env('verbatim', data)
610 616
611 617 #-----------------------------------------------------------------------------
612 618 # Standalone conversion functions
613 619 #-----------------------------------------------------------------------------
614 620
615 621 def rst2simplehtml(infile):
616 622 """Convert a rst file to simplified html suitable for blogger.
617 623
618 624 This just runs rst2html with certain parameters to produce really simple
619 625 html and strips the document header, so the resulting file can be easily
620 626 pasted into a blogger edit window.
621 627 """
622 628
623 629 # This is the template for the rst2html call that produces the cleanest,
624 630 # simplest html I could find. This should help in making it easier to
625 631 # paste into the blogspot html window, though I'm still having problems
626 632 # with linebreaks there...
627 633 cmd_template = ("rst2html --link-stylesheet --no-xml-declaration "
628 634 "--no-generator --no-datestamp --no-source-link "
629 635 "--no-toc-backlinks --no-section-numbering "
630 636 "--strip-comments ")
631 637
632 638 cmd = "%s %s" % (cmd_template, infile)
633 639 proc = subprocess.Popen(cmd,
634 640 stdout=subprocess.PIPE,
635 641 stderr=subprocess.PIPE,
636 642 shell=True)
637 643 html, stderr = proc.communicate()
638 644 if stderr:
639 645 raise IOError(stderr)
640 646
641 647 # Make an iterator so breaking out holds state. Our implementation of
642 648 # searching for the html body below is basically a trivial little state
643 649 # machine, so we need this.
644 650 walker = iter(html.splitlines())
645 651
646 652 # Find start of main text, break out to then print until we find end /div.
647 653 # This may only work if there's a real title defined so we get a 'div class'
648 654 # tag, I haven't really tried.
649 655 for line in walker:
650 656 if line.startswith('<body>'):
651 657 break
652 658
653 659 newfname = os.path.splitext(infile)[0] + '.html'
654 660 with open(newfname, 'w') as f:
655 661 for line in walker:
656 662 if line.startswith('</body>'):
657 663 break
658 664 f.write(line)
659 665 f.write('\n')
660 666
661 667 return newfname
662 668
663 669 known_formats = "rst (default), html, quick-html, latex"
664 670
665 671 def main(infile, format='rst'):
666 672 """Convert a notebook to html in one step"""
667 673 # XXX: this is just quick and dirty for now. When adding a new format,
668 674 # make sure to add it to the `known_formats` string above, which gets
669 675 # printed in in the catch-all else, as well as in the help
670 676 if format == 'rst':
671 677 converter = ConverterRST(infile)
672 678 converter.render()
673 679 elif format == 'html':
674 680 #Currently, conversion to html is a 2 step process, nb->rst->html
675 681 converter = ConverterRST(infile)
676 682 rstfname = converter.render()
677 683 rst2simplehtml(rstfname)
678 684 elif format == 'quick-html':
679 685 converter = ConverterQuickHTML(infile)
680 686 rstfname = converter.render()
681 687 elif format == 'latex':
682 688 converter = ConverterLaTeX(infile)
683 689 latexfname = converter.render()
684 690 else:
685 691 raise SystemExit("Unknown format '%s', " % format +
686 692 "known formats are: " + known_formats)
687 693
688 694 #-----------------------------------------------------------------------------
689 695 # Script main
690 696 #-----------------------------------------------------------------------------
691 697
692 698 if __name__ == '__main__':
693 699 parser = argparse.ArgumentParser(description=__doc__,
694 700 formatter_class=argparse.RawTextHelpFormatter)
695 701 # TODO: consider passing file like object around, rather than filenames
696 702 # would allow us to process stdin, or even http streams
697 703 #parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), default=sys.stdin)
698 704
699 705 #Require a filename as a positional argument
700 706 parser.add_argument('infile', nargs=1)
701 707 parser.add_argument('-f', '--format', default='rst',
702 708 help='Output format. Supported formats: \n' +
703 709 known_formats)
704 710 args = parser.parse_args()
705 711 main(infile=args.infile[0], format=args.format)
General Comments 0
You need to be logged in to leave comments. Login now