Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nbconvert/exporters/pdf.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

103 statements  

1"""Export to PDF via latex""" 

2 

3# Copyright (c) IPython Development Team. 

4# Distributed under the terms of the Modified BSD License. 

5from __future__ import annotations 

6 

7import os 

8import shutil 

9import subprocess 

10import sys 

11from tempfile import TemporaryDirectory 

12 

13from traitlets import Bool, Instance, Integer, List, Unicode, default 

14 

15from nbconvert.utils import _contextlib_chdir 

16 

17from .latex import LatexExporter 

18 

19 

20class LatexFailed(IOError): 

21 """Exception for failed latex run 

22 

23 Captured latex output is in error.output. 

24 """ 

25 

26 def __init__(self, output): 

27 """Initialize the error.""" 

28 self.output = output 

29 

30 def __unicode__(self): 

31 """Unicode representation.""" 

32 return "PDF creating failed, captured latex output:\n%s" % self.output 

33 

34 def __str__(self): 

35 """String representation.""" 

36 return self.__unicode__() 

37 

38 

39def prepend_to_env_search_path(varname, value, envdict): 

40 """Add value to the environment variable varname in envdict 

41 

42 e.g. prepend_to_env_search_path('BIBINPUTS', '/home/sally/foo', os.environ) 

43 """ 

44 if not value: 

45 return # Nothing to add 

46 

47 envdict[varname] = value + os.pathsep + envdict.get(varname, "") 

48 

49 

50class PDFExporter(LatexExporter): 

51 """Writer designed to write to PDF files. 

52 

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 """ 

57 

58 export_from_notebook = "PDF via LaTeX" 

59 

60 latex_count = Integer(3, help="How many times latex will be called.").tag(config=True) 

61 

62 latex_command = List( 

63 ["xelatex", "{filename}", "-quiet"], help="Shell command used to compile latex." 

64 ).tag(config=True) 

65 

66 bib_command = List(["bibtex", "{filename}"], help="Shell command used to run bibtex.").tag( 

67 config=True 

68 ) 

69 

70 verbose = Bool(False, help="Whether to display the output of latex commands.").tag(config=True) 

71 

72 texinputs = Unicode(help="texinputs dir. A notebook's directory is added") 

73 writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={"build_directory": "."}) 

74 

75 output_mimetype = "application/pdf" 

76 

77 _captured_output = List(Unicode()) 

78 

79 @default("file_extension") 

80 def _file_extension_default(self): 

81 return ".pdf" 

82 

83 @default("template_extension") 

84 def _template_extension_default(self): 

85 return ".tex.j2" 

86 

87 def run_command(self, command_list, filename, count, log_function, raise_on_failure=None): 

88 """Run command_list count times. 

89 

90 Parameters 

91 ---------- 

92 command_list : list 

93 A list of args to provide to Popen. Each element of this 

94 list will be interpolated with the filename to convert. 

95 filename : unicode 

96 The name of the file to convert. 

97 count : int 

98 How many times to run the command. 

99 raise_on_failure: Exception class (default None) 

100 If provided, will raise the given exception for if an instead of 

101 returning False on command failure. 

102 

103 Returns 

104 ------- 

105 success : bool 

106 A boolean indicating if the command was successful (True) 

107 or failed (False). 

108 """ 

109 command = [c.format(filename=filename) for c in command_list] 

110 

111 # This will throw a clearer error if the command is not found 

112 cmd = shutil.which(command_list[0]) 

113 if cmd is None: 

114 link = "https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex" 

115 msg = ( 

116 f"{command_list[0]} not found on PATH, if you have not installed " 

117 f"{command_list[0]} you may need to do so. Find further instructions " 

118 f"at {link}." 

119 ) 

120 raise OSError(msg) 

121 

122 times = "time" if count == 1 else "times" 

123 self.log.info("Running %s %i %s: %s", command_list[0], count, times, command) 

124 

125 shell = sys.platform == "win32" 

126 if shell: 

127 command = subprocess.list2cmdline(command) # type:ignore[assignment] 

128 env = os.environ.copy() 

129 prepend_to_env_search_path("TEXINPUTS", self.texinputs, env) 

130 prepend_to_env_search_path("BIBINPUTS", self.texinputs, env) 

131 prepend_to_env_search_path("BSTINPUTS", self.texinputs, env) 

132 

133 with open(os.devnull, "rb") as null: 

134 stdout = subprocess.PIPE if not self.verbose else None 

135 for _ in range(count): 

136 p = subprocess.Popen( 

137 command, 

138 stdout=stdout, 

139 stderr=subprocess.STDOUT, 

140 stdin=null, 

141 shell=shell, # noqa: S603 

142 env=env, 

143 ) 

144 out, _ = p.communicate() 

145 if p.returncode: 

146 if self.verbose: # noqa: SIM108 

147 # verbose means I didn't capture stdout with PIPE, 

148 # so it's already been displayed and `out` is None. 

149 out_str = "" 

150 else: 

151 out_str = out.decode("utf-8", "replace") 

152 log_function(command, out) 

153 self._captured_output.append(out_str) 

154 if raise_on_failure: 

155 msg = f'Failed to run "{command}" command:\n{out_str}' 

156 raise raise_on_failure(msg) 

157 return False # failure 

158 return True # success 

159 

160 def run_latex(self, filename, raise_on_failure=LatexFailed): 

161 """Run xelatex self.latex_count times.""" 

162 

163 def log_error(command, out): 

164 self.log.critical("%s failed: %s\n%s", command[0], command, out) 

165 

166 return self.run_command( 

167 self.latex_command, filename, self.latex_count, log_error, raise_on_failure 

168 ) 

169 

170 def run_bib(self, filename, raise_on_failure=False): 

171 """Run bibtex one time.""" 

172 filename = os.path.splitext(filename)[0] 

173 

174 def log_error(command, out): 

175 self.log.warning( 

176 "%s had problems, most likely because there were no citations", command[0] 

177 ) 

178 self.log.debug("%s output: %s\n%s", command[0], command, out) 

179 

180 return self.run_command(self.bib_command, filename, 1, log_error, raise_on_failure) 

181 

182 def from_notebook_node(self, nb, resources=None, **kw): 

183 """Convert from notebook node.""" 

184 latex, resources = super().from_notebook_node(nb, resources=resources, **kw) 

185 # set texinputs directory, so that local files will be found 

186 if resources and resources.get("metadata", {}).get("path"): 

187 self.texinputs = os.path.abspath(resources["metadata"]["path"]) 

188 else: 

189 self.texinputs = os.getcwd() 

190 

191 self._captured_outputs = [] 

192 with TemporaryDirectory() as td, _contextlib_chdir.chdir(td): 

193 notebook_name = "notebook" 

194 resources["output_extension"] = ".tex" 

195 tex_file = self.writer.write(latex, resources, notebook_name=notebook_name) 

196 self.log.info("Building PDF") 

197 self.run_latex(tex_file) 

198 if self.run_bib(tex_file): 

199 self.run_latex(tex_file) 

200 

201 pdf_file = notebook_name + ".pdf" 

202 if not os.path.isfile(pdf_file): 

203 raise LatexFailed("\n".join(self._captured_output)) 

204 self.log.info("PDF successfully created") 

205 with open(pdf_file, "rb") as f: 

206 pdf_data = f.read() 

207 

208 # convert output extension to pdf 

209 # the writer above required it to be tex 

210 resources["output_extension"] = ".pdf" 

211 # clear figure outputs and attachments, extracted by latex export, 

212 # so we don't claim to be a multi-file export. 

213 resources.pop("outputs", None) 

214 resources.pop("attachments", None) 

215 

216 return pdf_data, resources