diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95ca2b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +venv/ +*.zip +*.lock +*.log +.vscode/ +latex.egg-info/ +sample.pdf \ No newline at end of file diff --git a/latex/build.py b/latex/build.py index 9fad818..3c52300 100644 --- a/latex/build.py +++ b/latex/build.py @@ -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. @@ -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. @@ -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 @@ -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 @@ -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." + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3e8e44a --- /dev/null +++ b/tests/conftest.py @@ -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") diff --git a/tests/test_halt_on_error_MkBuilder.py b/tests/test_halt_on_error_MkBuilder.py new file mode 100644 index 0000000..725daeb --- /dev/null +++ b/tests/test_halt_on_error_MkBuilder.py @@ -0,0 +1,134 @@ +"""Tests for build_pdf with the new halt_on_error parameter. +""" +import pytest +from latex.build import LatexMkBuilder +from latex import LatexBuildError + + +##### builder is not specified + + +def test_default_good_true(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder() + pdf = builder.build_pdf(good_minimal_latex, halt_on_error=True) + assert pdf + + +def test_default_good_false(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder() + pdf = builder.build_pdf(good_minimal_latex, halt_on_error=False) + assert pdf + + +def test_default_bad_true(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder() + with pytest.raises(LatexBuildError): + builder.build_pdf(bad_minimal_latex, halt_on_error=True) + + +def test_default_bad_false(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder() + pdf = builder.build_pdf(bad_minimal_latex, halt_on_error=False) + assert pdf + + +##### builder=latexmk + +# this is missleading: if user specified pdflatex, latexmk ist used + + +##### builder=pdflatex + + +def test_pdflatex_good_true(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="pdflatex") + pdf = builder.build_pdf(good_minimal_latex, halt_on_error=True) + assert pdf + + +def test_pdflatex_good_false(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="pdflatex") + pdf = builder.build_pdf(good_minimal_latex, halt_on_error=False) + assert pdf + + +def test_pdflatex_bad_true(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="pdflatex") + with pytest.raises(LatexBuildError): + builder.build_pdf(bad_minimal_latex, halt_on_error=True) + + +def test_pdflatex_bad_false(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="pdflatex") + pdf = builder.build_pdf(bad_minimal_latex, halt_on_error=False) + assert pdf + + +##### builder=lualatex + + +def test_lualatex_good_true(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="lualatex") + pdf = builder.build_pdf(good_extra_latex, halt_on_error=True) + assert pdf + + +def test_lualatex_good_false(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="lualatex") + pdf = builder.build_pdf(good_extra_latex, halt_on_error=False) + assert pdf + + +def test_lualatex_bad_true(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="lualatex") + with pytest.raises(LatexBuildError): + builder.build_pdf(bad_extra_latex, halt_on_error=True) + + +def test_lualatex_bad_false(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="lualatex") + pdf = builder.build_pdf(bad_extra_latex, halt_on_error=False) + assert pdf + + +###### builder=xelatex + + +def test_xelatex_good_true(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="xelatex") + pdf = builder.build_pdf(good_extra_latex, halt_on_error=True) + assert pdf + + +def test_xelatex_good_false(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="xelatex") + pdf = builder.build_pdf(good_extra_latex, halt_on_error=False) + assert pdf + + +def test_xelatex_bad_true(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="xelatex") + with pytest.raises(LatexBuildError): + builder.build_pdf(bad_extra_latex, halt_on_error=True) + + +def test_xelatex_bad_false(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + builder = LatexMkBuilder(variant="xelatex") + pdf = builder.build_pdf(bad_extra_latex, halt_on_error=False) + assert pdf diff --git a/tests/test_halt_on_error_build_pdf.py b/tests/test_halt_on_error_build_pdf.py new file mode 100644 index 0000000..d7758d3 --- /dev/null +++ b/tests/test_halt_on_error_build_pdf.py @@ -0,0 +1,142 @@ +"""Tests for build_pdf with the new halt_on_error parameter. +""" + +import pytest +from latex import LatexBuildError, build_pdf + + +##### default (no builder specified) + + +def test_default_good_true(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_minimal_latex, halt_on_error=True) + assert pdf + + +def test_default_good_false(good_minimal_latex): + "If halt-on-error=False, no BuildError should be thrown." + pdf = build_pdf(good_minimal_latex, halt_on_error=False) + assert pdf + + +def test_default_bad_true(bad_minimal_latex): + "If halt-on-error=True (default), BuildError should be thrown if error arises." + with pytest.raises(LatexBuildError): + # builder.build_pdf(bad_latex) + build_pdf(bad_minimal_latex) + + +def test_default_bad_false(bad_minimal_latex): + "If halt-on-error=False, no BuildError should be thrown." + pdf = build_pdf(bad_minimal_latex, halt_on_error=False) + assert pdf + + +##### builder=latexmk + + +def test_pdflatexmk_good_true(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_minimal_latex, builder="latexmk", halt_on_error=True) + assert pdf + + +def test_pdflatexmk_good_false(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_minimal_latex, builder="latexmk", halt_on_error=False) + assert pdf + + +def test_pdflatmk_bad_true(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + with pytest.raises(LatexBuildError): + build_pdf(bad_minimal_latex, builder="latexmk", halt_on_error=True) + + +def test_pdflatexmk_bad_false(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + pdf = build_pdf(bad_minimal_latex, builder="latexmk", halt_on_error=False) + assert pdf + + +##### builder=pdflatex + + +def test_pdflatex_good_true(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_minimal_latex, builder="pdflatex", halt_on_error=True) + assert pdf + + +def test_pdflatex_good_false(good_minimal_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_minimal_latex, builder="pdflatex", halt_on_error=False) + assert pdf + + +def test_pdflatex_bad_true(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + with pytest.raises(LatexBuildError): + build_pdf(bad_minimal_latex, builder="pdflatex", halt_on_error=True) + + +# this one does not work because pdflatex does ot produce any output??? +def test_pdflatex_bad_false(bad_minimal_latex): + "Invalid LaTeX with halt_on_error=True" + pdf = build_pdf(bad_minimal_latex, builder="pdflatex", halt_on_error=False) + assert pdf + + +##### builder=lualatex + + +def test_lualatexmk_good_true(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_extra_latex, builder="lualatexmk", halt_on_error=True) + assert pdf + + +def test_lualatexmk_good_false(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_extra_latex, builder="lualatexmk", halt_on_error=False) + assert pdf + + +def test_lualatexmk_bad_true(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + with pytest.raises(LatexBuildError): + build_pdf(bad_extra_latex, builder="lualatexmk", halt_on_error=True) + + +def test_lualatexmk_bad_false(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + pdf = build_pdf(bad_extra_latex, builder="lualatexmk", halt_on_error=False) + assert pdf + + +##### builder=xelatex + + +def test_xelatexmk_good_true(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_extra_latex, builder="xelatexmk", halt_on_error=True) + assert pdf + + +def test_xelatexmk_good_false(good_extra_latex): + "Valid LaTeX with halt_on_error=True" + pdf = build_pdf(good_extra_latex, builder="xelatexmk", halt_on_error=False) + assert pdf + + +def test_xelatexmk_bad_true(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + with pytest.raises(LatexBuildError): + build_pdf(bad_extra_latex, builder="xelatexmk", halt_on_error=True) + + +def test_xelatexmk_bad_false(bad_extra_latex): + "Invalid LaTeX with halt_on_error=True" + pdf = build_pdf(bad_extra_latex, builder="lualatexmk", halt_on_error=False) + assert pdf diff --git a/tests/test_lualatex.py b/tests/test_lualatex.py new file mode 100644 index 0000000..93af987 --- /dev/null +++ b/tests/test_lualatex.py @@ -0,0 +1,46 @@ +from latex import build_pdf, LatexBuildError +from latex.build import LatexMkBuilder + + +# the example below should not compile on pdflatex, but on lualatex +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} +""" + +def test_lualatex(): + builder = LatexMkBuilder(variant='lualatex') + pdf = builder.build_pdf(min_latex) + + assert pdf + + +def test_lualatexmk(): + pdf = build_pdf(min_latex, builder='lualatexmk') + + assert pdf + + +def test_luatextmk_errorlog(): + """Check if parsing of error lines works.""" + f_min_latex = min_latex.replace(r"\maketitle", r"\makexxx") + try: + build_pdf(f_min_latex, builder='lualatexmk') + except LatexBuildError as err: + assert err.get_errors() diff --git a/tox.ini b/tox.ini index dc0f1db..a9858ec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py27,py33,py34 +#envlist = py27,py33,py34 +envlist = py39 [testenv] deps=pytest