Compare commits
12 Commits
3030798f71
...
master
Author | SHA1 | Date | |
---|---|---|---|
fdb43cc18f | |||
7bbaf1132d | |||
90067b4068 | |||
526d920bc9 | |||
51d900844d | |||
e311deeb6b | |||
737d64e0a8 | |||
86d523d222 | |||
6a130d0086 | |||
ab850bcedb | |||
a122e2bba1 | |||
36d182c860 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -96,3 +96,6 @@ ENV/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# pytest
|
||||
.pytest_cache/
|
||||
|
32
README.md
32
README.md
@@ -1,6 +1,12 @@
|
||||
# Pytex
|
||||
|
||||
Pytex is a simple package which make aa bridge between Latex and Python.
|
||||
Pytex is a simple package which make a bridge between Latex and Python.
|
||||
|
||||
## Installation
|
||||
|
||||
``` bash
|
||||
pip install mypytex
|
||||
```
|
||||
|
||||
## texenv: Bring Python inside latex
|
||||
|
||||
@@ -9,6 +15,30 @@ Pytex is a simple package which make aa bridge between Latex and Python.
|
||||
- **\Block{#}** is the equivalent of **{% block #%}** in Jinja2. Every python's commands will be run like in a script. **%-** is also available for line statement.
|
||||
- **\Var{#}** is the equivalent of **{{#}}** which print the statment.
|
||||
|
||||
Use the environment with a string template.
|
||||
|
||||
``` python
|
||||
>>> from pytex import texenv
|
||||
|
||||
>>> text = "Hello"
|
||||
>>> template = """\
|
||||
\Var{text}
|
||||
\Block{set a = 2}
|
||||
%-set b = 3
|
||||
\Var{a}
|
||||
\Var{b}
|
||||
"""
|
||||
|
||||
>>> template = texenv.from_string(template)
|
||||
>>> print(template.render(text = text)
|
||||
Hello
|
||||
|
||||
2
|
||||
3
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
## feed: converting template into tex files
|
||||
|
||||
|
60
pytex/latex_error_parser.py
Normal file
60
pytex/latex_error_parser.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
"""
|
||||
Parsing latex error to bubble up import ones
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
def coroutine(func):
|
||||
@wraps(func)
|
||||
def start(*args, **kwargs):
|
||||
cr = func(*args, **kwargs)
|
||||
next(cr)
|
||||
return cr
|
||||
return start
|
||||
|
||||
@coroutine
|
||||
def generic_sink(func):
|
||||
""" Generic sink
|
||||
|
||||
:param function: function that define the end of the sink
|
||||
|
||||
>>> print_sink = generic_sink(print)
|
||||
>>> print_sink.send("coucou")
|
||||
coucou
|
||||
"""
|
||||
while True:
|
||||
c = (yield)
|
||||
if c:
|
||||
func(c)
|
||||
|
||||
@coroutine
|
||||
def filter_errors(target):
|
||||
""" Filter pdflatex log to bubble up error
|
||||
|
||||
https://en.wikibooks.org/wiki/LaTeX/Errors_and_Warnings
|
||||
|
||||
>>> s = generic_sink(print)
|
||||
>>> tex_filter = filter_errors(s)
|
||||
>>> tex_filter.send("! Undefined control sequence.")
|
||||
! Undefined control sequence.
|
||||
>>> tex_filter.send("l.43 \\includegraphics")
|
||||
l.43 \\includegraphics
|
||||
>>> tex_filter.send("l.43 \\includegraphics")
|
||||
>>>
|
||||
"""
|
||||
while True:
|
||||
line = (yield)
|
||||
if line.startswith("!"):
|
||||
target.send(line)
|
||||
line = (yield)
|
||||
while not line.startswith('See'):
|
||||
target.send(line)
|
||||
line = (yield)
|
||||
|
||||
# -----------------------------
|
||||
# Reglages pour 'vim'
|
||||
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
|
||||
# cursor: 16 del
|
108
pytex/pytex.py
108
pytex/pytex.py
@@ -9,10 +9,21 @@ import logging
|
||||
import math as m
|
||||
import subprocess
|
||||
import random as rd
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
import os
|
||||
from .texenv import *
|
||||
from .latex_error_parser import generic_sink, filter_errors
|
||||
|
||||
formatter = logging.Formatter('%(name)s :: %(levelname)s :: %(message)s')
|
||||
steam_handler = logging.StreamHandler()
|
||||
steam_handler.setLevel(logging.DEBUG)
|
||||
steam_handler.setFormatter(formatter)
|
||||
# création de l'objet logger qui va nous servir à écrire dans les logs
|
||||
# on met le niveau du logger à DEBUG, comme ça il écrit tout
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.ERROR)
|
||||
logger.addHandler(steam_handler)
|
||||
|
||||
|
||||
EXPORT_DICT = {}
|
||||
EXPORT_DICT.update(m.__dict__)
|
||||
@@ -29,82 +40,109 @@ def update_export_dict(new_dict):
|
||||
"""
|
||||
EXPORT_DICT.update(new_dict)
|
||||
|
||||
def run_python(template:str, data:dict):
|
||||
""" Runs python commands using data
|
||||
|
||||
:param template: string containing python commands
|
||||
:param data: dictionary with variables needed to run commands
|
||||
|
||||
def feed(template, data, output="", force=0):
|
||||
""" Feed template with data to output
|
||||
"""
|
||||
jinja_template = texenv.from_string(template)
|
||||
return jinja_template.render(**data)
|
||||
|
||||
:param template: jinja2 template with texenv environment
|
||||
:param data: Data dictionnary
|
||||
def build_output(filename):
|
||||
""" Replace tpl with a number in filename and insure that this file does not exists """
|
||||
|
||||
num = 1
|
||||
output_p = Path(filename.replace('tpl', f'{num:02d}'))
|
||||
while output_p.exists():
|
||||
logger.debug(f"{output_p} exists. Try next one")
|
||||
num += 1
|
||||
output_p = Path(filename.replace('tpl', f'{num:02d}'))
|
||||
|
||||
return output_p
|
||||
|
||||
def feed(template_filename, data, output="", force=0):
|
||||
""" Feed template_filename with data and write it to output
|
||||
|
||||
:param template_filename: jinja2 template with texenv environment
|
||||
:param data: Data dictionary
|
||||
:param output: name of the output file
|
||||
(by default: tpl is replaced by a 2 digits number)
|
||||
:param force: Override is the output already exists
|
||||
|
||||
:return: output filename
|
||||
"""
|
||||
logger.info(f"Getting template {template}")
|
||||
tpl = texenv.get_template(str(template))
|
||||
logger.info(f"Getting template_filename {template_filename}")
|
||||
with open(template_filename, "r") as f:
|
||||
fed = run_python("".join(f.readlines()), data)
|
||||
|
||||
if not output:
|
||||
num = 1
|
||||
output_p = Path(template.replace('tpl', f'{num:02d}'))
|
||||
while output_p.exists() and not force:
|
||||
logger.debug(f"{output_p} exists. Try next one")
|
||||
num += 1
|
||||
output_p = Path(template.replace('tpl', f'{num:02d}'))
|
||||
output_p = build_output(template_filename)
|
||||
else:
|
||||
output_p = Path(output)
|
||||
if not force and output_p.exists():
|
||||
logger.error(f"{output} exists. Use force=1 do override it")
|
||||
raise ValueError(f"{output} exists. Use force=1 do override it")
|
||||
|
||||
output_dir = output_p.dirname()
|
||||
if not force and output_p.exists():
|
||||
logger.error(f"{output} exists. Use force=1 do override it")
|
||||
raise ValueError(f"{output} exists. Use force=1 do override it")
|
||||
|
||||
output_dir = output_p.parent
|
||||
if output_dir and not output_dir.exists():
|
||||
output_dir.mkdir_p()
|
||||
logger.debug(f"Creating output dir {output_dir}")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
with open(output_p, "w") as output_f:
|
||||
output_f.write(tpl.render(**EXPORT_DICT, **data))
|
||||
logger.info(f"{template} has been rendered to {output}.")
|
||||
output_f.write(fed)
|
||||
logger.info(f"{template_filename} has been rendered to {output}.")
|
||||
|
||||
return output_p
|
||||
|
||||
|
||||
def pdflatex(latex_file, output_dir=""):
|
||||
""" Compile latex file
|
||||
def pdflatex(tex_filename, output_dir=""):
|
||||
""" Compile a latex file with pdflatex
|
||||
|
||||
If output_dir is not set, it produce it next to the latex file.
|
||||
"""
|
||||
latex_file = Path(tex_filename)
|
||||
if not output_dir:
|
||||
output_dir = Path(latex_file).dirname().abspath()
|
||||
logger.debug(f"output_dir for dflatex is {output_dir}")
|
||||
output_dir = latex_file.parent.resolve()
|
||||
logger.debug(f"output_dir for pdflatex is {output_dir}")
|
||||
|
||||
pwd = Path('./').abspath()
|
||||
Path(output_dir).cd()
|
||||
prev_cwd = Path.cwd()
|
||||
os.chdir(output_dir)
|
||||
compilation = subprocess.Popen(
|
||||
[
|
||||
"pdflatex",
|
||||
# f"-output-directory={output_dir}",
|
||||
"lualatex",
|
||||
f"-output-directory={output_dir}",
|
||||
# "-halt-on-error",
|
||||
"-interaction=nonstopmode",
|
||||
"-shell-escape",
|
||||
str(Path(latex_file).name),
|
||||
str(latex_file.name),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
# shell=True
|
||||
)
|
||||
|
||||
latex_error_logger = filter_errors(generic_sink(logger.error))
|
||||
for line in compilation.stdout:
|
||||
if b"Error" in line:
|
||||
logger.error(line)
|
||||
latex_error_logger.send(line.decode("latin-1").rstrip('\r\n'))
|
||||
compilation_status = compilation.wait()
|
||||
|
||||
logger.debug(f"{latex_file.name} has been compiled in {output_dir}")
|
||||
pwd.cd()
|
||||
|
||||
os.chdir(prev_cwd)
|
||||
|
||||
|
||||
def clean(dirname=".", garbages=["*.aux", "*.log"]):
|
||||
def clean(dirname="", garbages=["*.aux", "*.log"]):
|
||||
""" Clean the directory from aux and log latex files """
|
||||
if not dirname:
|
||||
dirname = Path("./")
|
||||
for g in garbages:
|
||||
g_files = Path(dirname).files(g)
|
||||
g_files = Path(dirname).glob(g)
|
||||
logger.debug(f"Remove {g_files}")
|
||||
for g_file in g_files:
|
||||
g_file.remove()
|
||||
g_file.unlink()
|
||||
|
||||
# -----------------------------
|
||||
# Reglages pour 'vim'
|
||||
|
@@ -24,7 +24,6 @@ texenv = jinja2.Environment(
|
||||
line_statement_prefix='%-',
|
||||
line_comment_prefix='%#',
|
||||
loader=jinja2.ChoiceLoader([
|
||||
# jinja2.PackageLoader("notes_tools.reports", "templates"),
|
||||
jinja2.FileSystemLoader(['./']),
|
||||
]),
|
||||
extensions=['jinja2.ext.do']
|
||||
|
3
setup.py
3
setup.py
@@ -5,7 +5,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='mypytex',
|
||||
version='0.2',
|
||||
version='0.4',
|
||||
description='Writing latex files and compile it with python and jinja2',
|
||||
url='https://git.opytex.org/lafrite/Pytex',
|
||||
author='Bertrand Benjamin',
|
||||
@@ -14,7 +14,6 @@ setup(
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
'jinja2',
|
||||
'path.py',
|
||||
],
|
||||
)
|
||||
|
||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
61
tests/test_pytex.py
Normal file
61
tests/test_pytex.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from pytex import feed, pdflatex, clean
|
||||
import os
|
||||
from shutil import copyfile
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
TESTDIR = Path("tests")
|
||||
GOOD_TEMPLATE = TESTDIR / "tpl_good.tex"
|
||||
|
||||
GOOD_DATA = {
|
||||
"name": "Bob",
|
||||
"repetitions": 4,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def prepare_templates(tmpdir):
|
||||
wdir = tmpdir
|
||||
good_tpl = Path(GOOD_TEMPLATE)
|
||||
copyfile(good_tpl, wdir / good_tpl.name)
|
||||
prev_cwd = Path.cwd()
|
||||
|
||||
os.chdir(wdir)
|
||||
yield wdir
|
||||
os.chdir(prev_cwd)
|
||||
|
||||
|
||||
def test_feed_once(prepare_templates):
|
||||
output = feed(GOOD_TEMPLATE.name, GOOD_DATA)
|
||||
|
||||
|
||||
def test_feed_twice(prepare_templates):
|
||||
output1 = feed(GOOD_TEMPLATE.name, GOOD_DATA)
|
||||
output2 = feed(GOOD_TEMPLATE.name, GOOD_DATA)
|
||||
|
||||
|
||||
def test_feed_output(prepare_templates):
|
||||
dest = "./tests/special_name.tex"
|
||||
output = feed(GOOD_TEMPLATE.name, GOOD_DATA, dest)
|
||||
assert output.samefile(dest)
|
||||
|
||||
|
||||
def test_feed_output_noforce():
|
||||
pass
|
||||
# output = feed(GOOD_TEMPLATE, GOOD_DATA, )
|
||||
# os.system(f"rm {output}")
|
||||
|
||||
|
||||
def test_feed_pdflatex(prepare_templates):
|
||||
latex_file = feed(GOOD_TEMPLATE.name, GOOD_DATA)
|
||||
pdflatex(latex_file)
|
||||
clean(garbages=["*.aux", "*.log", "*.pdf"])
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Reglages pour 'vim'
|
||||
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
|
||||
# cursor: 16 del
|
15
tests/tpl_good.tex
Normal file
15
tests/tpl_good.tex
Normal file
@@ -0,0 +1,15 @@
|
||||
\documentclass{article}
|
||||
\usepackage[utf8]{inputenc} % Unicode support (Umlauts etc.)
|
||||
\usepackage[french]{babel} % Change hyphenation rules
|
||||
|
||||
|
||||
\begin{document}
|
||||
Coucou comment allez vous?
|
||||
|
||||
Je m'appelle \Var{name}.
|
||||
|
||||
%- for j in range(repetitions)
|
||||
Je peux faire \Var{j} \\
|
||||
%- endfor
|
||||
|
||||
\end{document}
|
Reference in New Issue
Block a user