# -*- 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 subprocess
import errno
from .base_classes import Command, Container, LatexObject
import pylatex
from pylatex import Package
from pylatex.errors import CompilerError
from pylatex.utils import rm_temp_dir
import pylatex.config as cf
[docs]class Document(pylatex.Document):
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=None,
inputenc=None, font_size=None, lmodern=None,
textcomp=None, microtype=None, page_numbers=None, indent=None,
geometry_options=None, data=None):
r"""
Args
----
default_filepath: str
The default path to save files.
documentclass: str or `~pylatex.base_classes.command.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: str or list
The options to supply to the geometry package
data: list
Initial content of the document.
"""
# preserve old default values for non standalone
if documentclass != 'standalone':
fontenc = 'T1' if fontenc is None else fontenc
inputenc = 'utf8' if inputenc is None else inputenc
lmodern = True if lmodern is None else lmodern
textcomp = True if textcomp is None else textcomp
page_numbers = True if page_numbers is None else page_numbers
font_size = 'normalsize' if font_size is None else font_size
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(pylatex.Document, self).__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
if font_size is not None:
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)
[docs] 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 = []
filepath = self._select_filepath(filepath)
filepath = os.path.join('.', filepath)
cur_dir = os.getcwd()
dest_dir = os.path.dirname(filepath)
basename = os.path.basename(filepath)
if basename == '':
basename = 'default_basename'
os.chdir(dest_dir)
self.generate_tex(basename)
if compiler is not None:
compilers = ((compiler, []),)
else:
latexmk_args = ['--pdf']
compilers = (
('latexmk', latexmk_args),
('pdflatex', [])
)
main_arguments = ['--interaction=nonstopmode', basename + '.tex']
os_error = None
for compiler, arguments in compilers:
command = [compiler] + arguments + compiler_args + main_arguments
try:
output = subprocess.check_output(command,
stderr=subprocess.STDOUT)
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
# try to catch windows 'perl.exe' not found so that we can
# try pdflatex instead rather than just crashing
output = str(e.output.decode())
import re
import sys
output = re.sub(r'\s+', '', output)
if "couldnotfindthescriptengine'perl.exe'" in output:
print("ERROR: Compiler latexmk failed since the dependency"
" 'perl.exe' was not found. Trying alternative "
"compilers. Specify the compiler in future to avoid"
" this check if not using latexmk.",
file=sys.stderr)
continue
else:
print(e.output.decode())
raise e
else:
if not silent:
print(output.decode())
if clean:
try:
# Try latexmk cleaning first
subprocess.check_output(['latexmk', '-c', basename],
stderr=subprocess.STDOUT)
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(basename + '.' + 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(basename + '.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.'
))
os.chdir(cur_dir)