/
document.py
409 lines (333 loc) · 13.5 KB
/
document.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# -*- coding: utf-8 -*-
"""
This module implements the class that deals with the full document.
.. :copyright: (c) 2014 by Jelte Fennema.
:license: MIT, see License for more details.
"""
import os
import sys
import subprocess
import errno
from .base_classes import Environment, Command, Container, LatexObject, \
UnsafeCommand
from .package import Package
from .errors import CompilerError
from .utils import dumps_list, rm_temp_dir, NoEscape
import pylatex.config as cf
class Document(Environment):
r"""
A class that contains a full LaTeX document.
If needed, you can append stuff to the preamble or the packages.
For instance, if you need to use ``\maketitle`` you can add the title,
author and date commands to the preamble to make it work.
"""
def __init__(self, default_filepath='default_filepath', *,
documentclass='article', document_options=None, fontenc='T1',
inputenc='utf8', font_size="normalsize", lmodern=True,
textcomp=True, microtype=None, page_numbers=True, indent=None,
geometry_options=None, data=None):
r"""
Args
----
default_filepath: str
The default path to save files.
documentclass: str or `~.Command`
The LaTeX class of the document.
document_options: str or `list`
The options to supply to the documentclass
fontenc: str
The option for the fontenc package. If it is `None`, the fontenc
package will not be loaded at all.
inputenc: str
The option for the inputenc package. If it is `None`, the inputenc
package will not be loaded at all.
font_size: str
The font size to declare as normalsize
lmodern: bool
Use the Latin Modern font. This is a font that contains more glyphs
than the standard LaTeX font.
textcomp: bool
Adds even more glyphs, for instance the Euro (€) sign.
page_numbers: bool
Adds the ability to add the last page to the document.
indent: bool
Determines whether or not the document requires indentation. If it
is `None` it will use the value from the active config. Which is
`True` by default.
geometry_options: dict
The options to supply to the geometry package
data: list
Initial content of the document.
"""
self.default_filepath = default_filepath
if isinstance(documentclass, Command):
self.documentclass = documentclass
else:
self.documentclass = Command('documentclass',
arguments=documentclass,
options=document_options)
if indent is None:
indent = cf.active.indent
if microtype is None:
microtype = cf.active.microtype
# These variables are used by the __repr__ method
self._fontenc = fontenc
self._inputenc = inputenc
self._lmodern = lmodern
self._indent = indent
self._microtype = microtype
packages = []
if fontenc is not None:
packages.append(Package('fontenc', options=fontenc))
if inputenc is not None:
packages.append(Package('inputenc', options=inputenc))
if lmodern:
packages.append(Package('lmodern'))
if textcomp:
packages.append(Package('textcomp'))
if page_numbers:
packages.append(Package('lastpage'))
if not indent:
packages.append(Package('parskip'))
if microtype:
packages.append(Package('microtype'))
if geometry_options is not None:
packages.append(Package('geometry', options=geometry_options))
super().__init__(data=data)
# Usually the name is the class name, but if we create our own
# document class, \begin{document} gets messed up.
self._latex_name = 'document'
self.packages |= packages
self.variables = []
self.preamble = []
if not page_numbers:
self.change_document_style("empty")
# No colors have been added to the document yet
self.color = False
self.meta_data = False
self.append(Command(command=font_size))
def _propagate_packages(self):
r"""Propogate packages.
Make sure that all the packages included in the previous containers
are part of the full list of packages.
"""
super()._propagate_packages()
for item in (self.preamble):
if isinstance(item, LatexObject):
if isinstance(item, Container):
item._propagate_packages()
for p in item.packages:
self.packages.add(p)
def dumps(self):
"""Represent the document as a string in LaTeX syntax.
Returns
-------
str
"""
head = self.documentclass.dumps() + '%\n'
head += self.dumps_packages() + '%\n'
head += dumps_list(self.variables) + '%\n'
head += dumps_list(self.preamble) + '%\n'
return head + '%\n' + super().dumps()
def generate_tex(self, filepath=None):
"""Generate a .tex file for the document.
Args
----
filepath: str
The name of the file (without .tex), if this is not supplied the
default filepath attribute is used as the path.
"""
super().generate_tex(self._select_filepath(filepath))
def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True,
compiler=None, compiler_args=None, silent=True):
"""Generate a pdf file from the document.
Args
----
filepath: str
The name of the file (without .pdf), if it is `None` the
``default_filepath`` attribute will be used.
clean: bool
Whether non-pdf files created that are created during compilation
should be removed.
clean_tex: bool
Also remove the generated tex file.
compiler: `str` or `None`
The name of the LaTeX compiler to use. If it is None, PyLaTeX will
choose a fitting one on its own. Starting with ``latexmk`` and then
``pdflatex``.
compiler_args: `list` or `None`
Extra arguments that should be passed to the LaTeX compiler. If
this is None it defaults to an empty list.
silent: bool
Whether to hide compiler output
"""
if compiler_args is None:
compiler_args = []
# In case of newer python with the use of the cwd parameter
# one can avoid to physically change the directory
# to the destination folder
python_cwd_available = sys.version_info >= (3, 6)
filepath = self._select_filepath(filepath)
if not os.path.basename(filepath):
filepath = os.path.join(os.path.abspath(filepath),
'default_basename')
else:
filepath = os.path.abspath(filepath)
cur_dir = os.getcwd()
dest_dir = os.path.dirname(filepath)
if not python_cwd_available:
os.chdir(dest_dir)
self.generate_tex(filepath)
if compiler is not None:
compilers = ((compiler, []),)
else:
latexmk_args = ['--pdf']
compilers = (
('latexmk', latexmk_args),
('pdflatex', [])
)
main_arguments = ['--interaction=nonstopmode', filepath + '.tex']
check_output_kwargs = {}
if python_cwd_available:
check_output_kwargs = {'cwd': dest_dir}
os_error = None
for compiler, arguments in compilers:
command = [compiler] + arguments + compiler_args + main_arguments
try:
output = subprocess.check_output(command,
stderr=subprocess.STDOUT,
**check_output_kwargs)
except (OSError, IOError) as e:
# Use FileNotFoundError when python 2 is dropped
os_error = e
if os_error.errno == errno.ENOENT:
# If compiler does not exist, try next in the list
continue
raise
except subprocess.CalledProcessError as e:
# For all other errors print the output and raise the error
print(e.output.decode())
raise
else:
if not silent:
print(output.decode())
if clean:
try:
# Try latexmk cleaning first
subprocess.check_output(['latexmk', '-c', filepath],
stderr=subprocess.STDOUT,
**check_output_kwargs)
except (OSError, IOError, subprocess.CalledProcessError):
# Otherwise just remove some file extensions.
extensions = ['aux', 'log', 'out', 'fls',
'fdb_latexmk']
for ext in extensions:
try:
os.remove(filepath + '.' + ext)
except (OSError, IOError) as e:
# Use FileNotFoundError when python 2 is dropped
if e.errno != errno.ENOENT:
raise
rm_temp_dir()
if clean_tex:
os.remove(filepath + '.tex') # Remove generated tex file
# Compilation has finished, so no further compilers have to be
# tried
break
else:
# Notify user that none of the compilers worked.
raise(CompilerError(
'No LaTex compiler was found\n'
'Either specify a LaTex compiler '
'or make sure you have latexmk or pdfLaTex installed.'
))
if not python_cwd_available:
os.chdir(cur_dir)
def _select_filepath(self, filepath):
"""Make a choice between ``filepath`` and ``self.default_filepath``.
Args
----
filepath: str
the filepath to be compared with ``self.default_filepath``
Returns
-------
str
The selected filepath
"""
if filepath is None:
return self.default_filepath
else:
if os.path.basename(filepath) == '':
filepath = os.path.join(filepath, os.path.basename(
self.default_filepath))
return filepath
def change_page_style(self, style):
r"""Alternate page styles of the current page.
Args
----
style: str
value to set for the page style of the current page
"""
self.append(Command("thispagestyle", arguments=style))
def change_document_style(self, style):
r"""Alternate page style for the entire document.
Args
----
style: str
value to set for the document style
"""
self.append(Command("pagestyle", arguments=style))
def add_color(self, name, model, description):
r"""Add a color that can be used throughout the document.
Args
----
name: str
Name to set for the color
model: str
The color model to use when defining the color
description: str
The values to use to define the color
"""
if self.color is False:
self.packages.append(Package("color"))
self.color = True
self.preamble.append(Command("definecolor", arguments=[name,
model,
description]))
def change_length(self, parameter, value):
r"""Change the length of a certain parameter to a certain value.
Args
----
parameter: str
The name of the parameter to change the length for
value: str
The value to set the parameter to
"""
self.preamble.append(UnsafeCommand('setlength',
arguments=[parameter, value]))
def set_variable(self, name, value):
r"""Add a variable which can be used inside the document.
Variables are defined before the preamble. If a variable with that name
has already been set, the new value will override it for future uses.
This is done by appending ``\renewcommand`` to the document.
Args
----
name: str
The name to set for the variable
value: str
The value to set for the variable
"""
name_arg = "\\" + name
variable_exists = False
for variable in self.variables:
if name_arg == variable.arguments._positional_args[0]:
variable_exists = True
break
if variable_exists:
renew = Command(command="renewcommand",
arguments=[NoEscape(name_arg), value])
self.append(renew)
else:
new = Command(command="newcommand",
arguments=[NoEscape(name_arg), value])
self.variables.append(new)