##// END OF EJS Templates
use py3compat.which in common locations
Min RK -
Show More
@@ -1,400 +1,369 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Decorators for labeling test objects.
3 3
4 4 Decorators that merely return a modified version of the original function
5 5 object are straightforward. Decorators that return a new function object need
6 6 to use nose.tools.make_decorator(original_function)(decorator) in returning the
7 7 decorator, in order to preserve metadata such as function name, setup and
8 8 teardown functions and so on - see nose.tools for more information.
9 9
10 10 This module provides a set of useful decorators meant to be ready to use in
11 11 your own tests. See the bottom of the file for the ready-made ones, and if you
12 12 find yourself writing a new one that may be of generic use, add it here.
13 13
14 14 Included decorators:
15 15
16 16
17 17 Lightweight testing that remains unittest-compatible.
18 18
19 19 - An @as_unittest decorator can be used to tag any normal parameter-less
20 20 function as a unittest TestCase. Then, both nose and normal unittest will
21 21 recognize it as such. This will make it easier to migrate away from Nose if
22 22 we ever need/want to while maintaining very lightweight tests.
23 23
24 24 NOTE: This file contains IPython-specific decorators. Using the machinery in
25 25 IPython.external.decorators, we import either numpy.testing.decorators if numpy is
26 26 available, OR use equivalent code in IPython.external._decorators, which
27 27 we've copied verbatim from numpy.
28 28
29 Authors
30 -------
31
32 - Fernando Perez <Fernando.Perez@berkeley.edu>
33 29 """
34 30
35 #-----------------------------------------------------------------------------
36 # Copyright (C) 2009-2011 The IPython Development Team
37 #
38 # Distributed under the terms of the BSD License. The full license is in
39 # the file COPYING, distributed as part of this software.
40 #-----------------------------------------------------------------------------
31 # Copyright (c) IPython Development Team.
32 # Distributed under the terms of the Modified BSD License.
41 33
42 #-----------------------------------------------------------------------------
43 # Imports
44 #-----------------------------------------------------------------------------
45
46 # Stdlib imports
47 34 import sys
48 35 import os
49 36 import tempfile
50 37 import unittest
51 38
52 # Third-party imports
53
54 # This is Michele Simionato's decorator module, kept verbatim.
55 39 from decorator import decorator
56 40
57 41 # Expose the unittest-driven decorators
58 42 from .ipunittest import ipdoctest, ipdocstring
59 43
60 44 # Grab the numpy-specific decorators which we keep in a file that we
61 45 # occasionally update from upstream: decorators.py is a copy of
62 46 # numpy.testing.decorators, we expose all of it here.
63 47 from IPython.external.decorators import *
64 48
65 49 # For onlyif_cmd_exists decorator
66 from IPython.utils.process import is_cmd_found
67 from IPython.utils.py3compat import string_types
50 from IPython.utils.py3compat import string_types, which
68 51
69 52 #-----------------------------------------------------------------------------
70 53 # Classes and functions
71 54 #-----------------------------------------------------------------------------
72 55
73 56 # Simple example of the basic idea
74 57 def as_unittest(func):
75 58 """Decorator to make a simple function into a normal test via unittest."""
76 59 class Tester(unittest.TestCase):
77 60 def test(self):
78 61 func()
79 62
80 63 Tester.__name__ = func.__name__
81 64
82 65 return Tester
83 66
84 67 # Utility functions
85 68
86 69 def apply_wrapper(wrapper,func):
87 70 """Apply a wrapper to a function for decoration.
88 71
89 72 This mixes Michele Simionato's decorator tool with nose's make_decorator,
90 73 to apply a wrapper in a decorator so that all nose attributes, as well as
91 74 function signature and other properties, survive the decoration cleanly.
92 75 This will ensure that wrapped functions can still be well introspected via
93 76 IPython, for example.
94 77 """
95 78 import nose.tools
96 79
97 80 return decorator(wrapper,nose.tools.make_decorator(func)(wrapper))
98 81
99 82
100 83 def make_label_dec(label,ds=None):
101 84 """Factory function to create a decorator that applies one or more labels.
102 85
103 86 Parameters
104 87 ----------
105 88 label : string or sequence
106 89 One or more labels that will be applied by the decorator to the functions
107 90 it decorates. Labels are attributes of the decorated function with their
108 91 value set to True.
109 92
110 93 ds : string
111 94 An optional docstring for the resulting decorator. If not given, a
112 95 default docstring is auto-generated.
113 96
114 97 Returns
115 98 -------
116 99 A decorator.
117 100
118 101 Examples
119 102 --------
120 103
121 104 A simple labeling decorator:
122 105
123 106 >>> slow = make_label_dec('slow')
124 107 >>> slow.__doc__
125 108 "Labels a test as 'slow'."
126 109
127 110 And one that uses multiple labels and a custom docstring:
128 111
129 112 >>> rare = make_label_dec(['slow','hard'],
130 113 ... "Mix labels 'slow' and 'hard' for rare tests.")
131 114 >>> rare.__doc__
132 115 "Mix labels 'slow' and 'hard' for rare tests."
133 116
134 117 Now, let's test using this one:
135 118 >>> @rare
136 119 ... def f(): pass
137 120 ...
138 121 >>>
139 122 >>> f.slow
140 123 True
141 124 >>> f.hard
142 125 True
143 126 """
144 127
145 128 if isinstance(label, string_types):
146 129 labels = [label]
147 130 else:
148 131 labels = label
149 132
150 133 # Validate that the given label(s) are OK for use in setattr() by doing a
151 134 # dry run on a dummy function.
152 135 tmp = lambda : None
153 136 for label in labels:
154 137 setattr(tmp,label,True)
155 138
156 139 # This is the actual decorator we'll return
157 140 def decor(f):
158 141 for label in labels:
159 142 setattr(f,label,True)
160 143 return f
161 144
162 145 # Apply the user's docstring, or autogenerate a basic one
163 146 if ds is None:
164 147 ds = "Labels a test as %r." % label
165 148 decor.__doc__ = ds
166 149
167 150 return decor
168 151
169 152
170 153 # Inspired by numpy's skipif, but uses the full apply_wrapper utility to
171 154 # preserve function metadata better and allows the skip condition to be a
172 155 # callable.
173 156 def skipif(skip_condition, msg=None):
174 157 ''' Make function raise SkipTest exception if skip_condition is true
175 158
176 159 Parameters
177 160 ----------
178 161
179 162 skip_condition : bool or callable
180 163 Flag to determine whether to skip test. If the condition is a
181 164 callable, it is used at runtime to dynamically make the decision. This
182 165 is useful for tests that may require costly imports, to delay the cost
183 166 until the test suite is actually executed.
184 167 msg : string
185 168 Message to give on raising a SkipTest exception.
186 169
187 170 Returns
188 171 -------
189 172 decorator : function
190 173 Decorator, which, when applied to a function, causes SkipTest
191 174 to be raised when the skip_condition was True, and the function
192 175 to be called normally otherwise.
193 176
194 177 Notes
195 178 -----
196 179 You will see from the code that we had to further decorate the
197 180 decorator with the nose.tools.make_decorator function in order to
198 181 transmit function name, and various other metadata.
199 182 '''
200 183
201 184 def skip_decorator(f):
202 185 # Local import to avoid a hard nose dependency and only incur the
203 186 # import time overhead at actual test-time.
204 187 import nose
205 188
206 189 # Allow for both boolean or callable skip conditions.
207 190 if callable(skip_condition):
208 191 skip_val = skip_condition
209 192 else:
210 193 skip_val = lambda : skip_condition
211 194
212 195 def get_msg(func,msg=None):
213 196 """Skip message with information about function being skipped."""
214 197 if msg is None: out = 'Test skipped due to test condition.'
215 198 else: out = msg
216 199 return "Skipping test: %s. %s" % (func.__name__,out)
217 200
218 201 # We need to define *two* skippers because Python doesn't allow both
219 202 # return with value and yield inside the same function.
220 203 def skipper_func(*args, **kwargs):
221 204 """Skipper for normal test functions."""
222 205 if skip_val():
223 206 raise nose.SkipTest(get_msg(f,msg))
224 207 else:
225 208 return f(*args, **kwargs)
226 209
227 210 def skipper_gen(*args, **kwargs):
228 211 """Skipper for test generators."""
229 212 if skip_val():
230 213 raise nose.SkipTest(get_msg(f,msg))
231 214 else:
232 215 for x in f(*args, **kwargs):
233 216 yield x
234 217
235 218 # Choose the right skipper to use when building the actual generator.
236 219 if nose.util.isgenerator(f):
237 220 skipper = skipper_gen
238 221 else:
239 222 skipper = skipper_func
240 223
241 224 return nose.tools.make_decorator(f)(skipper)
242 225
243 226 return skip_decorator
244 227
245 228 # A version with the condition set to true, common case just to attach a message
246 229 # to a skip decorator
247 230 def skip(msg=None):
248 231 """Decorator factory - mark a test function for skipping from test suite.
249 232
250 233 Parameters
251 234 ----------
252 235 msg : string
253 236 Optional message to be added.
254 237
255 238 Returns
256 239 -------
257 240 decorator : function
258 241 Decorator, which, when applied to a function, causes SkipTest
259 242 to be raised, with the optional message added.
260 243 """
261 244
262 245 return skipif(True,msg)
263 246
264 247
265 248 def onlyif(condition, msg):
266 249 """The reverse from skipif, see skipif for details."""
267 250
268 251 if callable(condition):
269 252 skip_condition = lambda : not condition()
270 253 else:
271 254 skip_condition = lambda : not condition
272 255
273 256 return skipif(skip_condition, msg)
274 257
275 258 #-----------------------------------------------------------------------------
276 259 # Utility functions for decorators
277 260 def module_not_available(module):
278 261 """Can module be imported? Returns true if module does NOT import.
279 262
280 263 This is used to make a decorator to skip tests that require module to be
281 264 available, but delay the 'import numpy' to test execution time.
282 265 """
283 266 try:
284 267 mod = __import__(module)
285 268 mod_not_avail = False
286 269 except ImportError:
287 270 mod_not_avail = True
288 271
289 272 return mod_not_avail
290 273
291 274
292 275 def decorated_dummy(dec, name):
293 276 """Return a dummy function decorated with dec, with the given name.
294 277
295 278 Examples
296 279 --------
297 280 import IPython.testing.decorators as dec
298 281 setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__)
299 282 """
300 283 dummy = lambda: None
301 284 dummy.__name__ = name
302 285 return dec(dummy)
303 286
304 287 #-----------------------------------------------------------------------------
305 288 # Decorators for public use
306 289
307 290 # Decorators to skip certain tests on specific platforms.
308 291 skip_win32 = skipif(sys.platform == 'win32',
309 292 "This test does not run under Windows")
310 293 skip_linux = skipif(sys.platform.startswith('linux'),
311 294 "This test does not run under Linux")
312 295 skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X")
313 296
314 297
315 298 # Decorators to skip tests if not on specific platforms.
316 299 skip_if_not_win32 = skipif(sys.platform != 'win32',
317 300 "This test only runs under Windows")
318 301 skip_if_not_linux = skipif(not sys.platform.startswith('linux'),
319 302 "This test only runs under Linux")
320 303 skip_if_not_osx = skipif(sys.platform != 'darwin',
321 304 "This test only runs under OSX")
322 305
323 306
324 307 _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and
325 308 os.environ.get('DISPLAY', '') == '')
326 309 _x11_skip_msg = "Skipped under *nix when X11/XOrg not available"
327 310
328 311 skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg)
329 312
330 313 # not a decorator itself, returns a dummy function to be used as setup
331 314 def skip_file_no_x11(name):
332 315 return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None
333 316
334 317 # Other skip decorators
335 318
336 319 # generic skip without module
337 320 skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod)
338 321
339 322 skipif_not_numpy = skip_without('numpy')
340 323
341 324 skipif_not_matplotlib = skip_without('matplotlib')
342 325
343 326 skipif_not_sympy = skip_without('sympy')
344 327
345 328 skip_known_failure = knownfailureif(True,'This test is known to fail')
346 329
347 330 known_failure_py3 = knownfailureif(sys.version_info[0] >= 3,
348 331 'This test is known to fail on Python 3.')
349 332
350 333 # A null 'decorator', useful to make more readable code that needs to pick
351 334 # between different decorators based on OS or other conditions
352 335 null_deco = lambda f: f
353 336
354 337 # Some tests only run where we can use unicode paths. Note that we can't just
355 338 # check os.path.supports_unicode_filenames, which is always False on Linux.
356 339 try:
357 340 f = tempfile.NamedTemporaryFile(prefix=u"tmp€")
358 341 except UnicodeEncodeError:
359 342 unicode_paths = False
360 343 else:
361 344 unicode_paths = True
362 345 f.close()
363 346
364 347 onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable "
365 348 "where we can use unicode in filenames."))
366 349
367 350
368 351 def onlyif_cmds_exist(*commands):
369 352 """
370 353 Decorator to skip test when at least one of `commands` is not found.
371 354 """
372 355 for cmd in commands:
373 try:
374 if not is_cmd_found(cmd):
375 return skip("This test runs only if command '{0}' "
376 "is installed".format(cmd))
377 except ImportError as e:
378 # is_cmd_found uses pywin32 on windows, which might not be available
379 if sys.platform == 'win32' and 'pywin32' in str(e):
380 return skip("This test runs only if pywin32 and command '{0}' "
381 "is installed".format(cmd))
382 raise e
356 if not which(cmd):
357 return skip("This test runs only if command '{0}' "
358 "is installed".format(cmd))
383 359 return null_deco
384 360
385 361 def onlyif_any_cmd_exists(*commands):
386 362 """
387 363 Decorator to skip test unless at least one of `commands` is found.
388 364 """
389 365 for cmd in commands:
390 try:
391 if is_cmd_found(cmd):
392 return null_deco
393 except ImportError as e:
394 # is_cmd_found uses pywin32 on windows, which might not be available
395 if sys.platform == 'win32' and 'pywin32' in str(e):
396 return skip("This test runs only if pywin32 and commands '{0}' "
397 "are installed".format(commands))
398 raise e
366 if which(cmd):
367 return null_deco
399 368 return skip("This test runs only if one of the commands {0} "
400 369 "is installed".format(commands))
@@ -1,123 +1,106 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for working with external processes.
4 4 """
5 5
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2008-2011 The IPython Development Team
8 #
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
12
13 #-----------------------------------------------------------------------------
14 # Imports
15 #-----------------------------------------------------------------------------
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
8
16 9 from __future__ import print_function
17 10
18 # Stdlib
19 11 import os
20 12 import sys
21 13
22 # Our own
23 14 if sys.platform == 'win32':
24 from ._process_win32 import _find_cmd, system, getoutput, arg_split, check_pid
15 from ._process_win32 import system, getoutput, arg_split, check_pid
25 16 elif sys.platform == 'cli':
26 from ._process_cli import _find_cmd, system, getoutput, arg_split, check_pid
17 from ._process_cli import system, getoutput, arg_split, check_pid
27 18 else:
28 from ._process_posix import _find_cmd, system, getoutput, arg_split, check_pid
19 from ._process_posix import system, getoutput, arg_split, check_pid
29 20
30 21 from ._process_common import getoutputerror, get_output_error_code, process_handler
31 22 from . import py3compat
32 23
33 #-----------------------------------------------------------------------------
34 # Code
35 #-----------------------------------------------------------------------------
36
37 24
38 25 class FindCmdError(Exception):
39 26 pass
40 27
41 28
42 29 def find_cmd(cmd):
43 30 """Find absolute path to executable cmd in a cross platform manner.
44 31
45 32 This function tries to determine the full path to a command line program
46 33 using `which` on Unix/Linux/OS X and `win32api` on Windows. Most of the
47 34 time it will use the version that is first on the users `PATH`.
48 35
49 36 Warning, don't use this to find IPython command line programs as there
50 37 is a risk you will find the wrong one. Instead find those using the
51 38 following code and looking for the application itself::
52 39
53 40 from IPython.utils.path import get_ipython_module_path
54 41 from IPython.utils.process import pycmd2argv
55 42 argv = pycmd2argv(get_ipython_module_path('IPython.terminal.ipapp'))
56 43
57 44 Parameters
58 45 ----------
59 46 cmd : str
60 47 The command line program to look for.
61 48 """
62 try:
63 path = _find_cmd(cmd).rstrip()
64 except OSError:
65 raise FindCmdError('command could not be found: %s' % cmd)
66 # which returns empty if not found
67 if path == '':
49 path = py3compat.which(cmd)
50 if path is None:
68 51 raise FindCmdError('command could not be found: %s' % cmd)
69 return os.path.abspath(path)
52 return path
70 53
71 54
72 55 def is_cmd_found(cmd):
73 56 """Check whether executable `cmd` exists or not and return a bool."""
74 57 try:
75 58 find_cmd(cmd)
76 59 return True
77 60 except FindCmdError:
78 61 return False
79 62
80 63
81 64 def pycmd2argv(cmd):
82 65 r"""Take the path of a python command and return a list (argv-style).
83 66
84 67 This only works on Python based command line programs and will find the
85 68 location of the ``python`` executable using ``sys.executable`` to make
86 69 sure the right version is used.
87 70
88 71 For a given path ``cmd``, this returns [cmd] if cmd's extension is .exe,
89 72 .com or .bat, and [, cmd] otherwise.
90 73
91 74 Parameters
92 75 ----------
93 76 cmd : string
94 77 The path of the command.
95 78
96 79 Returns
97 80 -------
98 81 argv-style list.
99 82 """
100 83 ext = os.path.splitext(cmd)[1]
101 84 if ext in ['.exe', '.com', '.bat']:
102 85 return [cmd]
103 86 else:
104 87 return [sys.executable, cmd]
105 88
106 89
107 90 def abbrev_cwd():
108 91 """ Return abbreviated version of cwd, e.g. d:mydir """
109 92 cwd = py3compat.getcwd().replace('\\','/')
110 93 drivepart = ''
111 94 tail = cwd
112 95 if sys.platform == 'win32':
113 96 if len(cwd) < 4:
114 97 return cwd
115 98 drivepart,tail = os.path.splitdrive(cwd)
116 99
117 100
118 101 parts = tail.split('/')
119 102 if len(parts) > 2:
120 103 tail = '/'.join(parts[-2:])
121 104
122 105 return (drivepart + (
123 106 cwd == '/' and '/' or tail))
@@ -1,147 +1,149 b''
1 1 """Export to PDF via latex"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import subprocess
7 7 import os
8 8 import sys
9 9
10 from IPython.utils.process import find_cmd
10 from IPython.utils.py3compat import which
11 11 from IPython.utils.traitlets import Integer, List, Bool, Instance
12 12 from IPython.utils.tempdir import TemporaryWorkingDirectory
13 13 from .latex import LatexExporter
14 14
15 15
16 16 class PDFExporter(LatexExporter):
17 17 """Writer designed to write to PDF files"""
18 18
19 19 latex_count = Integer(3, config=True,
20 20 help="How many times latex will be called."
21 21 )
22 22
23 23 latex_command = List([u"pdflatex", u"{filename}"], config=True,
24 24 help="Shell command used to compile latex."
25 25 )
26 26
27 27 bib_command = List([u"bibtex", u"{filename}"], config=True,
28 28 help="Shell command used to run bibtex."
29 29 )
30 30
31 31 verbose = Bool(False, config=True,
32 32 help="Whether to display the output of latex commands."
33 33 )
34 34
35 35 temp_file_exts = List(['.aux', '.bbl', '.blg', '.idx', '.log', '.out'], config=True,
36 36 help="File extensions of temp files to remove after running."
37 37 )
38 38
39 39 writer = Instance("jupyter_nbconvert.writers.FilesWriter", args=())
40 40
41 41 def run_command(self, command_list, filename, count, log_function):
42 42 """Run command_list count times.
43 43
44 44 Parameters
45 45 ----------
46 46 command_list : list
47 47 A list of args to provide to Popen. Each element of this
48 48 list will be interpolated with the filename to convert.
49 49 filename : unicode
50 50 The name of the file to convert.
51 51 count : int
52 52 How many times to run the command.
53 53
54 54 Returns
55 55 -------
56 56 success : bool
57 57 A boolean indicating if the command was successful (True)
58 58 or failed (False).
59 59 """
60 60 command = [c.format(filename=filename) for c in command_list]
61 61
62 62 # On windows with python 2.x there is a bug in subprocess.Popen and
63 63 # unicode commands are not supported
64 64 if sys.platform == 'win32' and sys.version_info < (3,0):
65 65 #We must use cp1252 encoding for calling subprocess.Popen
66 66 #Note that sys.stdin.encoding and encoding.DEFAULT_ENCODING
67 67 # could be different (cp437 in case of dos console)
68 command = [c.encode('cp1252') for c in command]
68 command = [c.encode('cp1252') for c in command]
69 69
70 70 # This will throw a clearer error if the command is not found
71 find_cmd(command_list[0])
71 cmd = which(command_list[0])
72 if cmd is None:
73 raise OSError("%s not found on PATH" % command_list[0])
72 74
73 75 times = 'time' if count == 1 else 'times'
74 76 self.log.info("Running %s %i %s: %s", command_list[0], count, times, command)
75 77 with open(os.devnull, 'rb') as null:
76 78 stdout = subprocess.PIPE if not self.verbose else None
77 79 for index in range(count):
78 80 p = subprocess.Popen(command, stdout=stdout, stdin=null)
79 81 out, err = p.communicate()
80 82 if p.returncode:
81 83 if self.verbose:
82 84 # verbose means I didn't capture stdout with PIPE,
83 85 # so it's already been displayed and `out` is None.
84 86 out = u''
85 87 else:
86 88 out = out.decode('utf-8', 'replace')
87 89 log_function(command, out)
88 90 return False # failure
89 91 return True # success
90 92
91 93 def run_latex(self, filename):
92 94 """Run pdflatex self.latex_count times."""
93 95
94 96 def log_error(command, out):
95 97 self.log.critical(u"%s failed: %s\n%s", command[0], command, out)
96 98
97 99 return self.run_command(self.latex_command, filename,
98 100 self.latex_count, log_error)
99 101
100 102 def run_bib(self, filename):
101 103 """Run bibtex self.latex_count times."""
102 104 filename = os.path.splitext(filename)[0]
103 105
104 106 def log_error(command, out):
105 107 self.log.warn('%s had problems, most likely because there were no citations',
106 108 command[0])
107 109 self.log.debug(u"%s output: %s\n%s", command[0], command, out)
108 110
109 111 return self.run_command(self.bib_command, filename, 1, log_error)
110 112
111 113 def clean_temp_files(self, filename):
112 114 """Remove temporary files created by pdflatex/bibtex."""
113 115 self.log.info("Removing temporary LaTeX files")
114 116 filename = os.path.splitext(filename)[0]
115 117 for ext in self.temp_file_exts:
116 118 try:
117 119 os.remove(filename+ext)
118 120 except OSError:
119 121 pass
120 122
121 123 def from_notebook_node(self, nb, resources=None, **kw):
122 124 latex, resources = super(PDFExporter, self).from_notebook_node(
123 125 nb, resources=resources, **kw
124 126 )
125 127 with TemporaryWorkingDirectory() as td:
126 128 notebook_name = "notebook"
127 129 tex_file = self.writer.write(latex, resources, notebook_name=notebook_name)
128 130 self.log.info("Building PDF")
129 131 rc = self.run_latex(tex_file)
130 132 if not rc:
131 133 rc = self.run_bib(tex_file)
132 134 if not rc:
133 135 rc = self.run_latex(tex_file)
134 136
135 137 pdf_file = notebook_name + '.pdf'
136 138 if not os.path.isfile(pdf_file):
137 139 raise RuntimeError("PDF creating failed")
138 140 self.log.info('PDF successfully created')
139 141 with open(pdf_file, 'rb') as f:
140 142 pdf_data = f.read()
141 143
142 144 # convert output extension to pdf
143 145 # the writer above required it to be tex
144 146 resources['output_extension'] = '.pdf'
145 147
146 148 return pdf_data, resources
147 149
General Comments 0
You need to be logged in to leave comments. Login now