Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/nbconvert/exporters/pdf.py: 30%
102 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
1"""Export to PDF via latex"""
3# Copyright (c) IPython Development Team.
4# Distributed under the terms of the Modified BSD License.
6import os
7import shutil
8import subprocess
9import sys
10from tempfile import TemporaryDirectory
12from traitlets import Bool, Instance, Integer, List, Unicode, default
14from nbconvert.utils import _contextlib_chdir
16from .latex import LatexExporter
19class LatexFailed(IOError): # noqa
20 """Exception for failed latex run
22 Captured latex output is in error.output.
23 """
25 def __init__(self, output):
26 """Initialize the error."""
27 self.output = output
29 def __unicode__(self):
30 """Unicode representation."""
31 return "PDF creating failed, captured latex output:\n%s" % self.output
33 def __str__(self):
34 """String representation."""
35 u = self.__unicode__()
36 return u
39def prepend_to_env_search_path(varname, value, envdict):
40 """Add value to the environment variable varname in envdict
42 e.g. prepend_to_env_search_path('BIBINPUTS', '/home/sally/foo', os.environ)
43 """
44 if not value:
45 return # Nothing to add
47 envdict[varname] = value + os.pathsep + envdict.get(varname, "")
50class PDFExporter(LatexExporter):
51 """Writer designed to write to PDF files.
53 This inherits from `LatexExporter`. It creates a LaTeX file in
54 a temporary directory using the template machinery, and then runs LaTeX
55 to create a pdf.
56 """
58 export_from_notebook = "PDF via LaTeX"
60 latex_count = Integer(3, help="How many times latex will be called.").tag(config=True)
62 latex_command = List(
63 ["xelatex", "{filename}", "-quiet"], help="Shell command used to compile latex."
64 ).tag(config=True)
66 bib_command = List(["bibtex", "{filename}"], help="Shell command used to run bibtex.").tag(
67 config=True
68 )
70 verbose = Bool(False, help="Whether to display the output of latex commands.").tag(config=True)
72 texinputs = Unicode(help="texinputs dir. A notebook's directory is added")
73 writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={"build_directory": "."})
75 output_mimetype = "application/pdf"
77 _captured_output = List()
79 @default("file_extension")
80 def _file_extension_default(self):
81 return ".pdf"
83 @default("template_extension")
84 def _template_extension_default(self):
85 return ".tex.j2"
87 def run_command( # noqa
88 self, command_list, filename, count, log_function, raise_on_failure=None
89 ):
90 """Run command_list count times.
92 Parameters
93 ----------
94 command_list : list
95 A list of args to provide to Popen. Each element of this
96 list will be interpolated with the filename to convert.
97 filename : unicode
98 The name of the file to convert.
99 count : int
100 How many times to run the command.
101 raise_on_failure: Exception class (default None)
102 If provided, will raise the given exception for if an instead of
103 returning False on command failure.
105 Returns
106 -------
107 success : bool
108 A boolean indicating if the command was successful (True)
109 or failed (False).
110 """
111 command = [c.format(filename=filename) for c in command_list]
113 # This will throw a clearer error if the command is not found
114 cmd = shutil.which(command_list[0])
115 if cmd is None:
116 link = "https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex"
117 msg = (
118 "{formatter} not found on PATH, if you have not installed "
119 "{formatter} you may need to do so. Find further instructions "
120 "at {link}.".format(formatter=command_list[0], link=link)
121 )
122 raise OSError(msg)
124 times = "time" if count == 1 else "times"
125 self.log.info("Running %s %i %s: %s", command_list[0], count, times, command)
127 shell = sys.platform == "win32"
128 if shell:
129 command = subprocess.list2cmdline(command) # type:ignore
130 env = os.environ.copy()
131 prepend_to_env_search_path("TEXINPUTS", self.texinputs, env)
132 prepend_to_env_search_path("BIBINPUTS", self.texinputs, env)
133 prepend_to_env_search_path("BSTINPUTS", self.texinputs, env)
135 with open(os.devnull, "rb") as null:
136 stdout = subprocess.PIPE if not self.verbose else None
137 for _ in range(count):
138 p = subprocess.Popen(
139 command,
140 stdout=stdout,
141 stderr=subprocess.STDOUT,
142 stdin=null,
143 shell=shell, # noqa
144 env=env,
145 )
146 out, _ = p.communicate()
147 if p.returncode:
148 if self.verbose: # noqa
149 # verbose means I didn't capture stdout with PIPE,
150 # so it's already been displayed and `out` is None.
151 out_str = ""
152 else:
153 out_str = out.decode("utf-8", "replace")
154 log_function(command, out)
155 self._captured_output.append(out_str)
156 if raise_on_failure:
157 msg = f'Failed to run "{command}" command:\n{out_str}'
158 raise raise_on_failure(msg)
159 return False # failure
160 return True # success
162 def run_latex(self, filename, raise_on_failure=LatexFailed):
163 """Run xelatex self.latex_count times."""
165 def log_error(command, out):
166 self.log.critical("%s failed: %s\n%s", command[0], command, out)
168 return self.run_command(
169 self.latex_command, filename, self.latex_count, log_error, raise_on_failure
170 )
172 def run_bib(self, filename, raise_on_failure=False):
173 """Run bibtex one time."""
174 filename = os.path.splitext(filename)[0]
176 def log_error(command, out):
177 self.log.warning(
178 "%s had problems, most likely because there were no citations", command[0]
179 )
180 self.log.debug("%s output: %s\n%s", command[0], command, out)
182 return self.run_command(self.bib_command, filename, 1, log_error, raise_on_failure)
184 def from_notebook_node(self, nb, resources=None, **kw):
185 """Convert from notebook node."""
186 latex, resources = super().from_notebook_node(nb, resources=resources, **kw)
187 # set texinputs directory, so that local files will be found
188 if resources and resources.get("metadata", {}).get("path"):
189 self.texinputs = os.path.abspath(resources["metadata"]["path"])
190 else:
191 self.texinputs = os.getcwd()
193 self._captured_outputs = []
194 with TemporaryDirectory() as td, _contextlib_chdir.chdir(td):
195 notebook_name = "notebook"
196 resources["output_extension"] = ".tex"
197 tex_file = self.writer.write(latex, resources, notebook_name=notebook_name)
198 self.log.info("Building PDF")
199 self.run_latex(tex_file)
200 if self.run_bib(tex_file):
201 self.run_latex(tex_file)
203 pdf_file = notebook_name + ".pdf"
204 if not os.path.isfile(pdf_file):
205 raise LatexFailed("\n".join(self._captured_output))
206 self.log.info("PDF successfully created")
207 with open(pdf_file, "rb") as f:
208 pdf_data = f.read()
210 # convert output extension to pdf
211 # the writer above required it to be tex
212 resources["output_extension"] = ".pdf"
213 # clear figure outputs and attachments, extracted by latex export,
214 # so we don't claim to be a multi-file export.
215 resources.pop("outputs", None)
216 resources.pop("attachments", None)
218 return pdf_data, resources