diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..80ec6ad --- /dev/null +++ b/.drone.yml @@ -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 diff --git a/.gitignore b/.gitignore index 09b9778..0f1edb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -tests/ __pycache__/ *.pyc documentation/build/ diff --git a/Dockerfile.simple b/Dockerfile.simple new file mode 100644 index 0000000..343f5a8 --- /dev/null +++ b/Dockerfile.simple @@ -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 diff --git a/Dockerfile.usecase b/Dockerfile.usecase new file mode 100644 index 0000000..172514a --- /dev/null +++ b/Dockerfile.usecase @@ -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" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a3f0e24 --- /dev/null +++ b/Makefile @@ -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" + diff --git a/README.md b/README.md index 33f233d..f9fd100 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # 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 @@ -10,84 +12,91 @@ Install and update using [pip](https://pip.pypa.io/en/stable/quickstart/) ## 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 1", "k>1"]) -\[ - A = \Var{e} -\] -Solution -\[ - \Var{e.simplify().explain() | join('=')} -\] -\end{document} -``` +%- set e = Expression.random("{a} + {b}") +\Var{e} -Generate latex files and compile those for 2 different subjects. - -``` -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} -\] +\Var{e.simplify()} \end{document} ``` -- `02_add_fractions.tex` +Information about my students are stored in a csv file (here `students.csv`) -```latex -\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} +``` csv +"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" ``` -And a ready to print pdf. - -- [ all_add_fraction.pdf ]( ./examples/all_add_fraction.pdf ) - - - - +Then I can produce a subject for each of my student +``` bash +$ bopytex tpl_simple.tex -s students.csv -c bopytex_config.py +``` diff --git a/bopytex/__init__.py b/bopytex/__init__.py index 1306988..acfc5bd 100644 --- a/bopytex/__init__.py +++ b/bopytex/__init__.py @@ -1,10 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 - -#from .bopytex import subject_metadatas, crazy_feed, pdfjoin - - # ----------------------------- # Reglages pour 'vim' # vim:set autoindent expandtab tabstop=4 shiftwidth=4: diff --git a/bopytex/bopytex.py b/bopytex/bopytex.py old mode 100755 new mode 100644 index d5380ce..7941a66 --- a/bopytex/bopytex.py +++ b/bopytex/bopytex.py @@ -1,237 +1,2 @@ -#!/usr/bin/env python -# encoding: utf-8 - -""" -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 +import bopytex.default_config as DEFAULT +from bopytex.service import orcherstrator diff --git a/bopytex/default_config.py b/bopytex/default_config.py new file mode 100644 index 0000000..a4444d5 --- /dev/null +++ b/bopytex/default_config.py @@ -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", + } + diff --git a/bopytex/filters.py b/bopytex/filters.py deleted file mode 100644 index 395794e..0000000 --- a/bopytex/filters.py +++ /dev/null @@ -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 diff --git a/bopytex/jinja2_env/texenv.py b/bopytex/jinja2_env/texenv.py new file mode 100644 index 0000000..d1fb124 --- /dev/null +++ b/bopytex/jinja2_env/texenv.py @@ -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 diff --git a/bopytex/lib/pythagore.py b/bopytex/lib/pythagore.py deleted file mode 100644 index 9eede1b..0000000 --- a/bopytex/lib/pythagore.py +++ /dev/null @@ -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 diff --git a/bopytex/macros/poly2Deg.tex b/bopytex/macros/poly2Deg.tex deleted file mode 100644 index 5c667e6..0000000 --- a/bopytex/macros/poly2Deg.tex +++ /dev/null @@ -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} diff --git a/bopytex/message.py b/bopytex/message.py new file mode 100644 index 0000000..0c2a54f --- /dev/null +++ b/bopytex/message.py @@ -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 diff --git a/bopytex/planner/__init__.py b/bopytex/planner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bopytex/planner/activate_corr_compile_join_planner.py b/bopytex/planner/activate_corr_compile_join_planner.py new file mode 100644 index 0000000..3a6ffe6 --- /dev/null +++ b/bopytex/planner/activate_corr_compile_join_planner.py @@ -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 diff --git a/bopytex/planner/exceptions.py b/bopytex/planner/exceptions.py new file mode 100644 index 0000000..65c7a29 --- /dev/null +++ b/bopytex/planner/exceptions.py @@ -0,0 +1,2 @@ +class PlannerMissingOption(Exception): + pass diff --git a/bopytex/planner/fake_planner.py b/bopytex/planner/fake_planner.py new file mode 100644 index 0000000..6617992 --- /dev/null +++ b/bopytex/planner/fake_planner.py @@ -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"]) + ] diff --git a/bopytex/planner/generate_compile_join_planner.py b/bopytex/planner/generate_compile_join_planner.py new file mode 100644 index 0000000..41a15ff --- /dev/null +++ b/bopytex/planner/generate_compile_join_planner.py @@ -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 diff --git a/bopytex/planner/naming.py b/bopytex/planner/naming.py new file mode 100644 index 0000000..bf022b4 --- /dev/null +++ b/bopytex/planner/naming.py @@ -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:]) + diff --git a/bopytex/scheduler.py b/bopytex/scheduler.py new file mode 100644 index 0000000..4c46ade --- /dev/null +++ b/bopytex/scheduler.py @@ -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() diff --git a/bopytex/script.py b/bopytex/script.py index bcec04c..15706a2 100644 --- a/bopytex/script.py +++ b/bopytex/script.py @@ -3,20 +3,30 @@ 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.argument( "template", type=click.Path(exists=True), nargs=1, - # help="File with the template. The name should have the following form tpl_... .", ) @click.option( "-w", "--working-dir", + default=".", type=click.Path(exists=True), - help="Where fed templates and compiled files will be placed", ) @click.option( "-s", @@ -25,9 +35,6 @@ from .bopytex import produce_and_compile default="", help="CSV containing list of students names", ) -@click.option( - "-d", "--dirty", is_flag=True, default=False, help="Do not clean after compilation", -) @click.option( "-n", "--no-compile", @@ -36,11 +43,18 @@ from .bopytex import produce_and_compile help="Do not compile source code", ) @click.option( - "-N", - "--number_subjects", + "-d", + "--dirty", + is_flag=True, + default=False, + help="Do not clean after compilation", +) +@click.option( + "-q", + "--quantity_subjects", type=int, default=1, - help="The number of subjects to make", + help="The quantity of subjects to make", ) @click.option( "-j", @@ -54,29 +68,31 @@ from .bopytex import produce_and_compile "--only-corr", is_flag=True, default=False, - help="Create and compile only correction from existing subjects", + help="Activate correction and compile only from existing subjects", ) @click.option( - "-c", + "-C", "--corr", is_flag=True, default=False, help="Create and compile correction while making subjects", ) @click.option( - "-C", - "--crazy", - is_flag=True, - default=False, - help="Crazy mode. Tries and tries again until template feeding success!", + "-c", + "--configfile", + type=str, + default="bopyptex_config.py", + help="Config file path", ) def new(**options): - """ Bopytex - - Feed the template (tpl_...) and then compile it with latex. - - """ - produce_and_compile(options) + for message in main(**options): + try: + assert message.status == 0 + except AssertionError: + logger.warning(message) + break + else: + logger.info(message.out) if __name__ == "__main__": diff --git a/bopytex/service.py b/bopytex/service.py new file mode 100755 index 0000000..5cc8421 --- /dev/null +++ b/bopytex/service.py @@ -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 diff --git a/bopytex/tasks.py b/bopytex/tasks.py new file mode 100644 index 0000000..d0f3287 --- /dev/null +++ b/bopytex/tasks.py @@ -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, + ) diff --git a/bopytex/worker/__init__.py b/bopytex/worker/__init__.py new file mode 100644 index 0000000..cf084dd --- /dev/null +++ b/bopytex/worker/__init__.py @@ -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) + diff --git a/bopytex/worker/activate_corr.py b/bopytex/worker/activate_corr.py new file mode 100644 index 0000000..3f300a2 --- /dev/null +++ b/bopytex/worker/activate_corr.py @@ -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}"], []) + diff --git a/bopytex/worker/clean.py b/bopytex/worker/clean.py new file mode 100644 index 0000000..593acaa --- /dev/null +++ b/bopytex/worker/clean.py @@ -0,0 +1,2 @@ +def clean(args: dict, deps, output): + pass diff --git a/bopytex/worker/compile.py b/bopytex/worker/compile.py new file mode 100644 index 0000000..5813337 --- /dev/null +++ b/bopytex/worker/compile.py @@ -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") diff --git a/bopytex/worker/generate.py b/bopytex/worker/generate.py new file mode 100644 index 0000000..8a84ae4 --- /dev/null +++ b/bopytex/worker/generate.py @@ -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]) diff --git a/bopytex/worker/join_pdf.py b/bopytex/worker/join_pdf.py new file mode 100644 index 0000000..a89155c --- /dev/null +++ b/bopytex/worker/join_pdf.py @@ -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), + ) diff --git a/example/simple/joined_example.pdf b/example/simple/joined_example.pdf new file mode 100644 index 0000000..a1c4cac Binary files /dev/null and b/example/simple/joined_example.pdf differ diff --git a/example/simple/tpl_example.tex b/example/simple/tpl_example.tex new file mode 100644 index 0000000..c02be2c --- /dev/null +++ b/example/simple/tpl_example.tex @@ -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} diff --git a/example/usecase/bopytex_config.py b/example/usecase/bopytex_config.py new file mode 100644 index 0000000..d8dd2b9 --- /dev/null +++ b/example/usecase/bopytex_config.py @@ -0,0 +1,4 @@ +from mapytex import Expression +direct_access = { + "Expression": Expression + } diff --git a/example/usecase/joined_example.pdf b/example/usecase/joined_example.pdf new file mode 100644 index 0000000..fb2fd5d Binary files /dev/null and b/example/usecase/joined_example.pdf differ diff --git a/example/usecase/students.csv b/example/usecase/students.csv new file mode 100644 index 0000000..d0ecdfe --- /dev/null +++ b/example/usecase/students.csv @@ -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" diff --git a/example/usecase/tpl_example.tex b/example/usecase/tpl_example.tex new file mode 100644 index 0000000..c69211e --- /dev/null +++ b/example/usecase/tpl_example.tex @@ -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} diff --git a/examples/01_add_fraction.tex b/examples/01_add_fraction.tex deleted file mode 100644 index e3e4e46..0000000 --- a/examples/01_add_fraction.tex +++ /dev/null @@ -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} \ No newline at end of file diff --git a/examples/02_add_fraction.tex b/examples/02_add_fraction.tex deleted file mode 100644 index f865e87..0000000 --- a/examples/02_add_fraction.tex +++ /dev/null @@ -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} \ No newline at end of file diff --git a/examples/all_add_fraction.pdf b/examples/all_add_fraction.pdf deleted file mode 100644 index 491367a..0000000 Binary files a/examples/all_add_fraction.pdf and /dev/null differ diff --git a/examples/tpl_add_fraction.tex b/examples/tpl_add_fraction.tex deleted file mode 100644 index 5285bd4..0000000 --- a/examples/tpl_add_fraction.tex +++ /dev/null @@ -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} diff --git a/requirements.txt b/requirements.txt index 6c00717..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index b8bc0fc..4e1e194 100644 --- a/setup.py +++ b/setup.py @@ -4,20 +4,18 @@ from setuptools import setup setup( - name='Bopytex', - version='0.1.1', - description='Command line tool for compiling latex with python command embedded', - author='Benjamin Bertrand', - author_email='programming@opytex.org', - packages=['bopytex'], + name="Bopytex", + version="0.9", + description="Command line tool for compiling latex with python command embedded", + author="Benjamin Bertrand", + author_email="benjamin.bertrand@opytex.org", + packages=["bopytex"], install_requires=[ - 'mapytex', - 'mypytex', - ], - entry_points={ - "console_scripts": ['bopytex=bopytex.script:new'] - }, - ) + "click", + "jinja2", + ], + entry_points={"console_scripts": ["bopytex=bopytex.script:new"]}, +) # ----------------------------- # Reglages pour 'vim' diff --git a/snippets/1_fraction.tex b/snippets/1_fraction.tex deleted file mode 100644 index 3e211ac..0000000 --- a/snippets/1_fraction.tex +++ /dev/null @@ -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} \ No newline at end of file diff --git a/snippets/Geometrie/fig/parcours.pdf b/snippets/Geometrie/fig/parcours.pdf deleted file mode 100644 index 51edb46..0000000 Binary files a/snippets/Geometrie/fig/parcours.pdf and /dev/null differ diff --git a/snippets/Geometrie/tpl_Pythagore_thales.tex b/snippets/Geometrie/tpl_Pythagore_thales.tex deleted file mode 100644 index d48ffe1..0000000 --- a/snippets/Geometrie/tpl_Pythagore_thales.tex +++ /dev/null @@ -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} diff --git a/snippets/all_fonctions.pdf b/snippets/all_fonctions.pdf deleted file mode 100644 index 86299c8..0000000 Binary files a/snippets/all_fonctions.pdf and /dev/null differ diff --git a/snippets/all_fraction.pdf b/snippets/all_fraction.pdf deleted file mode 100644 index 95a35fe..0000000 Binary files a/snippets/all_fraction.pdf and /dev/null differ diff --git a/snippets/all_suite.pdf b/snippets/all_suite.pdf deleted file mode 100644 index f15b1ac..0000000 Binary files a/snippets/all_suite.pdf and /dev/null differ diff --git a/snippets/tpl_fonctions.tex b/snippets/tpl_fonctions.tex deleted file mode 100644 index ea3fac8..0000000 --- a/snippets/tpl_fonctions.tex +++ /dev/null @@ -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} diff --git a/snippets/tpl_fraction.tex b/snippets/tpl_fraction.tex deleted file mode 100644 index da4d9d1..0000000 --- a/snippets/tpl_fraction.tex +++ /dev/null @@ -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} diff --git a/snippets/tpl_suite.tex b/snippets/tpl_suite.tex deleted file mode 100644 index 24ed5e1..0000000 --- a/snippets/tpl_suite.tex +++ /dev/null @@ -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} diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fakes/__init__.py b/test/fakes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fakes/dispatcher.py b/test/fakes/dispatcher.py new file mode 100644 index 0000000..a0fa672 --- /dev/null +++ b/test/fakes/dispatcher.py @@ -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, + } +) diff --git a/test/fakes/planner.py b/test/fakes/planner.py new file mode 100644 index 0000000..dd50afe --- /dev/null +++ b/test/fakes/planner.py @@ -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"]) + ] diff --git a/test/fakes/workers.py b/test/fakes/workers.py new file mode 100644 index 0000000..919ad1b --- /dev/null +++ b/test/fakes/workers.py @@ -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}"], []) + diff --git a/test/students.csv b/test/students.csv deleted file mode 100644 index 5ddf91d..0000000 --- a/test/students.csv +++ /dev/null @@ -1,6 +0,0 @@ -nom,classe,elo -Bob,1ST,1000 -Pipo,1ST,1300 -Popi,1ST,100 -Boule,1ST,4000 -Bill,1ST,1300 diff --git a/test/templates/tpl_test.tex b/test/templates/tpl_test.tex deleted file mode 100644 index dc3183b..0000000 --- a/test/templates/tpl_test.tex +++ /dev/null @@ -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} diff --git a/test/test_bopytex.py b/test/test_bopytex.py deleted file mode 100644 index 078777a..0000000 --- a/test/test_bopytex.py +++ /dev/null @@ -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 diff --git a/test/test_e2e.py b/test/test_e2e.py new file mode 100644 index 0000000..66ca212 --- /dev/null +++ b/test/test_e2e.py @@ -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() + diff --git a/test/test_planner.py b/test/test_planner.py new file mode 100644 index 0000000..7f9a45b --- /dev/null +++ b/test/test_planner.py @@ -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", + ), + ] diff --git a/test/test_scheduler.py b/test/test_scheduler.py new file mode 100644 index 0000000..737637b --- /dev/null +++ b/test/test_scheduler.py @@ -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 == [] diff --git a/test/test_service.py b/test/test_service.py new file mode 100644 index 0000000..0fd68f9 --- /dev/null +++ b/test/test_service.py @@ -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"] diff --git a/test/test_tasks.py b/test/test_tasks.py new file mode 100644 index 0000000..3f33138 --- /dev/null +++ b/test/test_tasks.py @@ -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 diff --git a/test/test_texenv.py b/test/test_texenv.py new file mode 100644 index 0000000..e68c5b4 --- /dev/null +++ b/test/test_texenv.py @@ -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" + + diff --git a/test/worker/source.pdf b/test/worker/source.pdf new file mode 100644 index 0000000..e745427 Binary files /dev/null and b/test/worker/source.pdf differ diff --git a/test/worker/test_compile.py b/test/worker/test_compile.py new file mode 100644 index 0000000..c046473 --- /dev/null +++ b/test/worker/test_compile.py @@ -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 diff --git a/test/worker/test_generate.py b/test/worker/test_generate.py new file mode 100644 index 0000000..a189274 --- /dev/null +++ b/test/worker/test_generate.py @@ -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 diff --git a/test/worker/test_join_pdf.py b/test/worker/test_join_pdf.py new file mode 100644 index 0000000..5d6b843 --- /dev/null +++ b/test/worker/test_join_pdf.py @@ -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()