Skip to content
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.pyc
venv/
*.zip
*.lock
*.log
.vscode/
latex.egg-info/
sample.pdf
189 changes: 116 additions & 73 deletions latex/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class LatexBuilder(object):
"""Base class for Latex builders."""

def build_pdf(self, source, texinputs=[]):
def build_pdf(self, source, texinputs=[], halt_on_error=True):
"""Generates a PDF from LaTeX a source.

If there are errors generating a ``LatexError`` is raised.
Expand Down Expand Up @@ -55,77 +55,106 @@ class LatexMkBuilder(LatexBuilder):
``$PATH``).
:param xelatex: The path to the ``xelatex`` binary (will be looked up on
``$PATH``).
:param lualatex: The path to the ``lualatex`` binary (will be looked up on
``$PATH``).
:param variant: The LaTeX variant to use. Valid choices are
`pdflatex` and `xelatex`. Defaults to `pdflatex`.
`pdflatex`, `xelatex` and `lualatex`. Defaults to `pdflatex`.
"""

def __init__(self, latexmk='latexmk', pdflatex='pdflatex',
xelatex='xelatex', variant='pdflatex'):
def __init__(
self,
latexmk="latexmk",
pdflatex="pdflatex",
xelatex="xelatex",
lualatex="lualatex",
variant="pdflatex",
):
self.latexmk = latexmk
self.pdflatex = pdflatex
self.xelatex = xelatex
self.lualatex = lualatex
self.variant = variant

@data('source')
def build_pdf(self, source, texinputs=[]):
with TempDir() as tmpdir,\
source.temp_saved(suffix='.latex', dir=tmpdir) as tmp:
@data("source")
def build_pdf(self, source, texinputs=[], halt_on_error=True):
with TempDir() as tmpdir, source.temp_saved(suffix=".latex", dir=tmpdir) as tmp:

# close temp file, so other processes can access it also on Windows
tmp.close()

base_fn = os.path.splitext(tmp.name)[0]
output_fn = base_fn + '.pdf'

latex_cmd = [shlex_quote(self.pdflatex),
'-interaction=batchmode',
'-halt-on-error',
'-no-shell-escape',
'-file-line-error',
'%O',
'%S', ]

if self.variant == 'pdflatex':
args = [self.latexmk,
'-pdf',
'-pdflatex={}'.format(' '.join(latex_cmd)),
tmp.name, ]
elif self.variant == 'xelatex':
args = [self.latexmk,
'-xelatex',
tmp.name, ]
output_fn = base_fn + ".pdf"
latex_cmd = [
shlex_quote(self.pdflatex),
"-interaction=batchmode",
"-no-shell-escape",
"-file-line-error",
"%O",
"%S",
]
if halt_on_error:
latex_cmd.insert(2, "-halt-on-error")

if self.variant == "pdflatex":
args = [
self.latexmk,
"-pdf",
"-pdflatex={}".format(" ".join(latex_cmd)),
tmp.name,
]
elif self.variant == "xelatex":
args = [
self.latexmk,
"-xelatex",
tmp.name,
]
if not halt_on_error:
args.insert(2, "-latexoption=-interaction=batchmode")
elif self.variant == "lualatex":
args = [
self.latexmk,
"-lualatex",
"-latexoption=--file-line-error",
tmp.name,
]
if not halt_on_error:
args.insert(2, "-latexoption=-interaction=batchmode")
else:
raise ValueError('Invalid LaTeX variant: {}'.format(
self.variant))
raise ValueError("Invalid LaTeX variant: {}".format(self.variant))

# create environment
newenv = os.environ.copy()
newenv['TEXINPUTS'] = os.pathsep.join(texinputs) + os.pathsep
newenv["TEXINPUTS"] = os.pathsep.join(texinputs) + os.pathsep

try:
subprocess.check_call(args,
cwd=tmpdir,
env=newenv,
stdin=open(os.devnull, 'r'),
stdout=open(os.devnull, 'w'),
stderr=open(os.devnull, 'w'), )
subprocess.check_call(
args,
cwd=tmpdir,
env=newenv,
stdin=open(os.devnull, "r"),
stdout=open(os.devnull, "w"),
stderr=open(os.devnull, "w"),
)
except CalledProcessError as e:
raise_from(LatexBuildError(base_fn + '.log'), e)
if halt_on_error:
raise_from(LatexBuildError(base_fn + ".log"), e)

return I(open(output_fn, 'rb').read(), encoding=None)
return I(open(output_fn, "rb").read(), encoding=None)

def is_available(self):
if not which(self.latexmk):
return False

if self.variant == 'pdflatex':
if self.variant == "pdflatex":
return bool(which(self.pdflatex))
if self.variant == 'xelatex':
if self.variant == "xelatex":
return bool(which(self.xelatex))
if self.variant == "lualatex":
return bool(which(self.lualatex))


class PdfLatexBuilder(LatexBuilder):
"""A simple pdflatex based buidler for LaTeX files.
"""A simple pdflatex based builder for LaTeX files.

Builds LaTeX files by copying them to a temporary directly and running
``pdflatex`` until the associated ``.aux`` file stops changing.
Expand All @@ -140,44 +169,54 @@ class PdfLatexBuilder(LatexBuilder):
``pdflatex`` can be rerun before an exception is thrown.
"""

def __init__(self, pdflatex='pdflatex', max_runs=15):
def __init__(self, pdflatex="pdflatex", max_runs=15):
self.pdflatex = pdflatex
self.max_runs = 15

@data('source')
def build_pdf(self, source, texinputs=[]):
with TempDir() as tmpdir,\
source.temp_saved(suffix='.latex', dir=tmpdir) as tmp:
@data("source")
def build_pdf(self, source, texinputs=[], halt_on_error=True):
with TempDir() as tmpdir, source.temp_saved(suffix=".latex", dir=tmpdir) as tmp:

# close temp file, so other processes can access it also on Windows
tmp.close()

# calculate output filename
base_fn = os.path.splitext(tmp.name)[0]
output_fn = base_fn + '.pdf'
aux_fn = base_fn + '.aux'
args = [self.pdflatex, '-interaction=batchmode', '-halt-on-error',
'-no-shell-escape', '-file-line-error', tmp.name]

output_fn = base_fn + ".pdf"
aux_fn = base_fn + ".aux"
args = [
self.pdflatex,
"-interaction=batchmode",
"-no-shell-escape",
"-file-line-error",
tmp.name,
]
if halt_on_error:
args.insert(2, "-halt-on-error")

# create environment
newenv = os.environ.copy()
newenv['TEXINPUTS'] = os.pathsep.join(texinputs) + os.pathsep
newenv["TEXINPUTS"] = os.pathsep.join(texinputs) + os.pathsep

# run until aux file settles
prev_aux = None
runs_left = self.max_runs
while runs_left:
try:
subprocess.check_call(args,
cwd=tmpdir,
env=newenv,
stdin=open(os.devnull, 'r'),
stdout=open(os.devnull, 'w'), )
subprocess.check_call(
args,
cwd=tmpdir,
env=newenv,
stdin=open(os.devnull, "r"),
stdout=open(os.devnull, "w"),
)
except CalledProcessError as e:
raise_from(LatexBuildError(base_fn + '.log'), e)
if halt_on_error:
raise_from(LatexBuildError(base_fn + ".log"), e)

# check aux-file
aux = open(aux_fn, 'rb').read()
aux = open(aux_fn, "rb").read()

if aux == prev_aux:
break
Expand All @@ -186,25 +225,28 @@ def build_pdf(self, source, texinputs=[]):
runs_left -= 1
else:
raise RuntimeError(
'Maximum number of runs ({}) without a stable .aux file '
'reached.'.format(self.max_runs))
"Maximum number of runs ({}) without a stable .aux file "
"reached.".format(self.max_runs)
)

return I(open(output_fn, 'rb').read(), encoding=None)
return I(open(output_fn, "rb").read(), encoding=None)

def is_available(self):
return bool(which(self.pdflatex))


BUILDERS = {
'latexmk': LatexMkBuilder,
'pdflatex': PdfLatexBuilder,
'xelatexmk': lambda: LatexMkBuilder(variant='xelatex'),
"latexmk": LatexMkBuilder,
# "pdflatex": PdfLatexBuilder, # this is not reliable!
"pdflatex": LatexMkBuilder,
"xelatexmk": lambda: LatexMkBuilder(variant="xelatex"),
"lualatexmk": lambda: LatexMkBuilder(variant="lualatex"),
}

PREFERRED_BUILDERS = ('latexmk', 'pdflatex', 'xelatexmk')
PREFERRED_BUILDERS = ("latexmk", "pdflatex", "xelatexmk", "lualatexmk")


def build_pdf(source, texinputs=[], builder=None):
def build_pdf(source, texinputs=[], builder=None, halt_on_error=True):
"""Builds a LaTeX source to PDF.

Will automatically instantiate an available builder (or raise a
Expand All @@ -215,21 +257,22 @@ def build_pdf(source, texinputs=[], builder=None):
:meth:`~latex.build.LatexBuilder.build_pdf` function.

:param builder: Specify which builder should be used - ``latexmk``,
``pdflatex`` or ``xelatexmk``.
``pdflatex``, ``xelatexmk`` or `lualatexmk`.
"""
if builder is None:
builders = PREFERRED_BUILDERS
elif builder not in BUILDERS:
raise RuntimeError('Invalid Builder specified')
raise RuntimeError("Invalid Builder specified: {}".format(builder))
else:
builders = (builder, )

builders = (builder,)
for bld in builders:
bld_cls = BUILDERS[bld]
builder = bld_cls()
if not builder.is_available():
continue
return builder.build_pdf(source, texinputs)
return builder.build_pdf(source, texinputs, halt_on_error)
else:
raise RuntimeError('No available builder could be instantiated. '
'Please make sure LaTeX is installed.')
raise RuntimeError(
"No available builder could be instantiated. "
"Please make sure LaTeX is installed."
)
50 changes: 50 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest


@pytest.fixture
def good_minimal_latex():
"Return a minimal latex code."
latex_code = r"""
\documentclass{article}
\begin{document}
Hello, world!
\end{document}
"""
return latex_code


@pytest.fixture
def bad_minimal_latex(good_minimal_latex):
return good_minimal_latex.replace("begin", "bgin")


@pytest.fixture
def good_extra_latex():
"A LaTeX code, which only works with lua and xelatex."
min_latex = r"""
\documentclass[12pt]{article}
\usepackage{fontspec}

\setmainfont{Times New Roman}

\title{Sample font document}
\author{Hubert Farnsworth}
\date{this month, 2014}

\begin{document}

\maketitle

This an \textit{example} of document compiled
with \textbf{xelatex} compiler. LuaLaTeX should
work fine also.

\end{document}
"""
return min_latex


@pytest.fixture
def bad_extra_latex(good_extra_latex):
"A LateX code for lua and xelatex with error."
return good_extra_latex.replace("title", "ttle")
Loading