Merge branch 'decoupled' into dev
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Bertrand Benjamin 2022-07-20 16:46:45 +02:00
commit d204eb19e1
69 changed files with 1862 additions and 1344 deletions

13
.drone.yml Normal file
View File

@ -0,0 +1,13 @@
kind: pipeline
name: default
steps:
- name: Publish
image: plugins/pypi
settings:
username:
from_secret: pypi_username
password:
from_secret: pypi_password
when:
event: tag

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
tests/
__pycache__/ __pycache__/
*.pyc *.pyc
documentation/build/ documentation/build/

16
Dockerfile.simple Normal file
View File

@ -0,0 +1,16 @@
FROM python:3.11-rc-slim
RUN apt-get update
RUN apt-get install -y texlive texlive-extra-utils
RUN apt-get install -y python3-dev
COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt
RUN mkdir -p /src/bopytex
COPY bopytex/ /src/bopytex
COPY setup.py /src/
RUN pip install -e /src
COPY example/simple /example
WORKDIR /example

17
Dockerfile.usecase Normal file
View File

@ -0,0 +1,17 @@
FROM python:3.11-rc-slim
RUN apt-get update
RUN apt-get install -y texlive texlive-extra-utils
RUN apt-get install -y python3-dev
COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt
RUN pip install mapytex
RUN mkdir -p /src/bopytex
COPY bopytex/ /src/bopytex
COPY setup.py /src/
RUN pip install -e /src
COPY example/usecase /example
WORKDIR /example
ENV BOPYTEXCONFIG="bopytex_config.py"

12
Makefile Normal file
View File

@ -0,0 +1,12 @@
docker-build-simple:
docker build -f Dockerfile.simple -t simple .
docker-simple: docker-build-simple
docker run simple sh -c "bopytex -q 2 tpl_example.tex && cat 1_example.tex"
docker-build-usecase:
docker build -f Dockerfile.usecase -t usecase .
docker-usecase: docker-build-usecase
docker run usecase sh -c "bopytex -s students.csv tpl_example.tex && cat 1_example.tex"

139
README.md
View File

@ -1,6 +1,8 @@
# Bopytex # Bopytex
Bopytex is a command line tool for producing random math exercises with their correction. It embeds [mapytex](https://git.opytex.org/lafrite/Mapytex) and [python](python.org) into [latex](latex-project.org) through [jinja](jinja.pocoo.org). [![Build Status](https://drone.opytex.org/api/badges/lafrite/Bopytex/status.svg)](https://drone.opytex.org/lafrite/Bopytex)
Bopytex is a command line tool which embed python into latex. It uses jinja2 to do so with a modified environnement to match with latex syntax.
## Installing ## Installing
@ -10,84 +12,91 @@ Install and update using [pip](https://pip.pypa.io/en/stable/quickstart/)
## Simple example ## Simple example
Let's say I want an exercise on adding 2 fractions (files are in `examples`). ``` latex
% save this as tpl_simple.tex
\documentclass[12pt]{article}
The *latex* template called `tpl_add_fraction.tex` \title{Bopytex example -- {{ number }}}
\begin{document}
\maketitle
%- set a = 10
%- set n = 2
We have two variables
\begin{itemize}
\item a: \Var{a}
\item n: \Var{n}
\end{itemize}
%# We can use blocks
\begin{itemize}
%- for i in n
\item \Var{a}
%- endfor
\end{itemize}
\end{document}
```
To create a version a this document type this
``` bash
$ bopytex tpl_simple.tex
```
## How I use it
I build this program to produce individual exams subjects for each of my student with the correction associated. I write a template, and bopytex build subject and correction.
To produce formulas and values, I use an another tool I an developing: `mapytex <https://git.opytex.org/lafrite/Mapytex`. I am importing it through `bopytex_config.py`.
``` python
# bopytex_config.py
from mapytex import Expression
from random import random
direct_access = {
"Expression": Expression,
"random": random
}
```
Every variables, objects or function inside this file will be available inside the template.
``` latex ``` latex
% tpl_example.tpl
\documentclass[12pt]{article} \documentclass[12pt]{article}
\title{Bopytex with Mapytex example -- \Var{ subject.name }}
\begin{document} \begin{document}
\section{Ajouts de fractions} \maketitle
Adding two fractions %- set e = Expression.random("{a} + {b}")
%- set e = Expression.random("{a} / {b} + {c} / {k*b}", ["b > 1", "k>1"]) \Var{e}
\[
A = \Var{e}
\]
Solution
\[
\Var{e.simplify().explain() | join('=')}
\]
\end{document}
```
Generate latex files and compile those for 2 different subjects. \Var{e.simplify()}
```
bopytex -t tpl_add_fractions.tex -N 2
```
It produces 2 sources files
- `01_add_fractions.tex`
```latex
\documentclass[12pt]{article}
\begin{document}
\section{Ajouts de fractions}
Adding two fractions
\[
A = \frac{- 2}{4} + \frac{7}{8}
\]
Solution
\[
\frac{- 2}{4} + \frac{7}{8}=\frac{- 2 \times 2}{4 \times 2} + \frac{7}{8}=\frac{- 4}{8} + \frac{7}{8}=\frac{- 4 + 7}{8}=\frac{3}{8}
\]
\end{document} \end{document}
``` ```
- `02_add_fractions.tex` Information about my students are stored in a csv file (here `students.csv`)
```latex ``` csv
\documentclass[12pt]{article} "Name","Age","Email","fraction level","calculus level"
"Spike Tucker","22","s.tucker@randatmail.com","7","3"
\begin{document} "Martin Payne","21","m.payne@randatmail.com","7","3"
"Kimberly Baker","20","k.baker@randatmail.com","1","8"
\section{Ajouts de fractions} "Emma Bailey","29","e.bailey@randatmail.com","2","5"
"Nicholas Taylor","28","n.taylor@randatmail.com","3","3"
Adding two fractions
\[
A = \frac{8}{9} + \frac{3}{63}
\]
Solution
\[
\frac{8}{9} + \frac{3}{63}=\frac{8 \times 7}{9 \times 7} + \frac{3}{63}=\frac{56}{63} + \frac{3}{63}=\frac{56 + 3}{63}=\frac{59}{63}
\]
\end{document}
``` ```
And a ready to print pdf. Then I can produce a subject for each of my student
- [ all_add_fraction.pdf ]( ./examples/all_add_fraction.pdf )
``` bash
$ bopytex tpl_simple.tex -s students.csv -c bopytex_config.py
```

View File

@ -1,10 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
#from .bopytex import subject_metadatas, crazy_feed, pdfjoin
# ----------------------------- # -----------------------------
# Reglages pour 'vim' # Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4: # vim:set autoindent expandtab tabstop=4 shiftwidth=4:

239
bopytex/bopytex.py Executable file → Normal file
View File

@ -1,237 +1,2 @@
#!/usr/bin/env python import bopytex.default_config as DEFAULT
# encoding: utf-8 from bopytex.service import orcherstrator
"""
Producing then compiling templates
"""
import csv
import os
import logging
from pathlib import Path
import pytex
from mapytex import render, Expression, random, stat
import bopytex.filters as filters
import random as rd
formatter = logging.Formatter("%(name)s :: %(levelname)s :: %(message)s")
steam_handler = logging.StreamHandler()
steam_handler.setLevel(logging.DEBUG)
steam_handler.setFormatter(formatter)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(steam_handler)
def setup():
render.set_render("tex")
#logger.debug(f"Render for Expression is {Expression.RENDER}")
mapytex_tools = {
"Expression": Expression,
"rdm": random,
"stat": stat,
"random": rd,
}
pytex.update_export_dict(mapytex_tools)
pytex.add_filter("calculus", filters.do_calculus)
def get_working_dir(options):
""" Get the working directory """
if options["working_dir"]:
working_dir = Path(options["working_dir"])
else:
try:
template = Path(options["template"])
except TypeError:
raise ValueError(
"Need to set the working directory \
or to give a template"
)
else:
working_dir = template.parent
logger.debug(f"The output directory will be {working_dir}")
return working_dir
def activate_printanswers(
texfile, noans=r"solution/print = false", ans=r"solution/print = true"
):
""" Activate printanswers mod in texfile """
output_fname = "corr_" + texfile
with open(texfile, "r") as input_f:
with open(output_fname, "w") as output_f:
for line in input_f.readlines():
output_f.write(line.replace(noans, ans))
return output_fname
def deactivate_printanswers(corr_fname):
""" Activate printanswers mod in texfile """
Path(corr_fname).remove()
def pdfjoin(pdf_files, destname, working_dir=".", rm_pdfs=1):
"""TODO: Docstring for pdfjoin.
:param pdf_files: list of pdf files to join
:param destname: name for joined pdf
:param working_dir: the working directory
:param rm_pdfs: Remove pdf_files after joining them
:returns: TODO
"""
joined_pdfs = Path(working_dir) / Path(destname)
pdf_files_str = " ".join(pdf_files)
pdfjam = f"pdfjam {pdf_files_str} -o {joined_pdfs}"
logger.debug(f"Run {pdfjam}")
logger.info("Joining pdf files")
os.system(pdfjam)
if rm_pdfs:
logger.info(f"Remove {pdf_files_str}")
os.system(f"rm {pdf_files_str}")
def extract_student_csv(csv_filename):
""" Extract student list from csv_filename """
with open(csv_filename, "r") as csvfile:
reader = csv.DictReader(csvfile)
return [r for r in reader]
def subject_metadatas(options):
""" Return metadata on subject to produce
if csv is given it will based on is
otherwise it will be based on quantity
:example:
>>> subject_metadata(10)
"""
if options["students_csv"]:
metadatas = []
for (i, s) in enumerate(extract_student_csv(options["students_csv"])):
d = {"num": f"{i+1:02d}"}
d.update(s)
metadatas.append(d)
elif options["number_subjects"] > 0:
metadatas = [{"num": f"{i+1:02d}"} for i in range(options["number_subjects"])]
else:
raise ValueError("Need metacsv or quantity to build subject metadata")
for meta in metadatas:
meta.update(
{
"template": str(Path(options["template"]).name),
"texfile": str(Path(options["template"]).name).replace(
"tpl", meta["num"]
),
"directory": str(Path(options["template"]).parent),
}
)
return metadatas
def feed(*args, **kwrds):
""" Nice and smooth pytex feed """
pytex.feed(*args, **kwrds)
def crazy_feed(*args, **kwrds):
""" Crazy mod for pytex feed """
while True:
try:
pytex.feed(*args, **kwrds)
except:
logger.debug(f"Crazy feed is working hard...! {args} {kwrds}")
else:
break
def clean(directory):
pytex.clean(directory)
def texcompile(filename):
logger.debug(f"Start compiling {filename}")
pytex.pdflatex(Path(filename))
logger.debug(f"End compiling")
def produce_and_compile(options):
""" Produce and compile subjects
"""
logger.debug(f"CI parser gets {options}")
template = Path(options["template"]).name
directory = Path(options["template"]).parent
metadatas = subject_metadatas(options)
logger.debug(f"Metadata {metadatas}")
setup()
for meta in metadatas:
logger.debug(f"Feeding template toward {meta['texfile']}")
if options["crazy"]:
crazy_feed(
template=Path(meta["directory"]) / meta["template"],
data=meta,
output=meta["texfile"],
force=1,
)
else:
feed(
template=Path(meta["directory"]) / meta["template"],
data=meta,
output=meta["texfile"],
force=1,
)
assert(Path(meta["texfile"]).exists())
logger.debug(f"{meta['texfile']} fed")
if options["corr"]:
logger.debug(f"Building correction for {meta['texfile']}")
meta.update({
"corr_texfile": activate_printanswers(meta["texfile"]),
})
if not options["no_compile"]:
logger.debug("Compiling")
for prefix in ["", "corr_"]:
key = prefix + "texfile"
try:
meta[key]
except KeyError:
pass
else:
texcompile(meta[key])
meta.update({
prefix+'pdffile': meta[key].replace('tex', 'pdf')
})
if not options["no_join"]:
for prefix in ["", "corr_"]:
key = prefix + "pdffile"
try:
pdfs = [m[key] for m in metadatas]
except KeyError:
pass
else:
pdfjoin(
pdfs,
template.replace("tpl", prefix+"all").replace(".tex", ".pdf"),
directory,
rm_pdfs=1,
)
if not options["dirty"]:
clean(directory)
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

26
bopytex/default_config.py Normal file
View File

@ -0,0 +1,26 @@
from bopytex.planner.generate_compile_join_planner import planner
from bopytex.worker import Dispatcher
from bopytex.worker.activate_corr import activate_corr
from bopytex.worker.clean import clean
from bopytex.worker.compile import pdflatex
from bopytex.worker.generate import generate
from bopytex.worker.join_pdf import pdfjam
from bopytex.jinja2_env.texenv import texenv
jinja2 = {
"environment": texenv
}
dispatcher = Dispatcher({
"GENERATE": generate,
"COMPILE": pdflatex,
"JOIN": pdfjam,
"CLEAN": clean,
"ACTIVATE_CORR": activate_corr,
})
latex = {
"solution": r"solution/print = true",
"no_solution": r"solution/print = false",
}

View File

@ -1,33 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
"""
Custom filter for Bopytex
"""
__all__ = ["do_calculus"]
def do_calculus(steps, name="A", sep="=", end="", joining=" \\\\ \n"):
"""Display properly the calculus
Generate this form string:
"name & sep & a_step end joining"
:param steps: list of steps
:returns: latex string ready to be endbeded
"""
ans = joining.join([
name + " & "
+ sep + " & "
+ str(s) + end for s in steps
])
return ans
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
# encoding: utf-8
import jinja2
__all__ = [
"texenv",
]
# Definition of jinja syntax for latex
texenv = jinja2.Environment(
block_start_string=r"\Block{",
block_end_string="}",
variable_start_string=r"\Var{",
variable_end_string="}",
comment_start_string=r"\#{",
comment_end_string="}",
line_statement_prefix="%-",
line_comment_prefix="%#",
loader=jinja2.ChoiceLoader(
[
jinja2.FileSystemLoader(["./"]),
]
),
extensions=["jinja2.ext.do"],
)
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

View File

@ -1,38 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
from random import randint
def pythagore_triplet(v_min = 1, v_max = 10):
"""Random pythagore triplet generator
:param v_min: minimum in randint
:param v_max: max in randint
:returns: (a,b,c) such that a^2 + b^2 = c^2
"""
u = randint(v_min,v_max)
v = randint(v_min,v_max)
while v == u:
v = randint(v_min,v_max)
u, v = max(u,v), min(u,v)
return (u**2-v**2 , 2*u*v, u**2 + v**2)
if __name__ == '__main__':
print(pythagore_triplet())
for j in range(1,10):
for i in range(j,10):
print((i**2-j**2 , 2*i*j, i**2 + j**2))
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

View File

@ -1,33 +0,0 @@
\Block{macro solveEquation(P)}
On commence par calculer le discriminant de $P(x) = \Var{P}$.
\begin{eqnarray*}
\Delta & = & b^2-4ac \\
\Var{P.delta.explain()|calculus(name="\\Delta")}
\end{eqnarray*}
\Block{if P.delta > 0}
comme $\Delta = \Var{P.delta} > 0$ donc $P$ a deux racines
\begin{eqnarray*}
x_1 & = & \frac{-b - \sqrt{\Delta}}{2a} = \frac{\Var{-P.b} - \sqrt{\Var{P.delta}}}{2 \times \Var{P.a}} = \Var{P.roots()[0] } \\
x_2 & = & \frac{-b + \sqrt{\Delta}}{2a} = \frac{\Var{-P.b} + \sqrt{\Var{P.delta}}}{2 \times \Var{P.a}} = \Var{P.roots()[1] }
\end{eqnarray*}
Les solutions de l'équation $\Var{P} = 0$ sont donc $\mathcal{S} = \left\{ \Var{P.roots()[0]}; \Var{P.roots()[1]} \right\}$
\Block{elif P.delta == 0}
Comme $\Delta = 0$ donc $P$ a une racine
\begin{eqnarray*}
x_1 = \frac{-b}{2a} = \frac{-\Var{P.b}}{2\times \Var{P.a}} = \Var{P.roots()[0]} \\
\end{eqnarray*}
La solution de $\Var{P} = 0$ est donc $\mathcal{S} = \left\{ \Var{P.roots()[0]}\right\}$
\Block{else}
Alors $\Delta = \Var{P.delta} < 0$ donc $P$ n'a pas de racine donc l'équation $\Var{P} = 0$ n'a pas de solution.
\Block{endif}
\Block{endmacro}

36
bopytex/message.py Normal file
View File

@ -0,0 +1,36 @@
class Message():
def __init__(self, status, out, err):
self._status = status
self._out = out
self._err = err
@property
def status(self):
return self._status
@property
def out(self):
return self._out
@property
def err(self):
return self._err
def __repr__(self):
return f"Message(status={self.status}, out={self.out}, err={self.err})"
class SubprocessMessage(Message):
def __init__(self, process):
self._process = process
@property
def status(self):
return self._process.wait()
@property
def out(self):
return self._process.stdout
@property
def err(self):
return self._process.stderr

View File

View File

@ -0,0 +1,58 @@
from bopytex.tasks import Task, activate_corr_on, compile_pdf, join_pdfs
import bopytex.planner.naming as naming
import os
def list_files(dir=".", accept=lambda _: True, reject=lambda _: False):
files = []
for file in os.listdir(dir):
if accept(file) and not reject(file):
files.append(file)
return files
def planner(options: dict) -> list[Task]:
sources = list_files(
accept=lambda x: x.endswith(".tex"),
reject=lambda x: x.startswith("tpl_"),
)
options["sources"] = sources
return tasks_builder(options)
def tasks_builder(
options: dict,
) -> list[Task]:
opt = {
"no_join": False,
"no_pdf": False,
}
opt.update(options)
try:
sources = opt["sources"]
no_join = opt["no_join"]
no_pdf = opt["no_pdf"]
except KeyError:
raise PlannerMissingOption("An option is missing")
tasks = []
corr_pdfs = []
for source in sources:
corr_source = naming.corr(source)
tasks.append(activate_corr_on(source, opt, corr_source))
if not no_pdf:
corr_pdf = naming.source2pdf(corr_source)
tasks.append(compile_pdf(corr_source, corr_pdf))
corr_pdfs.append(corr_pdf)
if not no_join:
joined = "joined.pdf"
if corr_pdfs:
corr_joined = naming.corr(joined)
tasks.append(join_pdfs(corr_pdfs, corr_joined))
return tasks

View File

@ -0,0 +1,2 @@
class PlannerMissingOption(Exception):
pass

View File

@ -0,0 +1,9 @@
from bopytex.tasks import Task
def simple(options: dict) -> list[Task]:
"""Simple planner with options['quantity'] tasks and no dependencies"""
return [
Task("DO", args={"number": i}, deps=[], output=f"{i}")
for i in range(options["quantity"])
]

View File

@ -0,0 +1,103 @@
from bopytex.tasks import Task, activate_corr_on, compile_pdf, generate, join_pdfs
import bopytex.planner.naming as naming
from bopytex.planner.exceptions import PlannerMissingOption
import csv
def build_subject_list_from_infos(infos: list[dict]) -> list[dict]:
subjects = []
digit = len(str(len(infos)))
for i, infos in enumerate(infos):
subjects.append({"number": str(i + 1).zfill(digit), **infos})
return subjects
def build_subject_list_from_qty(qty: int) -> list[dict]:
subjects = []
digit = len(str(qty))
for i in range(qty):
subjects.append({"number": str(i + 1).zfill(digit)})
return subjects
def planner(options: dict) -> list[Task]:
try:
students_csv = options["students_csv"]
assert options["students_csv"] != ""
except (KeyError, AssertionError):
try:
quantity_subjects = options["quantity_subjects"]
assert options["quantity_subjects"] != 0
except (KeyError, AssertionError):
raise PlannerMissingOption("students_csv or quantity_subjects is required")
else:
options["subjects"] = build_subject_list_from_qty(qty=quantity_subjects)
else:
with open(students_csv, "r") as csv_file:
reader = csv.DictReader(csv_file)
infos = [r for r in reader]
options["subjects"] = build_subject_list_from_infos(infos)
return tasks_builder(options)
def tasks_builder(
options: dict,
) -> list[Task]:
opt = {
"corr": False,
"no_join": False,
"no_pdf": False,
}
opt.update(options)
try:
template = opt["template"]
subjects = opt["subjects"]
corr = opt["corr"]
no_join = opt["no_join"]
no_pdf = opt["no_pdf"]
except KeyError:
raise PlannerMissingOption("An option is missing")
tasks = []
pdfs = []
corr_pdfs = []
for subject in subjects:
source = naming.template2source(template, subject)
args = {
"subject": subject,
"options": options
}
tasks.append(generate(template, args, source))
if not no_pdf:
pdf = naming.source2pdf(source)
tasks.append(compile_pdf(source, pdf))
pdfs.append(pdf)
if corr:
corr_source = naming.corr(source)
tasks.append(activate_corr_on(source, opt, corr_source))
if not no_pdf:
corr_pdf = naming.source2pdf(corr_source)
tasks.append(compile_pdf(corr_source, corr_pdf))
corr_pdfs.append(corr_pdf)
if not no_join:
joined = naming.join(template)
if pdfs:
tasks.append(join_pdfs(pdfs, joined))
if corr_pdfs:
corr_joined = naming.corr(joined)
tasks.append(join_pdfs(corr_pdfs, corr_joined))
return tasks

15
bopytex/planner/naming.py Normal file
View File

@ -0,0 +1,15 @@
def template2source(template: str, metadatas: dict):
return metadatas["number"] + template[3:]
def corr(source):
return "corr_" + source
def source2pdf(source):
return source[:-4] + ".pdf"
def join(template):
return source2pdf("joined" + template[3:])

78
bopytex/scheduler.py Normal file
View File

@ -0,0 +1,78 @@
""" Scheduler for action to make """
from bopytex.tasks import Task
from bopytex.worker import Dispatcher
class Scheduler:
"""Scheduler is responsible of getting tasks (the tasks) and yield those that can be done"""
def __init__(self, dispatcher:Dispatcher, output_done: list[str] = None):
self._dispatcher = dispatcher
if output_done is None:
self._output_done = []
else:
self._output_done = output_done
self._tasks = []
self._failed_tasks = []
@property
def tasks(self) -> list[Task]:
"""List all the tasks todo"""
return self._tasks
@property
def doable_tasks(self) -> list[Task]:
"""List all doable tasks"""
return [
task
for task in self.tasks
if not task.deps or all([d in self.output_done for d in task.deps])
]
@property
def all_deps(self) -> set[str]:
"""List dependencies of all tasks"""
return {d for task in self.tasks for d in task.deps}
@property
def all_output(self) -> set[str]:
"""List ouput of all tasks"""
return {task.output for task in self.tasks}
@property
def output_done(self) -> list[str]:
return self._output_done
@property
def failed_tasks(self) -> list[Task]:
return self._failed_tasks
def append(self, tasks: list[Task]):
self._tasks += tasks
def is_finishable(self):
return self.all_deps.issubset(self.all_output)
def next_task(self):
try:
task = self.doable_tasks[0]
except IndexError:
raise StopIteration
self._tasks.remove(task)
message = self._dispatcher(task)
if message.status == 0:
self._output_done.append(task.output)
else:
self._failed_tasks.append(task)
return message
def backlog(self):
""" Yield tasks sorted according to dependencies """
while self.doable_tasks:
yield self.next_task()

View File

@ -3,20 +3,30 @@
import click import click
from .bopytex import produce_and_compile import logging
from bopytex.service import main
formatter = logging.Formatter("%(name)s :: %(levelname)s :: %(message)s")
steam_handler = logging.StreamHandler()
steam_handler.setLevel(logging.DEBUG)
steam_handler.setFormatter(formatter)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(steam_handler)
@click.command() @click.command()
@click.argument( @click.argument(
"template", "template",
type=click.Path(exists=True), type=click.Path(exists=True),
nargs=1, nargs=1,
# help="File with the template. The name should have the following form tpl_... .",
) )
@click.option( @click.option(
"-w", "-w",
"--working-dir", "--working-dir",
default=".",
type=click.Path(exists=True), type=click.Path(exists=True),
help="Where fed templates and compiled files will be placed",
) )
@click.option( @click.option(
"-s", "-s",
@ -25,9 +35,6 @@ from .bopytex import produce_and_compile
default="", default="",
help="CSV containing list of students names", help="CSV containing list of students names",
) )
@click.option(
"-d", "--dirty", is_flag=True, default=False, help="Do not clean after compilation",
)
@click.option( @click.option(
"-n", "-n",
"--no-compile", "--no-compile",
@ -36,11 +43,18 @@ from .bopytex import produce_and_compile
help="Do not compile source code", help="Do not compile source code",
) )
@click.option( @click.option(
"-N", "-d",
"--number_subjects", "--dirty",
is_flag=True,
default=False,
help="Do not clean after compilation",
)
@click.option(
"-q",
"--quantity_subjects",
type=int, type=int,
default=1, default=1,
help="The number of subjects to make", help="The quantity of subjects to make",
) )
@click.option( @click.option(
"-j", "-j",
@ -54,29 +68,31 @@ from .bopytex import produce_and_compile
"--only-corr", "--only-corr",
is_flag=True, is_flag=True,
default=False, default=False,
help="Create and compile only correction from existing subjects", help="Activate correction and compile only from existing subjects",
) )
@click.option( @click.option(
"-c", "-C",
"--corr", "--corr",
is_flag=True, is_flag=True,
default=False, default=False,
help="Create and compile correction while making subjects", help="Create and compile correction while making subjects",
) )
@click.option( @click.option(
"-C", "-c",
"--crazy", "--configfile",
is_flag=True, type=str,
default=False, default="bopyptex_config.py",
help="Crazy mode. Tries and tries again until template feeding success!", help="Config file path",
) )
def new(**options): def new(**options):
""" Bopytex for message in main(**options):
try:
Feed the template (tpl_...) and then compile it with latex. assert message.status == 0
except AssertionError:
""" logger.warning(message)
produce_and_compile(options) break
else:
logger.info(message.out)
if __name__ == "__main__": if __name__ == "__main__":

99
bopytex/service.py Executable file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env python
# encoding: utf-8
"""
Producing then compiling templates
"""
import importlib.util
import os
from pathlib import Path
from bopytex.scheduler import Scheduler
from bopytex import default_config
def orcherstrator(
options: dict,
planner,
dispatcher,
):
tasks = planner(options)
scheduler = Scheduler(dispatcher, [options["template"]])
scheduler.append(tasks)
for message in scheduler.backlog():
yield message
def load_module(modulefile: str):
spec = importlib.util.spec_from_file_location("module.name", modulefile)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def clean_vars_keys(
vars: dict,
keys: list[str] = [
"__name__",
"__doc__",
"__package__",
"__loader__",
"__spec__",
"__file__",
"__cached__",
"__builtins__",
],
) -> dict:
new_dict = vars.copy()
for k in keys:
del new_dict[k]
return new_dict
def config_from_file(filename: str) -> dict:
if Path(filename).exists():
local_config = vars(load_module(filename))
return clean_vars_keys(local_config)
else:
return {}
def build_config(options: dict) -> dict:
"""Look for options["configfile"] to load it with default_config and options"""
config = clean_vars_keys(vars(default_config))
configfile = ""
try:
configfile = options["configfile"]
except KeyError:
pass
try:
configfile = os.environ["BOPYTEXCONFIG"]
except KeyError:
pass
if configfile:
local_config = config_from_file(configfile)
config.update(local_config)
config.update(options)
return config
def main(**options):
config = build_config(options)
orcherstre = orcherstrator(
config, planner=default_config.planner, dispatcher=default_config.dispatcher
)
for message in orcherstre:
yield message
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

64
bopytex/tasks.py Normal file
View File

@ -0,0 +1,64 @@
""" Produce tasks to do
It essentially place things at the right place.
"""
from dataclasses import dataclass
@dataclass
class Task:
action: str
args: dict
deps: list
output: str
def generate(template: str, meta: dict, output: str):
"""Create a task to generate a subject"""
return Task(
action="GENERATE",
args=meta,
deps=[template],
output=output,
)
def activate_corr_on(src: str, meta:dict, output: str):
"""Create a task to activate correction for src"""
return Task(
action="ACTIVATE_CORR",
args=meta,
deps=[src],
output=output,
)
def compile_pdf(src: str, output: str):
"""Create a task to compile src"""
return Task(
action="COMPILE",
args={},
deps=[src],
output=output,
)
def join_pdfs(pdfs: list, output: str):
"""Create task to join pdf together"""
return Task(
action="JOIN",
args={},
deps=pdfs,
output=output,
)
def clean(files: list):
"""Create task to clean files"""
return Task(
action="CLEAN",
args={},
deps=files,
output=None,
)

View File

@ -0,0 +1,18 @@
class ActionNotFound(Exception):
pass
class Dispatcher:
def __init__(self, actions: list):
self._actions = actions
def __call__(self, task):
try:
choosen_action = self._actions[task.action]
except KeyError:
raise ActionNotFound(
f"The action {task.action} is not in {self._actions.keys()}"
)
return choosen_action(args=task.args, deps=task.deps, output=task.output)

View File

@ -0,0 +1,14 @@
from bopytex.message import Message
def activate_corr(args, deps, output):
no_solution = args["latex"]["no_solution"]
solution = args["latex"]["solution"]
with open(deps[0], "r") as input_f:
with open(output, "w") as output_f:
for line in input_f.readlines():
output_f.write(line.replace(no_solution, solution))
return Message(0, [f"ACTIVATE CORR - {deps[0]} to {output}"], [])

2
bopytex/worker/clean.py Normal file
View File

@ -0,0 +1,2 @@
def clean(args: dict, deps, output):
pass

25
bopytex/worker/compile.py Normal file
View File

@ -0,0 +1,25 @@
import subprocess
from bopytex.message import Message
from ..message import SubprocessMessage
def curstomtex(command: str, options: str):
def func(args: dict, deps, output) -> Message:
compile_process = subprocess.Popen(
[command, options, deps[0]],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return Message(
compile_process.wait(),
list(compile_process.stdout),
list(compile_process.stderr),
)
return func
latexmk = curstomtex("latexmk", "-f")
pdflatex = curstomtex("pdflatex", "--interaction=nonstopmode")

View File

@ -0,0 +1,32 @@
from jinja2.environment import Template
from bopytex.message import Message
def generate(args, deps, output):
env = args["options"]["jinja2"]["environment"]
template = env.get_template(deps[0])
variables = {
"options":args["options"],
"subject":args["subject"],
}
try:
args["options"]["direct_access"]
except KeyError:
pass
else:
for (k,v) in args["options"]["direct_access"].items():
if k not in ["options", "subject"]:
variables[k] = v
try:
with open(output, "w") as out:
fed = template.render(variables)
out.write(fed)
return Message(0, [f"GENERATE - {deps[0]} to {output}"], [])
except Exception as e:
return Message(1, [], [e])

View File

@ -0,0 +1,33 @@
import subprocess
from bopytex.message import Message, SubprocessMessage
def pdfjam(args: dict, deps, output):
joining_process = subprocess.Popen(
["pdfjam"] + deps + ["-o", output],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return Message(
joining_process.wait(),
list(joining_process.stdout),
list(joining_process.stderr),
)
def gs(args: dict, deps, output):
""" Not working. The command works in terminal but not here """
joining_process = subprocess.Popen(
["gs", f"-dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile={output}"]
+ deps,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return Message(
joining_process.wait(),
list(joining_process.stdout),
list(joining_process.stderr),
)

Binary file not shown.

View File

@ -0,0 +1,30 @@
\documentclass[12pt]{article}
\title{Bopytex example -- \Var{ subject.number }}
\begin{document}
\maketitle
%- set a = 10
%- set n = 2
We have two variables
\begin{itemize}
\item a: \Var{a}
\item n: \Var{n}
\end{itemize}
%# We can use blocks
\begin{itemize}
%- for i in range(n)
\item \Var{a}
%- endfor
\end{itemize}
\section{Variables}
\subsection{subject}
\Var{subject}
\subsection{options}
\Var{options}
\end{document}

View File

@ -0,0 +1,4 @@
from mapytex import Expression
direct_access = {
"Expression": Expression
}

Binary file not shown.

View File

@ -0,0 +1,6 @@
"Name","Age","Email","fraction level","calculus level"
"Spike Tucker","22","s.tucker@randatmail.com","7","3"
"Martin Payne","21","m.payne@randatmail.com","7","3"
"Kimberly Baker","20","k.baker@randatmail.com","1","8"
"Emma Bailey","29","e.bailey@randatmail.com","2","5"
"Nicholas Taylor","28","n.taylor@randatmail.com","3","3"
1 Name Age Email fraction level calculus level
2 Spike Tucker 22 s.tucker@randatmail.com 7 3
3 Martin Payne 21 m.payne@randatmail.com 7 3
4 Kimberly Baker 20 k.baker@randatmail.com 1 8
5 Emma Bailey 29 e.bailey@randatmail.com 2 5
6 Nicholas Taylor 28 n.taylor@randatmail.com 3 3

View File

@ -0,0 +1,22 @@
\documentclass[12pt]{article}
\title{Bopytex with Mapytex example -- \Var{ subject.number }}
% solution/print = false
\begin{document}
\maketitle
%- set e = Expression.random("{a} + {b}")
\Var{e}
\Var{e.simplify()}
\section{Variables}
\subsection{subject}
\Var{subject}
\subsection{options}
\Var{options}
\end{document}

View File

@ -1,18 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\begin{document}
\section{Ajouts de fractions}
Adding two fractions
\[
A = \frac{- 2}{4} + \frac{7}{8}
\]
Solution
\[
\frac{- 2}{4} + \frac{7}{8}=\frac{- 2 \times 2}{4 \times 2} + \frac{7}{8}=\frac{- 4}{8} + \frac{7}{8}=\frac{- 4 + 7}{8}=\frac{3}{8}
\]
\end{document}

View File

@ -1,18 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\begin{document}
\section{Ajouts de fractions}
Adding two fractions
\[
A = \frac{8}{9} + \frac{3}{63}
\]
Solution
\[
\frac{8}{9} + \frac{3}{63}=\frac{8 \times 7}{9 \times 7} + \frac{3}{63}=\frac{56}{63} + \frac{3}{63}=\frac{56 + 3}{63}=\frac{59}{63}
\]
\end{document}

Binary file not shown.

View File

@ -1,19 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\begin{document}
\section{Ajouts de fractions}
Adding two fractions
%- set e = Expression.random("{a} / {b} + {c} / {k*b}", ["b > 1", "k>1"])
\[
A = \Var{e}
\]
Solution
\[
\Var{e.simplify().explain() | join('=')}
\]
\end{document}

View File

@ -1,33 +0,0 @@
alabaster==0.7.10
appdirs==1.4.3
Babel==2.4.0
decorator==4.0.11
docutils==0.13.1
imagesize==0.7.1
ipython==5.3.0
ipython-genutils==0.2.0
Jinja2==2.9.6
location==0.0.7
MarkupSafe==1.0
mpmath==0.19
packaging==16.8
pexpect==4.2.1
pickleshare==0.7.4
prompt-toolkit==1.0.14
ptyprocess==0.5.1
Pygments==2.2.0
pyparsing==2.2.0
## !! Could not determine repository location
pytz==2017.2
requests==2.13.0
simplegeneric==0.8.1
six==1.10.0
snowballstemmer==1.2.1
Sphinx==1.5.5
sphinx-rtd-theme==0.2.4
sympy==1.0
traitlets==4.3.2
wcwidth==0.1.7
mapytex==2.0.8
mypytex==0.2
path.py==12.1

View File

@ -4,20 +4,18 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='Bopytex', name="Bopytex",
version='0.1.1', version="0.9",
description='Command line tool for compiling latex with python command embedded', description="Command line tool for compiling latex with python command embedded",
author='Benjamin Bertrand', author="Benjamin Bertrand",
author_email='programming@opytex.org', author_email="benjamin.bertrand@opytex.org",
packages=['bopytex'], packages=["bopytex"],
install_requires=[ install_requires=[
'mapytex', "click",
'mypytex', "jinja2",
], ],
entry_points={ entry_points={"console_scripts": ["bopytex=bopytex.script:new"]},
"console_scripts": ['bopytex=bopytex.script:new'] )
},
)
# ----------------------------- # -----------------------------
# Reglages pour 'vim' # Reglages pour 'vim'

View File

@ -1,133 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[francais]{babel}
\usepackage[T1]{fontenc}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{amsfonts}
\title{
Snippets pour Opytex \\
Fractions
}
\author{
Benjamin Bertrand
}
\begin{document}
\maketitle
\section{Simplifications de fractions}
\begin{itemize}
\item Trouver le numérateur quand le dénominateur augmente
%
\begin{align*}
\dfrac{2}{6} = \dfrac{\ldots}{48}
\end{align*}
Solution
\begin{align*}
\dfrac{2}{6} = \dfrac{16}{48}
\end{align*}
\item Trouver le numérateur quand le dénominateur diminue
%
\begin{align*}
\dfrac{12}{9} = \dfrac{\cdots}{3}
\end{align*}
Solution
\begin{align*}
\dfrac{12}{9} = \dfrac{4}{3}
\end{align*}
Explications
\begin{align*}
\frac{ 12 }{ 9 }=\frac{ 4 \times 3 }{ 3 \times 3 }=\frac{ 4 }{ 3 }
\end{align*}
\end{itemize}
\section{Ajouts de fractions}
\begin{itemize}
\item Fraction avec le même dénominateur
\begin{align*}
A = \frac{ 1 }{ 4 } + \frac{ 5 }{ 4 }
\end{align*}
Solution
\begin{align*}
\frac{ 1 }{ 4 } + \frac{ 5 }{ 4 }=\frac{ 1 + 5 }{ 4 }=\frac{ 6 }{ 4 }=\frac{ 3 \times 2 }{ 2 \times 2 }=\frac{ 3 }{ 2 }
\end{align*}
\item Fraction avec un denominateur multiple de l'autre
\begin{align*}
A = \frac{ 10 }{ 7 } + \frac{ 3 }{ 49 }
\end{align*}
Solution
\begin{align*}
\frac{ 10 }{ 7 } + \frac{ 3 }{ 49 }=\frac{ 10 \times 7 }{ 7 \times 7 } + \frac{ 3 \times 1 }{ 49 \times 1 }=\frac{ 70 }{ 49 } + \frac{ 3 }{ 49 }=\frac{ 70 + 3 }{ 49 }=\frac{ 73 }{ 49 }
\end{align*}
\item Fraction avec des dénominateurs premiers entre eux
\begin{align*}
A = \frac{ 10 }{ 3 } + \frac{ 4 }{ 2 }
\end{align*}
Solution
\begin{align*}
\frac{ 10 }{ 3 } + \frac{ 4 }{ 2 }=\frac{ 10 \times 2 }{ 3 \times 2 } + \frac{ 4 \times 3 }{ 2 \times 3 }=\frac{ 20 }{ 6 } + \frac{ 12 }{ 6 }=\frac{ 20 + 12 }{ 6 }=\frac{ 32 }{ 6 }=\frac{ 16 \times 2 }{ 3 \times 2 }=\frac{ 16 }{ 3 }
\end{align*}
\item Une fraction et un entier
\begin{align*}
A = \frac{ 6 }{ 8 } + 9
\end{align*}
Solution
\begin{align*}
\frac{ 6 }{ 8 } + 9=\frac{ 6 \times 1 }{ 8 \times 1 } + \frac{ 9 \times 8 }{ 1 \times 8 }=\frac{ 6 }{ 8 } + \frac{ 72 }{ 8 }=\frac{ 6 + 72 }{ 8 }=\frac{ 78 }{ 8 }=\frac{ 39 \times 2 }{ 4 \times 2 }=\frac{ 39 }{ 4 }
\end{align*}
\item Une fraction et un entier
\begin{align*}
A = 2 + \frac{ 8 }{ 2 }
\end{align*}
Solution
\begin{align*}
2 + \frac{ 8 }{ 2 }=\frac{ 2 \times 2 }{ 1 \times 2 } + \frac{ 8 \times 1 }{ 2 \times 1 }=\frac{ 4 }{ 2 } + \frac{ 8 }{ 2 }=\frac{ 4 + 8 }{ 2 }=6
\end{align*}
\end{itemize}
\section{Multiplications de fractions}
\begin{itemize}
\item Une fraction et un entier
\begin{align*}
A = 5 \times \frac{ 7 }{ 8 }
\end{align*}
Solution
\begin{align*}
5 \times \frac{ 7 }{ 8 }=\frac{ 7 }{ 8 } \times 5=\frac{ 7 \times 5 }{ 8 }=\frac{ 35 }{ 8 }
\end{align*}
\item Fraction avec des dénominateurs quelconques
\begin{align*}
A = \frac{ 5 }{ 10 } \times \frac{ 4 }{ 7 }
\end{align*}
Solution
\begin{align*}
\frac{ 5 }{ 10 } \times \frac{ 4 }{ 7 }=\frac{ 4 }{ 7 } \times \frac{ 5 }{ 10 }=\frac{ 2 \times 2 \times 5 }{ 7 \times 5 \times 2 }=\frac{ 4 \times 5 }{ 7 \times 10 }=\frac{ 20 }{ 70 }=\frac{ 2 \times 10 }{ 7 \times 10 }=\frac{ 2 }{ 7 }
\end{align*}
\end{itemize}
\end{document}

Binary file not shown.

View File

@ -1,121 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[french]{babel}
\usepackage[T1]{fontenc}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{graphicx}
\title{%
Snippets pour Opytex \\
Pythagore et Thalès
}
\author{%
Benjamin Bertrand
}
\begin{document}
\maketitle
\section{Pythagore}
\section{Thalès}
\section{Mélange des 2}
\subsection{Longueur du parcours}
% exo de geometrie comme au brevet blanc.
%- set AD, AC, DC = random_pythagore()
%- set tourACDA = AC+AD+DC
%- set AE, AF = round(tourACDA/2*random(), 1), round(tourACDA/2*random(), 1)
%- set EF = round(tourACDA - AE - AF - randint(20,40)*0.2, 1)
%- set tourAEFA = round(AE+EF+AF, 1)
%- set rapport = randint(2,5)
%- set AE1, AF1, EF1 = round(AE/rapport,2) , round(AF/rapport,2), round(EF/rapport,2)
%- set objectif = randint(floor(tourAEFA), tourACDA)
%- if objectif > 100
%- set unit = "m"
%- else
%- set unit = "km"
%- endif
Une commune souhaite aménager des parcours de santé sur son territoire. On fait deux propositions au conseil municipale, schématisés ci-dessous:
\begin{itemize}
\item Le parcours ACDA
\item Le parcours AEFA
\end{itemize}
Ils souhaitent faire un parcours dont la longueur s'approche le plus possible de \Var{objectif}\Var{unit}.
Peux-tu les aider à choisir le parcours? Justifie
\textbf{Attention: La figure proposée au conseil municipale n'est pas à l'échelle, mais les codages et les dimension données sont correctes.}
\begin{minipage}{0.6\textwidth}
\includegraphics[scale = 0.4]{./fig/parcours}
\end{minipage}
\begin{minipage}{0.4\textwidth}
\begin{itemize}
\item $AC = \Var{AC}\Var{unit}$
\item $CD = \Var{DC}\Var{unit}$
\item $AE' = \Var{AE1}\Var{unit}$
\item $AE = \Var{AE}\Var{unit}$
\item $AF = \Var{AF}\Var{unit}$
\item $E'F' = \Var{EF1}\Var{unit}$
\item $(E'F') // (EF)$
\item L'angle $\widehat{EAF}$ vaut $30^o$
\end{itemize}
\end{minipage}
\begin{solution}
\begin{itemize}
\item Parcours ACDA:
D'après la figure, on voit que le triangle $ACD$ est rectangle en $C$ donc d'après le théorème de Pythagore, on a
\begin{align*}
AD^2 &= AC^2 + DC^2 \\
AD^2 &= \Var{AC}^2 + \Var{DC}^2 \\
AD^2 &= \Var{AC**2} + \Var{DC**2} \\
AD^2 &= \Var{AC**2 + DC**2} \\
AD &= \sqrt{\Var{AC**2 + DC**2}} = \Var{AD}\Var{unit}
\end{align*}
Donc le parcours ACDA mesure
\begin{align*}
AD + AC + CD = \Var{AD} + \Var{AC} + \Var{DC} = \Var{tourACDA}\Var{unit}
\end{align*}
\item Parcours AEFA:
D'après les données, on sait que $(EF) // (E'F')$. On voit aussi que $A$, $E'$ et $E$ sont alignés. Il en est de même pour les points $A$, $F'$ et $F$. Donc d'après le théorème de Thalès
\begin{tabular}{|c|c|c|c|}
\hline
Triangle AEF & AE = \Var{AE} & AF = \Var{AF} & EF \\
\hline
Triangle AE'F' & AE' = \Var{AE1} & AF' & E'F' = \Var{EF1} \\
\hline
\end{tabular}
est un tableau de proportionnalité. Donc on peut faire un produit en croix pour calcul $EF$.
\begin{align*}
EF = \frac{E'F' \times AE}{AE'} = \frac{\Var{EF1} \times \Var{AE}}{\Var{AE1}} = \Var{EF} \Var{unit}
\end{align*}
Donc le parcours AEFA mesure
\begin{align*}
AF + AE + EF = \Var{AF} + \Var{AE} + \Var{EF} = \Var{tourAEFA}\Var{unit}
\end{align*}
\item Choix du parcours:
%- if abs(tourACDA - objectif) < abs(tourAEFA - objectif)
Il faudra choisir le tour $ACDA$ car sa longueur est plus proche de \Var{objectif}\Var{unit}.
%- else
Il faudra choisir le tour $AFEA$ car sa longueur est plus proche de \Var{objectif}\Var{unit}.
%- endif
\end{itemize}
\end{solution}
\end{document}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,96 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[francais]{babel}
\usepackage[T1]{fontenc}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{amsfonts}
\title{
Snippets pour Opytex \\
Fonctions
}
\author{
Benjamin Bertrand
}
\begin{document}
\maketitle
\section{Calculer des images}
\begin{enumerate}
%-set f = Expression.random("{a}*x^2 + {b}*x + {c}")
\item $\forall x \in \mathbb{R} \qquad f(x) = \Var{f}$
Solution:
\begin{align*}
f(0) &= \Var{f(0).explain() | join('=')} \\
f(1) &= \Var{f(1).explain() | join('=')} \\
f(2) &= \Var{f(2).explain() | join('=')} \\
f({10}) &= \Var{f(10).explain() | join('=')} \\
f({100}) &= \Var{f(100).explain() | join('=')}
\end{align*}
\end{enumerate}
\section{Résolution d'équation du 2nd degré}
%- macro solveEquation(P)
On commence par calculer le discriminant de $P(x) = \Var{P}$.
\begin{eqnarray*}
\Delta & = & b^2-4ac \\
\Var{P.delta.explain()|calculus(name="\\Delta")}
\end{eqnarray*}
\Block{if P.delta > 0}
comme $\Delta = \Var{P.delta} > 0$ donc $P$ a deux racines
\begin{eqnarray*}
x_1 & = & \frac{-b - \sqrt{\Delta}}{2a} = \frac{\Var{-P.b} - \sqrt{\Var{P.delta}}}{2 \times \Var{P.a}} = \Var{P.roots[0] } \\
x_2 & = & \frac{-b + \sqrt{\Delta}}{2a} = \frac{\Var{-P.b} + \sqrt{\Var{P.delta}}}{2 \times \Var{P.a}} = \Var{P.roots[1] }
\end{eqnarray*}
Les solutions de l'équation $\Var{P} = 0$ sont donc $\mathcal{S} = \left\{ \Var{P.roots[0]}; \Var{P.roots[1]} \right\}$
\Block{elif P.delta == 0}
Comme $\Delta = 0$ donc $P$ a une racine
\begin{eqnarray*}
x_1 = \frac{-b}{2a} = \frac{-\Var{P.b}}{2\times \Var{P.a}} = \Var{P.roots[0]} \\
\end{eqnarray*}
La solution de $\Var{P} = 0$ est donc $\mathcal{S} = \left\{ \Var{P.roots[0]}\right\}$
\Block{else}
Alors $\Delta = \Var{P.delta} < 0$ donc $P$ n'a pas de racine donc l'équation $\Var{P} = 0$ n'a pas de solution.
\Block{endif}
%- endmacro
\begin{enumerate}
%-set P = Expression.random("{a}*x^2 + {b}*x + {c}", ["b**2-4*a*c>0"])
\item Étude du polynôme $P$, $\forall x \in \mathbb{R} \quad P(x) = \Var{P}$
Solution:
\Var{solveEquation(P)}
%-set P = Expression.random("{a}*x^2 + {b}*x + {c}", ["b**2-4*a*c==0"])
\item Étude du polynôme $P$, $\forall x \in \mathbb{R} \quad P(x) = \Var{P}$
Solution:
\Var{solveEquation(P)}
%-set P = Expression.random("{a}*x^2 + {b}*x + {c}", ["b**2-4*a*c<0"])
\item Étude du polynôme $P$, $\forall x \in \mathbb{R} \quad P(x) = \Var{P}$
Solution:
\Var{solveEquation(P)}
\end{enumerate}
\end{document}

View File

@ -1,133 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[francais]{babel}
\usepackage[T1]{fontenc}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{amsfonts}
\title{
Snippets pour Opytex \\
Fractions
}
\author{
Benjamin Bertrand
}
\begin{document}
\maketitle
\section{Simplifications de fractions}
\begin{itemize}
\item Trouver le numérateur quand le dénominateur augmente
\Block{set a,b,ans,c = random_str("{a},{b},{a*c},{b*c}", conditions = ["{a} != {b}"], val_min = 2, val_max = 10).split(',')}%
\begin{align*}
\dfrac{\Var{a}}{\Var{b}} = \dfrac{\ldots}{\Var{c}}
\end{align*}
Solution
\begin{align*}
\dfrac{\Var{a}}{\Var{b}} = \dfrac{\Var{ans}}{\Var{c}}
\end{align*}
\item Trouver le numérateur quand le dénominateur diminue
\Block{set a,b,ans,c = random_str("{a*c},{b*c},{a},{b}", conditions = ["{a} != {b}"], val_min = 2, val_max = 10).split(',')}%
\begin{align*}
\dfrac{\Var{a}}{\Var{b}} = \dfrac{\cdots}{\Var{c}}
\end{align*}
Solution
\begin{align*}
\dfrac{\Var{a}}{\Var{b}} = \dfrac{\Var{ans}}{\Var{c}}
\end{align*}
Explications
\Block{set f = Expression(a + "/" +b)}
\begin{align*}
\Var{f.simplify().explain()|join('=')}
\end{align*}
\end{itemize}
\section{Ajouts de fractions}
\begin{itemize}
\item Fraction avec le même dénominateur
\Block{set e = Expression.random("{a} / {b} + {c} / {b}", ["{b} > 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\item Fraction avec un denominateur multiple de l'autre
\Block{set e = Expression.random("{a} / {b} + {c} / {b*d}", ["{b} > 1","{d} > 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\item Fraction avec des dénominateurs premiers entre eux
\Block{set e = Expression.random("{a} / {b} + {c} / {d}", ["{b} > 1","{d} > 1", "gcd({b},{d}) == 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\item Une fraction et un entier
\Block{set e = Expression.random("{a} / {b} + {c}", ["{b} > 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\item Une fraction et un entier
\Block{set e = Expression.random("{c} + {a} / {b}", ["{b} > 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\end{itemize}
\section{Multiplications de fractions}
\begin{itemize}
\item Une fraction et un entier
\Block{set e = Expression.random("{c} * {a} / {b}", ["{b} > 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\item Fraction avec des dénominateurs quelconques
\Block{set e = Expression.random("{a} / {b} * {c} / {d}", ["{b} > 1","{d} > 1"], val_min = 1)}
\begin{align*}
A = \Var{e}
\end{align*}
Solution
\begin{align*}
\Var{e.simplify().explain() | join('=')}
\end{align*}
\end{itemize}
\end{document}

View File

@ -1,87 +0,0 @@
% vim:ft=tex:
%
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[francais]{babel}
\usepackage[T1]{fontenc}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{amsfonts}
\title{
Snippets pour Opytex \\
Suites
}
\author{
Benjamin Bertrand
}
\begin{document}
\maketitle
\section{Calculs de termes}
\begin{enumerate}
\item Calculer les termes $u_0$, $u_1$, $u_2$, $u_{10}$ et $u_{100}$ pour les suites suivantes
\begin{enumerate}
%-set u = Expression.random("{a}*n+{b}")
\item $\forall n \in \mathbb{N} \qquad u_n = \Var{u}$
Solution:
\begin{align*}
u_0 &= \Var{u(0).explain() | join('=')} \\
u_1 &= \Var{u(1).explain() | join('=')} \\
u_2 &= \Var{u(2).explain() | join('=')} \\
u_{10} &= \Var{u(10).explain() | join('=')} \\
u_{100} &= \Var{u(100).explain() | join('=')}
\end{align*}
%-set v = Expression.random("({a}*n+{b})/{c}", ["c>1"])
\item $\forall n \in \mathbb{N} \qquad v_n = \Var{v|replace("frac","dfrac")}$
Solution:
\begin{align*}
v_0 &= \Var{v(0).explain() | join('=')} \\
v_1 &= \Var{v(1).explain() | join('=')} \\
v_2 &= \Var{v(2).explain() | join('=')} \\
v_{10} &= \Var{v(10).explain() | join('=')} \\
v_{100} &= \Var{v(100).explain() | join('=')}
\end{align*}
%-set v = Expression.random("({a}*n+{b})/{c}", ["c>1"])
\item $\forall n \in \mathbb{N} \qquad v_n = \Var{v}$
Solution:
\begin{align*}
%- for j in [0, 1, 2, 10, 100]
v_{\Var{j}} &= \Var{v(j).explain() | join('=')} \\
%- endfor
\end{align*}
%-set f = Expression.random("{a}*x")
%-set v0 = randint(0, 10)
\item $\forall n \in \mathbb{N} \qquad v_{n+1} = \Var{f("v_n")} \mbox{ et } v_0 = \Var{v0}$
Solution:
\begin{align*}
v_0 &= \Var{v0} \\
%-set v = f(v0)
v_1 &= \Var{v.explain() | join('=')} \\
%-set v = f(v)
v_2 &= \Var{v.explain() | join('=')} \\
\end{align*}
Pour le terme 10, il faut calculer tous les autres avant!
\begin{align*}
%#- Trick to move around scoping rules
%#- https://stackoverflow.com/a/49699589
%- set v = namespace(val = v)
%- for i in range(8)
%- set v.val = f(v.val)
v_{\Var{i+3}} &= \Var{v.val.explain() | join('=')} \\
%- endfor
\end{align*}
\end{enumerate}
\end{enumerate}
\end{document}

0
test/__init__.py Normal file
View File

0
test/fakes/__init__.py Normal file
View File

10
test/fakes/dispatcher.py Normal file
View File

@ -0,0 +1,10 @@
from bopytex.worker import Dispatcher
from .workers import fake_worker, success_worker, fail_worker
fake_dispatcher = Dispatcher(
{
"FAKE": fake_worker,
"SUCCESS": success_worker,
"FAILURE": fail_worker,
}
)

9
test/fakes/planner.py Normal file
View File

@ -0,0 +1,9 @@
from bopytex.tasks import Task
def simple(options: dict) -> list[Task]:
"""Simple planner with options['quantity'] tasks and no dependencies"""
return [
Task("FAKE", args={"number": i}, deps=[], output=f"{i}")
for i in range(options["quantity"])
]

13
test/fakes/workers.py Normal file
View File

@ -0,0 +1,13 @@
from bopytex.message import Message
def fake_worker(args, deps, output):
return Message(0, [f"FAKE - {args} - {deps} - {output}"], [])
def success_worker(args, deps, output):
return Message(0, [f"SUCCESS - {args} - {deps} - {output}"], [])
def fail_worker(args, deps, output):
return Message(1, [f"FAILURE - {args} - {deps} - {output}"], [])

View File

@ -1,6 +0,0 @@
nom,classe,elo
Bob,1ST,1000
Pipo,1ST,1300
Popi,1ST,100
Boule,1ST,4000
Bill,1ST,1300
1 nom classe elo
2 Bob 1ST 1000
3 Pipo 1ST 1300
4 Popi 1ST 100
5 Boule 1ST 4000
6 Bill 1ST 1300

View File

@ -1,22 +0,0 @@
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[francais]{babel}
\usepackage[T1]{fontenc}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{amsfonts}
\title{
Tests
}
\author{
Benjamin Bertrand
}
\begin{document}
\maketitle
\end{document}

View File

@ -1,212 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
import pytest
import os
from pathlib import Path
from shutil import copyfile
from bopytex.bopytex import produce_and_compile, subject_metadatas
SNIPPETS_PATH = Path("snippets/")
TEST_PATH = Path("test")
TEST_TEMPLATE_PATH = TEST_PATH / "templates/"
@pytest.fixture
def prepare_test_template(tmp_path):
""" Create a tmp directory, copy snippets inside
return tmp directory name
"""
tmp = tmp_path
snippets = TEST_TEMPLATE_PATH.glob("tpl_*.tex")
for s in snippets:
copyfile(s, tmp / s.name)
csvs = TEST_PATH.glob("*.csv")
for s in csvs:
copyfile(s, tmp / s.name)
prev_dir = Path.cwd()
os.chdir(tmp)
yield tmp
os.chdir(prev_dir)
@pytest.fixture
def prepare_snippets(tmp_path):
""" Create a tmp directory, copy snippets inside
return tmp directory name
"""
tmp = tmp_path
snippets = SNIPPETS_PATH.glob("tpl_*.tex")
for s in snippets:
copyfile(s, tmp / s.name)
prev_dir = Path.cwd()
os.chdir(tmp)
yield tmp
os.chdir(prev_dir)
def test_produce_and_compile_base(prepare_test_template):
test_tpl = list(Path(".").glob("tpl_*.tex"))
assert [tpl.name for tpl in test_tpl] == ["tpl_test.tex"]
for tpl in test_tpl:
produce_and_compile(
{
"template": tpl,
"working_dir": None,
"only_corr": False,
"students_csv": None,
"number_subjects": 1,
"dirty": False,
"no_compile": False,
"no_join": False,
"corr": False,
"crazy": False,
}
)
def test_produce_and_compile_csv(prepare_test_template):
test_tpl = Path(".").glob("tpl_*.tex")
for tpl in test_tpl:
options = {
"template": tpl,
"working_dir": None,
"only_corr": False,
"students_csv": "students.csv",
"number_subjects": 1,
"dirty": False,
"no_compile": False,
"no_join": False,
"corr": False,
"crazy": False,
}
# produce_and_compile(options)
def test_metadatas(prepare_test_template):
test_tpl = Path(".").glob("tpl_*.tex")
for tpl in test_tpl:
options = {
"template": tpl,
"working_dir": None,
"only_corr": False,
"students_csv": "students.csv",
"number_subjects": 1,
"dirty": False,
"no_compile": False,
"no_join": False,
"corr": False,
"crazy": False,
}
metadatas = subject_metadatas(options)
meta = [
{
"num": "01",
"nom": "Bob",
"classe": "1ST",
"elo": "1000",
"texfile": "01_test.tex",
"template": "tpl_test.tex",
"directory": ".",
},
{
"num": "02",
"nom": "Pipo",
"classe": "1ST",
"elo": "1300",
"texfile": "02_test.tex",
"template": "tpl_test.tex",
"directory": ".",
},
{
"num": "03",
"nom": "Popi",
"classe": "1ST",
"elo": "100",
"texfile": "03_test.tex",
"template": "tpl_test.tex",
"directory": ".",
},
{
"num": "04",
"nom": "Boule",
"classe": "1ST",
"elo": "4000",
"texfile": "04_test.tex",
"template": "tpl_test.tex",
"directory": ".",
},
{
"num": "05",
"nom": "Bill",
"classe": "1ST",
"elo": "1300",
"texfile": "05_test.tex",
"template": "tpl_test.tex",
"directory": ".",
},
]
assert metadatas == meta
def test_pdfjoin_current_directory(prepare_test_template):
wdir = prepare_test_template
pass
def test_pdfjoin_deep_directory():
pass
def test_pdfjoin_dont_remove():
pass
def test_subject_names():
pass
def test_feed_texfiles():
pass
def test_tex2pdf_current_directory():
pass
def test_tex2pdf_deep_directory():
pass
def test_activate_solution():
pass
# def test_snippets(prepare_snippets):
# snippets = list(Path(".").glob("tpl_*.tex"))
# for tpl in snippets:
# produce_and_compile(
# {
# "template": tpl,
# "working_dir": None,
# "only_corr": False,
# "students_csv": None,
# "number_subjects": 1,
# "dirty": False,
# "no_compile": False,
# "no_join": False,
# "corr": False,
# "crazy": False,
# }
# )
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

85
test/test_e2e.py Normal file
View File

@ -0,0 +1,85 @@
import os
import jinja2
from pathlib import Path
from bopytex.service import main
import pytest
@pytest.fixture
def template_path(tmp_path):
template = tmp_path / "tpl_source.tex"
with open(template, "w") as tpl:
tpl.write(
"""
\\documentclass{article}
\\begin{document}
First document.
Subject {{ number }}
\\end{document}
"""
)
return template
@pytest.fixture
def bad_template_path(tmp_path):
template = tmp_path / "tpl_source.tex"
with open(template, "w") as tpl:
tpl.write(
"""
\\documentclass{article}
\\begin{document}
First document.
Subject {{ number }}
"""
)
return template
@pytest.fixture
def jinja2_env(tmp_path):
templateEnv = jinja2.Environment(loader=jinja2.FileSystemLoader(tmp_path))
return templateEnv
def test_with_default_planner(template_path, jinja2_env, tmp_path):
os.chdir(tmp_path)
options = {
"template": str(template_path.name),
"quantity_subjects": 3,
"corr": False,
"no_join": False,
"no_pdf": False,
"jinja2": {
"environment": jinja2_env,
},
}
for message in main(**options):
assert message.status == 0
assert Path("joined_source.pdf").exists()
def test_with_default_planner_bad_template(bad_template_path, jinja2_env, tmp_path):
os.chdir(tmp_path)
options = {
"template": str(bad_template_path.name),
"quantity_subjects": 3,
"corr": False,
"no_join": False,
"no_pdf": False,
"jinja2": {
"environment": jinja2_env,
},
}
for message in main(**options):
pass
assert not Path("joined_source.pdf").exists()

382
test/test_planner.py Normal file
View File

@ -0,0 +1,382 @@
from bopytex.planner.generate_compile_join_planner import tasks_builder as gcj_task_builder
from bopytex.planner.activate_corr_compile_join_planner import tasks_builder as accj_task_builder
from bopytex.tasks import Task
def test_tasks_builder_generate():
tasks = gcj_task_builder(
options={
"template": "tpl_source.tex",
"subjects": [{"number": "01"}, {"number": "02"}],
"no_pdf": True,
}
)
assert tasks == [
Task(
action="GENERATE",
args={
"options": {
"no_pdf": True,
"subjects": [{"number": "01"}, {"number": "02"}],
'template': 'tpl_source.tex',
},
"subject": {"number": "01"}
},
deps=["tpl_source.tex"],
output="01_source.tex",
),
Task(
action="GENERATE",
args={
"options": {
"no_pdf": True,
"subjects": [{"number": "01"}, {"number": "02"}],
'template': 'tpl_source.tex',
},
"subject": {"number": "02"}
},
deps=["tpl_source.tex"],
output="02_source.tex",
),
]
def test_tasks_builder_generate_compile():
tasks = gcj_task_builder(
options={
"template": "tpl_source.tex",
"subjects": [{"number": "01"}, {"number": "02"}],
"no_join": True,
}
)
assert tasks == [
Task(
action="GENERATE",
args={
"options": {
"no_join": True,
"subjects": [{"number": "01"}, {"number": "02"}],
'template': 'tpl_source.tex',
},
"subject": {"number": "01"}
},
deps=["tpl_source.tex"],
output="01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["01_source.tex"],
output="01_source.pdf",
),
Task(
action="GENERATE",
args={
"options": {
"no_join": True,
"subjects": [{"number": "01"}, {"number": "02"}],
'template': 'tpl_source.tex',
},
"subject": {"number": "02"}
},
deps=["tpl_source.tex"],
output="02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["02_source.tex"],
output="02_source.pdf",
),
]
def test_tasks_builder_generate_compile_join():
tasks = gcj_task_builder(
options={
"template": "tpl_source.tex",
"subjects": [{"number": "01"}, {"number": "02"}],
}
)
assert tasks == [
Task(
action="GENERATE",
args={
"options": {
"subjects": [{"number": "01"}, {"number": "02"}],
'template': 'tpl_source.tex',
},
"subject": {"number": "01"}
},
deps=["tpl_source.tex"],
output="01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["01_source.tex"],
output="01_source.pdf",
),
Task(
action="GENERATE",
args={
"options": {
"subjects": [{"number": "01"}, {"number": "02"}],
'template': 'tpl_source.tex',
},
"subject": {"number": "02"}
},
deps=["tpl_source.tex"],
output="02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["02_source.tex"],
output="02_source.pdf",
),
Task(
action="JOIN",
args={},
deps=["01_source.pdf", "02_source.pdf"],
output="joined_source.pdf",
),
]
def test_tasks_builder_generate_compile_corr():
tasks = gcj_task_builder(
options={
"template": "tpl_source.tex",
"subjects": [{"number": "01"}, {"number": "02"}],
"corr": True,
"no_join": True,
}
)
assert tasks == [
Task(
action="GENERATE",
args={
"options": {
"subjects": [{"number": "01"}, {"number": "02"}],
"corr": True,
"no_join": True,
'template': 'tpl_source.tex',
},
"subject": {"number": "01"}
},
deps=["tpl_source.tex"],
output="01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["01_source.tex"],
output="01_source.pdf",
),
Task(
action="ACTIVATE_CORR",
args={
'corr': True,
'no_join': True,
'no_pdf': False,
'template': 'tpl_source.tex',
"subjects": [{'number': '01'}, {'number': '02'}]
},
deps=["01_source.tex"],
output="corr_01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["corr_01_source.tex"],
output="corr_01_source.pdf",
),
Task(
action="GENERATE",
args={
"options": {
"subjects": [{"number": "01"}, {"number": "02"}],
"corr": True,
"no_join": True,
'template': 'tpl_source.tex',
},
"subject": {"number": "02"}
},
deps=["tpl_source.tex"],
output="02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["02_source.tex"],
output="02_source.pdf",
),
Task(
action="ACTIVATE_CORR",
args={
'corr': True,
'no_join': True,
'no_pdf': False,
'template': 'tpl_source.tex',
"subjects": [{'number': '01'}, {'number': '02'}]
},
deps=["02_source.tex"],
output="corr_02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["corr_02_source.tex"],
output="corr_02_source.pdf",
),
]
def test_tasks_builder_generate_compile_corr_joined():
tasks = gcj_task_builder(
options={
"template": "tpl_source.tex",
"subjects": [{"number": "01"}, {"number": "02"}],
"corr": True,
"no_join": False,
}
)
assert tasks == [
Task(
action="GENERATE",
args={
"options": {
"subjects": [{"number": "01"}, {"number": "02"}],
"corr": True,
"no_join": False,
'template': 'tpl_source.tex',
},
"subject": {"number": "01"}
},
deps=["tpl_source.tex"],
output="01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["01_source.tex"],
output="01_source.pdf",
),
Task(
action="ACTIVATE_CORR",
args={
'corr': True,
'no_join': False,
'no_pdf': False,
'template': 'tpl_source.tex',
"subjects": [{'number': '01'}, {'number': '02'}]
},
deps=["01_source.tex"],
output="corr_01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["corr_01_source.tex"],
output="corr_01_source.pdf",
),
Task(
action="GENERATE",
args={
"options": {
"subjects": [{"number": "01"}, {"number": "02"}],
"corr": True,
"no_join": False,
'template': 'tpl_source.tex',
},
"subject": {"number": "02"}
},
deps=["tpl_source.tex"],
output="02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["02_source.tex"],
output="02_source.pdf",
),
Task(
action="ACTIVATE_CORR",
args={
'corr': True,
'no_join': False,
'no_pdf': False,
'template': 'tpl_source.tex',
"subjects": [{'number': '01'}, {'number': '02'}]
},
deps=["02_source.tex"],
output="corr_02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["corr_02_source.tex"],
output="corr_02_source.pdf",
),
Task(
action="JOIN",
args={},
deps=["01_source.pdf", "02_source.pdf"],
output="joined_source.pdf",
),
Task(
action="JOIN",
args={},
deps=["corr_01_source.pdf", "corr_02_source.pdf"],
output="corr_joined_source.pdf",
),
]
def test_only_corr_tasks_builder():
tasks = accj_task_builder(
options={
"sources": ["01_source.tex", "02_source.tex"],
}
)
assert tasks == [
Task(
action="ACTIVATE_CORR",
args={
'no_join': False,
'no_pdf': False,
'sources': ['01_source.tex', '02_source.tex']
},
deps=["01_source.tex"],
output="corr_01_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["corr_01_source.tex"],
output="corr_01_source.pdf",
),
Task(
action="ACTIVATE_CORR",
args={
'no_join': False,
'no_pdf': False,
'sources': ['01_source.tex', '02_source.tex']
},
deps=["02_source.tex"],
output="corr_02_source.tex",
),
Task(
action="COMPILE",
args={},
deps=["corr_02_source.tex"],
output="corr_02_source.pdf",
),
Task(
action="JOIN",
args={},
deps=["corr_01_source.pdf", "corr_02_source.pdf"],
output="corr_joined.pdf",
),
]

161
test/test_scheduler.py Normal file
View File

@ -0,0 +1,161 @@
from bopytex.message import Message
from bopytex.tasks import Task
from bopytex.scheduler import Scheduler
from .fakes.dispatcher import fake_dispatcher
import pytest
def test_schedule_append():
scheduler = Scheduler(dispatcher=fake_dispatcher)
tasks = [
Task(action="FAKE", args={}, deps=["dep1", "dep2"], output="end1"),
Task(action="FAKE", args={}, deps=["dep1", "dep3"], output="end2"),
]
scheduler.append(tasks)
assert scheduler.tasks == tasks
assert scheduler.all_deps == {"dep1", "dep2", "dep3"}
assert scheduler.all_output == {"end1", "end2"}
def test_schedule_one_task():
scheduler = Scheduler(dispatcher=fake_dispatcher)
tasks = [Task(action="FAKE", args={}, deps=[], output="end")]
scheduler.append(tasks)
assert scheduler.doable_tasks == tasks
result = scheduler.next_task()
assert result.status == 0
assert result.out == ["FAKE - {} - [] - end"]
assert result.err == []
assert scheduler.tasks == []
assert scheduler.output_done == ["end"]
def test_schedule_one_task_with_args():
scheduler = Scheduler(dispatcher=fake_dispatcher)
tasks = [Task(action="FAKE", args={"task": "one"}, deps=[], output="one")]
scheduler.append(tasks)
result = scheduler.next_task()
assert result.status == 0
assert result.out == ["FAKE - {'task': 'one'} - [] - one"]
assert result.err == []
assert scheduler.tasks == []
assert scheduler.output_done == ["one"]
def test_schedule_multiple_tasks():
scheduler = Scheduler(dispatcher=fake_dispatcher)
t1 = Task(action="FAKE", args={"task": "one"}, deps=[], output="one")
t2 = Task(action="FAKE", args={"task": "two"}, deps=[], output="two")
t3 = Task(action="FAKE", args={"task": "three"}, deps=[], output="three")
scheduler.append([t1, t2, t3])
assert scheduler.doable_tasks == [t1, t2, t3]
assert scheduler.is_finishable()
result = scheduler.next_task()
assert result.status == 0
assert scheduler.tasks == [t2, t3]
assert scheduler.output_done == ["one"]
result = scheduler.next_task()
assert result.status == 0
assert scheduler.tasks == [t3]
assert scheduler.output_done == ["one", "two"]
result = scheduler.next_task()
assert result.status == 0
assert scheduler.tasks == []
assert scheduler.output_done == ["one", "two", "three"]
def test_schedule_multiple_tasks_with_dependencies():
scheduler = Scheduler(dispatcher=fake_dispatcher)
t1 = Task(action="FAKE", args={"task": "one"}, deps=["three"], output="one")
t2 = Task(action="FAKE", args={"task": "two"}, deps=["one"], output="two")
t3 = Task(action="FAKE", args={"task": "three"}, deps=[], output="three")
scheduler.append([t1, t2, t3])
assert scheduler.doable_tasks == [t3]
assert scheduler.is_finishable()
result = scheduler.next_task()
assert result.status == 0
assert scheduler.tasks == [t1, t2]
assert scheduler.output_done == ["three"]
assert scheduler.doable_tasks == [t1]
result = scheduler.next_task()
assert result.status == 0
assert scheduler.tasks == [t2]
assert scheduler.output_done == ["three", "one"]
assert scheduler.doable_tasks == [t2]
result = scheduler.next_task()
assert result.status == 0
assert scheduler.tasks == []
assert scheduler.output_done == ["three", "one", "two"]
def test_schedule_multiple_tasks_with_dependencies_loop():
scheduler = Scheduler(dispatcher=fake_dispatcher)
t1 = Task(action="FAKE", args={"task": "one"}, deps=["three"], output="one")
t2 = Task(action="FAKE", args={"task": "two"}, deps=["one"], output="two")
t3 = Task(action="FAKE", args={"task": "three"}, deps=[], output="three")
scheduler.append([t1, t2, t3])
for task in scheduler.backlog():
pass
assert scheduler.output_done == ["three", "one", "two"]
def test_schedule_empty_task():
scheduler = Scheduler(dispatcher=fake_dispatcher)
scheduler.append([])
with pytest.raises(StopIteration):
scheduler.next_task()
def test_schedule_multiple_tasks_with_undoable_dependencies():
scheduler = Scheduler(dispatcher=fake_dispatcher)
t1 = Task(action="FAKE", args={"task": "one"}, deps=["three"], output="one")
t2 = Task(action="FAKE", args={"task": "two"}, deps=[], output="two")
scheduler.append([t1, t2])
assert scheduler.doable_tasks == [t2]
assert not scheduler.is_finishable()
for _ in scheduler.backlog():
pass
assert scheduler.tasks == [t1]
assert scheduler.output_done == ["two"]
assert scheduler.doable_tasks == []
def test_schedule_multiple_tasks_with_failling_tasks():
scheduler = Scheduler(dispatcher=fake_dispatcher)
t1 = Task(action="FAILURE", args={"task": "one"}, deps=["three"], output="one")
t2 = Task(action="FAKE", args={"task": "two"}, deps=["one"], output="two")
t3 = Task(action="FAKE", args={"task": "three"}, deps=[], output="three")
t4 = Task(action="FAILURE", args={"task": "four"}, deps=[], output="four")
scheduler.append([t1, t2, t3, t4])
assert scheduler.doable_tasks == [t3, t4]
assert scheduler.is_finishable()
status = []
for message in scheduler.backlog():
status.append(message.status)
assert status == [0, 1, 1]
assert scheduler.tasks == [t2]
assert scheduler.failed_tasks == [t1, t4]
assert scheduler.output_done == ["three"]
assert scheduler.doable_tasks == []

60
test/test_service.py Normal file
View File

@ -0,0 +1,60 @@
import os
import pytest
from bopytex.planner import fake_planner
from bopytex.service import build_config, orcherstrator
from bopytex.tasks import Task
from bopytex.worker import Dispatcher
from .fakes.workers import fake_worker
def test_service():
options = {"quantity": 3, "template": "tpl_src.tex"}
dispatcher = Dispatcher(actions={"DO": fake_worker})
service = orcherstrator(options, fake_planner.simple, dispatcher)
for i, message in enumerate(service):
assert message.status == 0
assert message.out == [f"FAKE - {{'number': {i}}} - [] - {i}"]
def test_get_config_no_configfile():
pass
@pytest.fixture
def config_file(tmp_path):
config_file = tmp_path / "bopytex_config.py"
with open(config_file, "w") as f:
f.write(
"""
from bopytex.jinja2_env.texenv import texenv
jinja2 = {
"environment": texenv,
}
"""
)
return config_file
def test_get_config_with_configfile(config_file, tmp_path):
os.chdir(tmp_path)
config = build_config({"from_option": "config", "configfile": str(config_file)})
assert type(config) == dict
assert set(config.keys()) == {
"generate",
"configfile",
"Dispatcher",
"planner",
"pdflatex",
"clean",
"latex",
"activate_corr",
"dispatcher",
"pdfjam",
"jinja2",
"texenv",
"from_option",
}
assert list(config["jinja2"].keys()) == ["environment"]

41
test/test_tasks.py Normal file
View File

@ -0,0 +1,41 @@
from bopytex.tasks import activate_corr_on, clean, compile_pdf, generate, join_pdfs
def test_build_task_generate():
template = "tpl_source.tex"
task = generate(template, {"subject": "01"}, output="source.tex")
assert task.action == "GENERATE"
assert task.args == {"subject": "01"}
assert task.deps == [template]
def test_build_task_activate_corr_on():
src = "source.tex"
task = activate_corr_on(src, meta={}, output="corr_source.tex")
assert task.action == "ACTIVATE_CORR"
assert task.args == {}
assert task.deps == [src]
def test_build_task_compile():
src = "source.tex"
task = compile_pdf(src, output="source.pdf")
assert task.action == "COMPILE"
assert task.args == {}
assert task.deps == [src]
def test_build_task_join():
pdfs = [f"{i}_source.pdf" for i in range(3)]
task = join_pdfs(pdfs, output="joined.pdf")
assert task.action == "JOIN"
assert task.args == {}
assert task.deps == pdfs
def test_build_task_clean():
files = ["source.aux", "source.log"]
task = clean(files)
assert task.action == "CLEAN"
assert task.args == {}
assert task.deps == files

40
test/test_texenv.py Normal file
View File

@ -0,0 +1,40 @@
from bopytex.jinja2_env.texenv import texenv
def test_variable_block():
base_template = r"\Var{a}"
jinja2_template = texenv.from_string(base_template)
output = jinja2_template.render(a=2)
assert output == "2"
def test_block_string():
base_template = r"\Block{set a = 2}\Var{a}"
jinja2_template = texenv.from_string(base_template)
output = jinja2_template.render()
assert output == "2"
def test_block_line_statement():
base_template = r"""%-set a = 2
\Var{a}"""
jinja2_template = texenv.from_string(base_template)
output = jinja2_template.render()
assert output == "2"
def test_block_line_statement_with_comment():
base_template = r"""%-set a = 2
%# comment
\Var{a}"""
jinja2_template = texenv.from_string(base_template)
output = jinja2_template.render()
assert output == "\n2"
def test_add_filter():
texenv.filters["count_caracters"] = lambda x: len(x)
base_template = r"""\Var{a} has \Var{a | count_caracters}"""
jinja2_template = texenv.from_string(base_template)
output = jinja2_template.render(a="coucou")
assert output == "coucou has 6"

BIN
test/worker/source.pdf Normal file

Binary file not shown.

View File

@ -0,0 +1,35 @@
import os
from pathlib import Path
from bopytex.worker.compile import latexmk
import pytest
@pytest.fixture
def tex_path(tmp_path):
source = tmp_path / "source.tex"
with open(source, "w") as src:
src.write(
"""
\\documentclass{article}
\\begin{document}
First document. This is a simple example, with no
extra parameters or packages included.
\\end{document}
"""
)
return source
def test_latexmk(tex_path, tmp_path):
#tmp_path = tex_path.parent
os.chdir(tmp_path)
texfile = str(tex_path.name)
output = "source.pdf"
message = latexmk({}, [texfile], "source.pdf")
assert message.status == 0
assert Path(output).exists

View File

@ -0,0 +1,86 @@
import os
import jinja2
from bopytex.worker.generate import generate
import pytest
@pytest.fixture
def jinja2_env(tmp_path):
templateEnv = jinja2.Environment(loader=jinja2.FileSystemLoader(tmp_path))
return templateEnv
@pytest.fixture
def template_path(tmp_path):
template = tmp_path / "template.j2"
with open(template, "w") as tpl:
tpl.write("Plop {{ a }}")
return template
def test_generate(template_path, jinja2_env):
tmp_path = template_path.parent
os.chdir(tmp_path)
assert template_path.exists
template = str(template_path.name)
output = "output"
message = generate(
args={
"options": {
"direct_access": {"a": 2},
"jinja2": {"environment": jinja2_env},
},
"subject": {},
},
deps=[template],
output=output,
)
print(message.err)
assert message.status == 0
assert message.out == ["GENERATE - template.j2 to output"]
with open(output, "r") as out:
lines = out.readlines()
assert lines == ["Plop 2"]
@pytest.fixture
def template_path_with_random(tmp_path):
template = tmp_path / "template.j2"
with open(template, "w") as tpl:
tpl.write("Plop {{ random.randint(0, 10) }}")
return template
def test_generate_with_random(template_path_with_random, jinja2_env):
tmp_path = template_path_with_random.parent
os.chdir(tmp_path)
assert template_path_with_random.exists
template = str(template_path_with_random.name)
output = "output"
import random
message = generate(
args={
"options": {
"jinja2": {"environment": jinja2_env},
"direct_access": {
"random": random,
}
},
"subject":{},
},
deps=[template],
output=output,
)
print(message.err)
assert message.status == 0
with open(output, "r") as out:
lines = out.readlines()
assert int(lines[0][-1]) >= 0
assert int(lines[0][-1]) < 10

View File

@ -0,0 +1,50 @@
import os
import shutil
from pathlib import Path
from bopytex.worker.join_pdf import pdfjam
import pytest
@pytest.fixture
def multiple_pdf(tmp_path, request):
this_file = Path(request.module.__file__)
source = this_file.parent / "source.pdf"
assert source.exists()
qty = 3
dests = []
for i in range(qty):
dest = tmp_path / f"source_{i}.pdf"
shutil.copyfile(source, dest)
assert dest.exists()
dests.append(dest)
return dests
def test_join_pdf(multiple_pdf):
tmp_path = multiple_pdf[0].parent
os.chdir(tmp_path)
deps = [str(d.name) for d in multiple_pdf]
output = "joined.pdf"
message = pdfjam({"pwd": Path.cwd()}, deps, output)
assert message.status == 0
assert Path(output).exists()
def test_join_pdf_failed(multiple_pdf, tmp_path):
os.chdir(tmp_path)
deps = [str(d.name) for d in multiple_pdf] + ["doesnotexists.pdf"]
output = "joined.pdf"
message = pdfjam({"pwd": Path.cwd()}, deps, output)
assert message.status == 66
assert not Path(output).exists()