Source code for pylatex.document

# -*- 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)