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

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

2 

3# Copyright (c) IPython Development Team. 

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

5 

6import os 

7import shutil 

8import subprocess 

9import sys 

10from tempfile import TemporaryDirectory 

11 

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

13 

14from nbconvert.utils import _contextlib_chdir 

15 

16from .latex import LatexExporter 

17 

18 

19class LatexFailed(IOError): # noqa 

20 """Exception for failed latex run 

21 

22 Captured latex output is in error.output. 

23 """ 

24 

25 def __init__(self, output): 

26 """Initialize the error.""" 

27 self.output = output 

28 

29 def __unicode__(self): 

30 """Unicode representation.""" 

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

32 

33 def __str__(self): 

34 """String representation.""" 

35 u = self.__unicode__() 

36 return u 

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() 

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( # noqa 

88 self, command_list, filename, count, log_function, raise_on_failure=None 

89 ): 

90 """Run command_list count times. 

91 

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. 

104 

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] 

112 

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) 

123 

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

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

126 

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) 

134 

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 

161 

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

163 """Run xelatex self.latex_count times.""" 

164 

165 def log_error(command, out): 

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

167 

168 return self.run_command( 

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

170 ) 

171 

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

173 """Run bibtex one time.""" 

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

175 

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) 

181 

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

183 

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() 

192 

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) 

202 

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() 

209 

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) 

217 

218 return pdf_data, resources