# -*- 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 errno
import os
import subprocess
import sys
import pylatex.config as cf
from .base_classes import (
Command,
Container,
Environment,
LatexObject,
SpecialArguments,
UnsafeCommand,
)
from .errors import CompilerError
from .package import Package
from .utils import NoEscape, dumps_list, rm_temp_dir
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"))
# Make sure we don't add this options command for an empty list,
# because that breaks.
if geometry_options:
packages.append(
Command(
"geometry",
arguments=SpecialArguments(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)
[docs]
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()
[docs]
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))
[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 = []
# 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
[docs]
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))
[docs]
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))
[docs]
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])
)
[docs]
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]))
[docs]
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)