From cd8f921a4e725f03cd1011675662f19999fc5c78 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Fri, 29 Nov 2024 14:53:44 +0100 Subject: [PATCH 1/9] try update docker building as github actions --- .github/workflows/build-container.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 647bffd..a7fe1b9 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -13,24 +13,24 @@ jobs: name: 'Build amc2moodle container' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - id: docker-tag uses: yuya-takeyama/docker-tag-from-github-ref-action@v1 with: latest-branches: 'main,master' - name: "Build:checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to Github Packages - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.PAT }} - name: 'Build:dockerimage' - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: # relative path to the place where source code with Dockerfile is located context: ./docker From e19013e778d28a38d8ec13595b5d4f81d4fcadb9 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Fri, 29 Nov 2024 14:54:46 +0100 Subject: [PATCH 2/9] swicth workflo to ran on any cases --- .github/workflows/build-container.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index a7fe1b9..eb53908 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -2,10 +2,10 @@ name: 'Build amc2moodle container' on: push: - # branches: - # - 'master' - tags: - - 'v*' + branches: + - '*' #'master' + # tags: + # - 'v*' jobs: From 58b8c448128b4b2bd361c32ad46bf3603e461e8d Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Fri, 29 Nov 2024 15:03:21 +0100 Subject: [PATCH 3/9] add missing id in build-container.yml --- .github/workflows/build-container.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index eb53908..e6a2e00 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -30,6 +30,7 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.PAT }} - name: 'Build:dockerimage' + id: docker_build uses: docker/build-push-action@v6 with: # relative path to the place where source code with Dockerfile is located From 8e7a728c46635cc7edef8f3e9740ddbfa051be80 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Fri, 29 Nov 2024 15:28:23 +0100 Subject: [PATCH 4/9] update pyproject with optional dependencies for testing and start update Dockerfile --- docker/Dockerfile | 30 ++++++++++++++++++++---------- pyproject.toml | 5 +++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9dbdd08..770bce1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,11 +9,22 @@ LABEL org.opencontainers.image.source = "https://github.com/nennigb/amc2moodle/" ENV MONITOR_DIR=/tmp/work ENV LOG_DIR=/tmp/daemon ENV INSTALL_DIR_A2M=/tmp/amc2moodle +ENV VENVDIR=/tmp/venv +SHELL ["/bin/bash", "-c"] # install debian packages RUN apt update && \ - apt install -yy wget ghostscript imagemagick libtext-unidecode-perl latexml xmlindent python3-pip git && \ - apt clean + apt install -yy wget \ + ghostscript \ + imagemagick \ + libtext-unidecode-perl \ + latexml \ + xmlindent \ + python3-pip \ + python3-venv \ + git && \ + apt clean &&\ + rm -rf /var/lib/{apt,dpkg,cache,log}/ # move policy file RUN mv /etc/ImageMagick-6/policy.xml /etc/ImageMagick-6/policy.xml.off @@ -24,18 +35,17 @@ RUN mv /etc/ImageMagick-6/policy.xml /etc/ImageMagick-6/policy.xml.off WORKDIR /tmp RUN git clone https://github.com/nennigb/amc2moodle.git -b master ${INSTALL_DIR_A2M} WORKDIR ${INSTALL_DIR_A2M} -RUN pip3 install -U pip && \ - pip3 install . +RUN mkdir -p ${VENVDIR} && \ + python -m venv ${VENVDIR} +ENV PATH="${VENVDIR}/bin:$PATH" +RUN pip install -U pip && \ + pip install '.[test,cov]' # check if amc2moodle and moodle2amc work WORKDIR / -ENV TEXINPUTS=.:${INSTALL_DIR_A2M}/amc2moodle/moodle2amc/test:$TEXINPUTS +ENV TEXINPUTS=.:${INSTALL_DIR_A2M}/amc2moodle/tests/payload_test_moodle2amc/:${TEXINPUTS} RUN echo ${TEXINPUTS} -RUN python -m amc2moodle.amc2moodle.test && \ - python -m amc2moodle.utils.test && \ - python -m amc2moodle.moodle2amc.test - - +RUN python -m pytest --pyargs amc2moodle # create dir RUN mkdir ${MONITOR_DIR} && \ diff --git a/pyproject.toml b/pyproject.toml index ec9f95e..8f8a09a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,11 @@ dependencies = [ ] requires-python='>=3.8' +[project.optional-dependencies] +test = ["pytest"] +lint = [ "black", "flake8"] +cov = ["pytest < 5.0.0", "pytest-cov[all]"] + [project.urls] Homepage = "https://github.com/nennigb/amc2moodle" Repository = "https://github.com/nennigb/amc2moodle" From 4928c5d6e097e0ba3cda8ad76208e476304b9d63 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Fri, 29 Nov 2024 15:29:40 +0100 Subject: [PATCH 5/9] move covergae optional dependencies to test --- docker/Dockerfile | 2 +- pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 770bce1..981e25f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,7 +33,7 @@ RUN mv /etc/ImageMagick-6/policy.xml /etc/ImageMagick-6/policy.xml.off # RUN pip3 install -U pip && \ # pip install amc2moodle WORKDIR /tmp -RUN git clone https://github.com/nennigb/amc2moodle.git -b master ${INSTALL_DIR_A2M} +RUN git clone https://github.com/nennigb/amc2moodle.git -b fix-issue-for-building-docker-container ${INSTALL_DIR_A2M} WORKDIR ${INSTALL_DIR_A2M} RUN mkdir -p ${VENVDIR} && \ python -m venv ${VENVDIR} diff --git a/pyproject.toml b/pyproject.toml index 8f8a09a..865f445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,9 +27,8 @@ dependencies = [ requires-python='>=3.8' [project.optional-dependencies] -test = ["pytest"] +test = ["pytest", "pytest-cov[all]"] lint = [ "black", "flake8"] -cov = ["pytest < 5.0.0", "pytest-cov[all]"] [project.urls] Homepage = "https://github.com/nennigb/amc2moodle" From e143a88d5cb273e1efaa2e63b1f8305677cde217 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Fri, 29 Nov 2024 15:32:51 +0100 Subject: [PATCH 6/9] fix typos in Dockerfile + reactivate version activation of workflow for building container --- .github/workflows/build-container.yml | 8 ++++---- docker/Dockerfile | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index e6a2e00..62e9d37 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -2,10 +2,10 @@ name: 'Build amc2moodle container' on: push: - branches: - - '*' #'master' - # tags: - # - 'v*' + # branches: + # - 'master' + tags: + - 'v*' jobs: diff --git a/docker/Dockerfile b/docker/Dockerfile index 981e25f..5fe6635 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ FROM texlive/texlive:latest # labels LABEL AUTHOR='Luc Laurent' LABEL MAINTAINER='Luc Laurent & Benoit Nennig' -LABEL org.opencontainers.image.source = "https://github.com/nennigb/amc2moodle/" +LABEL org.opencontainers.image.source="https://github.com/nennigb/amc2moodle/" # declare useful directories using environement ENV MONITOR_DIR=/tmp/work @@ -30,16 +30,14 @@ RUN apt update && \ RUN mv /etc/ImageMagick-6/policy.xml /etc/ImageMagick-6/policy.xml.off # install pip and Python pkg -# RUN pip3 install -U pip && \ -# pip install amc2moodle WORKDIR /tmp -RUN git clone https://github.com/nennigb/amc2moodle.git -b fix-issue-for-building-docker-container ${INSTALL_DIR_A2M} +RUN git clone https://github.com/nennigb/amc2moodle.git -b master ${INSTALL_DIR_A2M} WORKDIR ${INSTALL_DIR_A2M} RUN mkdir -p ${VENVDIR} && \ python -m venv ${VENVDIR} ENV PATH="${VENVDIR}/bin:$PATH" RUN pip install -U pip && \ - pip install '.[test,cov]' + pip install '.[test]' # check if amc2moodle and moodle2amc work WORKDIR / From ee8790b95dc2fc2104848b24f53ed30ebe4f56e6 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Sat, 30 Nov 2024 10:25:23 +0100 Subject: [PATCH 7/9] update all versions of actions to @main --- .github/workflows/build-container.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 62e9d37..317be28 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -13,25 +13,25 @@ jobs: name: 'Build amc2moodle container' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@main - id: docker-tag - uses: yuya-takeyama/docker-tag-from-github-ref-action@v1 + uses: yuya-takeyama/docker-tag-from-github-ref-action@main with: latest-branches: 'main,master' - name: "Build:checkout" - uses: actions/checkout@v4 + uses: actions/checkout@main - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@main - name: Login to Github Packages - uses: docker/login-action@v3 + uses: docker/login-action@main with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.PAT }} - name: 'Build:dockerimage' id: docker_build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@main with: # relative path to the place where source code with Dockerfile is located context: ./docker From 5146354d8644473f6df7f81f0b78aadf6d7f3717 Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Tue, 3 Dec 2024 10:49:23 +0100 Subject: [PATCH 8/9] try to deal with unstable latex repo --- .github/workflows/ci-mac-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-mac-os.yml b/.github/workflows/ci-mac-os.yml index 9a4152b..adfef6a 100644 --- a/.github/workflows/ci-mac-os.yml +++ b/.github/workflows/ci-mac-os.yml @@ -54,7 +54,7 @@ jobs: run: | # xstring is just required for testing --includestyles sudo tlmgr update --self - sudo tlmgr install collection-fontsrecommended bophook xstring --verify-repo=none + sudo tlmgr install --verify-repo=none collection-fontsrecommended bophook xstring - name: Test latexml run: | From 512fa65133302af63b42a201dab7abc55ed2485d Mon Sep 17 00:00:00 2001 From: Luc LAURENT Date: Thu, 12 Dec 2024 13:15:05 +0100 Subject: [PATCH 9/9] Many changes * update pyproject.toml to integrate the use of ruff as code analyser * update Docker's files (Dockerfile, README, bash script) to ensure logrotation and a few fixes * many improvments of typos * update workflow to use @main on actions --- .github/workflows/ci-mac-os.yml | 174 +++++++++--------- README.md | 8 +- amc2moodle/__init__.py | 5 +- amc2moodle/amc2moodle/amc2moodle_class.py | 35 ++-- amc2moodle/amc2moodle/bin/amc2moodle.py | 19 +- amc2moodle/amc2moodle/convert.py | 60 +++--- amc2moodle/moodle2amc/__init__.py | 3 +- amc2moodle/moodle2amc/_questions.py | 41 +++-- amc2moodle/moodle2amc/_quiz.py | 28 +-- amc2moodle/moodle2amc/bin/moodle2amc.py | 18 +- amc2moodle/tests/test_amc2moodle.py | 14 +- amc2moodle/tests/test_moodle2amc.py | 13 +- .../tests/test_utils_calculatedParser.py | 10 +- amc2moodle/tests/test_utils_text.py | 6 +- amc2moodle/utils/calculatedParser.py | 52 +++--- amc2moodle/utils/customLogging.py | 2 +- amc2moodle/utils/flatex.py | 10 +- amc2moodle/utils/misc.py | 1 + amc2moodle/utils/text.py | 4 +- docker/Dockerfile | 17 +- docker/README.md | 13 +- docker/autorun-amc2moodle.sh | 26 ++- pyproject.toml | 9 +- 23 files changed, 307 insertions(+), 261 deletions(-) diff --git a/.github/workflows/ci-mac-os.yml b/.github/workflows/ci-mac-os.yml index adfef6a..046ed6e 100644 --- a/.github/workflows/ci-mac-os.yml +++ b/.github/workflows/ci-mac-os.yml @@ -6,16 +6,15 @@ name: CI-mac-os # events but only for the master branch on: push: - branches: [ '*' ] - paths-ignore: # Don't trigger on files that are updated by the CI + branches: ["*"] + paths-ignore: # Don't trigger on files that are updated by the CI - README.md pull_request: - branches: [ '*' ] + branches: ["*"] schedule: # * is a special character in YAML so you have to quote this string # run at 02:01 on every 15th day-of-month. - - cron: '1 2 */15 * *' - + - cron: "1 2 */15 * *" # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -23,93 +22,90 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13'] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] env: # set env variable for Wand - MAGICK_HOME: '/opt/homebrew' + MAGICK_HOME: "/opt/homebrew" # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@main - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@main - with: - python-version: ${{ matrix.python-version }} - - - name: Install the LaTeXML dependences - run: | - python -m pip install --upgrade pip - - # Install requirements for lateXML - brew update - brew install imagemagick - brew install --cask basictex - brew install latexml + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@main + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@main + with: + python-version: ${{ matrix.python-version }} - - name: Update $PATH - # run: echo ::add-path::/Library/TeX/texbin # depreciated - run: echo "/Library/TeX/texbin" >> $GITHUB_PATH - - - name: Add fonts (required by moodle2amc tests) - run: | - # xstring is just required for testing --includestyles - sudo tlmgr update --self - sudo tlmgr install --verify-repo=none collection-fontsrecommended bophook xstring - - - name: Test latexml - run: | - # add --strict flag to be more strict in error catching - latexml --noparse --nocomment --strict --path=./amc2moodle/amc2moodle --dest=./out.xml ./amc2moodle/tests/payload_test_amc2moodle/QCM.tex - - - name: Install amc2moodle - run: | - # add -e to have write access for test. TODO : change with temp file - pip install -e . - - - name: Test amc2moodle - run: | - python -m amc2moodle.tests.test_amc2moodle - - - name: Test moodle2amc - run: | - # add automultiplechoice.sty local copy to LaTeX PATH (for this step) - # export TEXINPUTS=.:./tests/payload_test_amc2moodle/:$TEXINPUTS (during tests, automultiplechoice.sty is copied in the temporary test folder ) - python -m amc2moodle.tests.test_moodle2amc - - name: Test text parser - run: | - python -m amc2moodle.tests.test_utils_text - - name: Test calculated questions parsers - run: | - python -m amc2moodle.tests.test_utils_calculatedParser - - - # Store output files - # amc2moodle - - name: Archive XML test output of amc2moodle (without tikz) - if: ${{ always() }} - uses: actions/upload-artifact@main - with: - name: test_notikz_${{ matrix.python-version }} - path: output_tests/test_notikz.xml - - - name: Archive XML test output of amc2moodle (with tikz) - if: ${{ always() }} - uses: actions/upload-artifact@main - with: - name: test_tikz_${{ matrix.python-version }} - path: output_tests/test_tikz.xml - # moodle2amc - - name: Move latex output - if: ${{ always() }} - run: | - mkdir moodle-bank-exemple-output - mv output_tests/test_moodle-bank-exemple.* moodle-bank-exemple-output/ - - - name: Archive LaTeX test output (moodle2amc) - if: ${{ always() }} - uses: actions/upload-artifact@main - with: - name: test_moodle-bank-exemple_${{ matrix.python-version }} - path: moodle-bank-exemple-output + - name: Install the LaTeXML dependences + run: | + python -m pip install --upgrade pip - + # Install requirements for lateXML + brew update + brew install imagemagick + brew install --cask basictex + brew install latexml + + - name: Update $PATH + # run: echo ::add-path::/Library/TeX/texbin # depreciated + run: echo "/Library/TeX/texbin" >> $GITHUB_PATH + + - name: Add fonts (required by moodle2amc tests) + run: | + # xstring is just required for testing --includestyles + sudo tlmgr update --self + sudo tlmgr install --verify-repo=none collection-fontsrecommended bophook xstring + + - name: Test latexml + run: | + # add --strict flag to be more strict in error catching + latexml --noparse --nocomment --strict --path=./amc2moodle/amc2moodle --dest=./out.xml ./amc2moodle/tests/payload_test_amc2moodle/QCM.tex + + - name: Install amc2moodle + run: | + # add -e to have write access for test. TODO : change with temp file + pip install -e . + + - name: Test amc2moodle + run: | + python -m amc2moodle.tests.test_amc2moodle + + - name: Test moodle2amc + run: | + # add automultiplechoice.sty local copy to LaTeX PATH (for this step) + # export TEXINPUTS=.:./tests/payload_test_amc2moodle/:$TEXINPUTS (during tests, automultiplechoice.sty is copied in the temporary test folder ) + python -m amc2moodle.tests.test_moodle2amc + - name: Test text parser + run: | + python -m amc2moodle.tests.test_utils_text + - name: Test calculated questions parsers + run: | + python -m amc2moodle.tests.test_utils_calculatedParser + + # Store output files + # amc2moodle + - name: Archive XML test output of amc2moodle (without tikz) + if: ${{ always() }} + uses: actions/upload-artifact@main + with: + name: test_notikz_${{ matrix.python-version }} + path: output_tests/test_notikz.xml + + - name: Archive XML test output of amc2moodle (with tikz) + if: ${{ always() }} + uses: actions/upload-artifact@main + with: + name: test_tikz_${{ matrix.python-version }} + path: output_tests/test_tikz.xml + # moodle2amc + - name: Move latex output + if: ${{ always() }} + run: | + mkdir moodle-bank-exemple-output + mv output_tests/test_moodle-bank-exemple.* moodle-bank-exemple-output/ + + - name: Archive LaTeX test output (moodle2amc) + if: ${{ always() }} + uses: actions/upload-artifact@main + with: + name: test_moodle-bank-exemple_${{ matrix.python-version }} + path: moodle-bank-exemple-output diff --git a/README.md b/README.md index 0d33097..a00c7e9 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Help and options can be obtained using amc2moodle -h ``` Then on moodle, go to the course `administration\question bank\import` and choose 'moodle XML format' and tick: **If your grade are not conform to that you must use: 'Nearest grade if not listed' in import option in the moodle question bank** (see below for details). -Examples of the `amc2moodle` possibilities are given at [QCM.pdf](./amc2moodle/amc2moodle/test/QCM.pdf) +Examples of the `amc2moodle` possibilities are given at [QCM.pdf](./amc2moodle/tests/payload_test_amc2moodle/QCM.pdf) If your original exam uses [AMC-TXT syntax](https://www.auto-multiple-choice.net/auto-multiple-choice.en/AMC-TXT.shtml), you must first convert it to LaTeX before feeding it to `amc2moodle`. To convert an AMC-TXT file to LaTeX, generate the exam documents with AMC graphical interface as usual. AMC will generate a LaTeX version of your exam called `DOC-filtered.tex` inside the project directory, which you can pass to `amc2moodle`. @@ -82,7 +82,7 @@ Help and options can be obtained using ``` moodle2amc -h ``` -Then the output LaTeX can be edited and included for creating amc exams. Examples of the `moodle2amc` possibilities are given [here](./amc2moodle/moodle2amc/test/moodle-bank-exemple.pdf). +Then the output LaTeX can be edited and included for creating amc exams. Examples of the `moodle2amc` possibilities are given [here](./amc2moodle/tests/payload_test_moodle2amc/moodle-bank-exemple.pdf). @@ -110,8 +110,8 @@ If you want to contribute to `amc2moodle`, your are welcomed! Don't hesitate to - add support for other language (French and English are present) in AMC command - ... -To ensure code homogeneity among contributors, we use a source-code analyzer (e.g. `pylint`). -Before submitting a PR, run the tests suite. +To ensure code homogeneity among contributors, we use [`ruff`](https://docs.astral.sh/ruff/) as source-code analyzer included in [`hatch`](https://hatch.pypa.io/1.9/config/static-analysis/) (e.g. `hatch fmt --check`). +Before submitting a PR, run the tests suite using `hatch test` (or `hatch test -c` to run test suite with covering report). ## License This file is part of amc2moodle, a tool to convert automultiplechoice quizzes to moodle questions. diff --git a/amc2moodle/__init__.py b/amc2moodle/__init__.py index e3b606b..b32a209 100644 --- a/amc2moodle/__init__.py +++ b/amc2moodle/__init__.py @@ -18,9 +18,12 @@ along with this program. If not, see . """ -from amc2moodle._version import __version__ import logging + +from amc2moodle._version import __version__ + from .utils.customLogging import CountLogger as _CountLogger + # Define the new default logger class. It has to be here to ensure that all # logger instances will have the good class # TODO check for possible side effect diff --git a/amc2moodle/amc2moodle/amc2moodle_class.py b/amc2moodle/amc2moodle/amc2moodle_class.py index 9f088ab..5a01727 100644 --- a/amc2moodle/amc2moodle/amc2moodle_class.py +++ b/amc2moodle/amc2moodle/amc2moodle_class.py @@ -18,18 +18,19 @@ along with this program. If not, see . """ -from typing import Callable -from ..amc2moodle import convert -from ..utils.flatex import Flatex -import subprocess -import sys +import logging import os import shutil -from importlib import util # python 3.x +import subprocess +import sys import tempfile -from shutil import copytree -import logging from concurrent.futures import ThreadPoolExecutor +from importlib import util # python 3.x +from shutil import copytree +from typing import Callable + +from ..amc2moodle import convert +from ..utils.flatex import Flatex # activate logger Logger = logging.getLogger(__name__) @@ -49,7 +50,7 @@ def checkTools(show=True): Logger.critical("Please install lxml's Python module") # LaTeXML latexMLwhich = subprocess.run(['which', 'latexml'], - stdout=subprocess.DEVNULL) + stdout=subprocess.DEVNULL, check=False) latexmlOk = latexMLwhich.returncode == 0 if not latexmlOk: Logger.critical("Please install LaTeXML software (see https://dlmf.nist.gov/LaTeXML/)") @@ -271,22 +272,22 @@ def runLaTeXML(self): def runXMLindent(self): """Run XML indentation with subprocess.""" # check for xmlindent - xmlindentwhich = subprocess.run(['which', 'xmlindent']) + xmlindentwhich = subprocess.run(['which', 'xmlindent'], check=False) xmlindentOk = xmlindentwhich.returncode == 0 # check for xmllint (Macos) - xmllintwhich = subprocess.run(['which', 'xmllint']) + xmllintwhich = subprocess.run(['which', 'xmllint'], check=False) xmllintOk = xmllintwhich.returncode == 0 # linux if xmlindentOk: Logger.debug(' > Indenting XML output...') subprocess.run(['xmlindent', self.output, '-o', self.output], - stdout=subprocess.DEVNULL) + stdout=subprocess.DEVNULL, check=False) # MacOS if xmllintOk and not xmlindentOk: Logger.debug(' > Indenting XML output...') subprocess.run(['xmllint', self.output, '--format', '--output', self.output], - stdout=subprocess.DEVNULL) + stdout=subprocess.DEVNULL, check=False) def runCleanXML(self): """Clean final XML file remove "%" added by LaTeXML at end of lines (EXPERIMENTAL).""" @@ -295,8 +296,8 @@ def runCleanXML(self): # copy output file and remove '%\n' (a temporary file will be used and deleted) fdTemp, pathTemp = tempfile.mkstemp(dir=getPathFile(self.inputtex), prefix='xmlclean') - Logger.debug(" > Cleaning: create {} ".format(pathTemp)) - with open(self.output, 'r') as fin, open(pathTemp, 'w') as fout: + Logger.debug(f" > Cleaning: create {pathTemp} ") + with open(self.output) as fin, open(pathTemp, 'w') as fout: # set replacement counter for patern occurence nreplacement = 0 for lno, line in enumerate(fin): @@ -305,12 +306,12 @@ def runCleanXML(self): # remove '%' at ends of lines line = line.replace(pattern, "\n") if count > 0: - Logger.debug(" Remove pattern at (line {} of {})".format(lno+1, self.output)) + Logger.debug(f" Remove pattern at (line {lno+1} of {self.output})") fout.write(line) # copy temporary file to the output os.close(fdTemp) shutil.copy(pathTemp, self.output) - Logger.info(" > Cleaning: done, with {} replacements.".format(nreplacement)) + Logger.info(f" > Cleaning: done, with {nreplacement} replacements.") def runBuilding(self): """Build the xml file for Moodle quizz.""" diff --git a/amc2moodle/amc2moodle/bin/amc2moodle.py b/amc2moodle/amc2moodle/bin/amc2moodle.py index 1caf4f1..4da7639 100755 --- a/amc2moodle/amc2moodle/bin/amc2moodle.py +++ b/amc2moodle/amc2moodle/bin/amc2moodle.py @@ -18,13 +18,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from amc2moodle.utils.customLogging import customLogger -import amc2moodle as amdlpkg -from amc2moodle.amc2moodle import amc2moodle_class as a2m import argparse import os import sys +import amc2moodle as amdlpkg +from amc2moodle.amc2moodle import amc2moodle_class as a2m +from amc2moodle.utils.customLogging import customLogger + def run(): """ Read command line input and run amc2mooodle conversion. @@ -65,7 +66,7 @@ def run(): parser.add_argument("-V", "--version", help='''Show the current version of moodle2amc''', action="version", - version="%(prog)s v{version}".format(version=amdlpkg.__version__)) + version=f"%(prog)s v{amdlpkg.__version__}") parser.add_argument("-v", "--verbose", help='''Show all log messages in CLI. Use -vv for more verbosity.''', required=False, action="count",default=0) @@ -110,13 +111,13 @@ def run(): logObj = customLogger('amc2moodle') logObj.setupConsoleLogger(verbositylevel=verboseMode, silent=silentMode, - txtinfo=amdlpkg.__version__) + txtinfo=amdlpkg.__version__) # check input file fileInOk = False if fileIn is not None: fileInOk = os.path.exists(fileIn) - + # declare log file if fileInOk and logFileMode: logFile = os.path.splitext(os.path.basename(fileIn))[0]+'_amc2moodle.log' @@ -150,13 +151,13 @@ def run(): indentXML=indentFlag, usetempdir=tempDir, magic_flag=magic_flag, cleanXML=cleanXML, include_styles=include_styles) - else: + else: # exit with error status globalReturncode = 1 #info about the log if fileInOk and logFileMode: - Logger.info("Log file of amc2moodle's run: {}".format(logFile)) - + Logger.info(f"Log file of amc2moodle's run: {logFile}") + # exit with error status sys.exit(globalReturncode) diff --git a/amc2moodle/amc2moodle/convert.py b/amc2moodle/amc2moodle/convert.py index 6b89acd..3c5dc1e 100755 --- a/amc2moodle/amc2moodle/convert.py +++ b/amc2moodle/amc2moodle/convert.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ This file is part of amc2moodle, a convertion tool to recast quiz written with the LaTeX format used by automuliplechoice 1.0.3 into the @@ -19,17 +18,17 @@ along with this program. If not, see . """ -import sys -from abc import ABC, abstractmethod -from lxml import etree import base64 +import logging import os -from wand.image import Image as wandImage -from xml.sax.saxutils import unescape -from ..utils.calculatedParser import * import random -import logging +from abc import ABC, abstractmethod +from xml.sax.saxutils import unescape +from lxml import etree +from wand.image import Image as wandImage + +from ..utils.calculatedParser import * # Define default and global SUPPORTED_Q_TYPE = ('amc_questionmult', 'amc_question', 'amc_questionnumeric', @@ -49,7 +48,7 @@ # Add `amc_aucune` if required 'amc_autocomplete': 1, # String for - 'amc_aucune': u"aucune de ces réponses n'est correcte.", + 'amc_aucune': "aucune de ces réponses n'est correcte.", # **Scoring see AMC doc** # Simple : e :incohérence, b: bonne, m: mauvaise, p: planché @@ -92,7 +91,7 @@ def strtobool(s): elif isinstance(s, str): return s.lower() in ("yes", "true", "t", "1") else: - raise ValueError("The argument '{}' must be a string or a boolean.".format(s)) + raise ValueError(f"The argument '{s}' must be a string or a boolean.") def basename(s): @@ -144,10 +143,7 @@ def convertImage(self, fileIn, fileOut, resolution): im.strip() # for (k, v) in im.artifacts.items(): # print(k, v) - Logger.debug(" Conversion from {} to {} (imgResolution={}).".format( - os.path.splitext(fileIn)[1], - os.path.splitext(fileOut)[1], - resolution)) + Logger.debug(f" Conversion from {os.path.splitext(fileIn)[1]} to {os.path.splitext(fileOut)[1]} (imgResolution={resolution}).") im.save(filename=fileOut) im.close() @@ -216,8 +212,7 @@ def __init__(self, Qi, context): def __repr__(self): """ Change string representation. """ - rep = ("Instance of {} containing the question '{}'." - .format(self.__class__.__name__, self.name)) + rep = (f"Instance of {self.__class__.__name__} containing the question '{self.name}'.") return rep def __str__(self): @@ -270,7 +265,7 @@ def _setWithOptionsOrDefault(self, opt_name, default_value): def convert(self): """ Run all questions convertion steps. """ - Logger.debug(" * processing {} question '{}'...".format(self.__class__.__name__, self.name)) + Logger.debug(f" * processing {self.__class__.__name__} question '{self.name}'...") self._options() self._encodeImg() self._scoring() @@ -291,7 +286,7 @@ def _options(self): optlist = Qi.xpath("./note[@class='amc_choices_options']") if optlist and 'o' in optlist[0].text.strip().split(","): Qiwantshuffle = False - Logger.debug(" Keep choices order in question '{}'.".format(self.name)) + Logger.debug(f" Keep choices order in question '{self.name}'.") etree.SubElement(Qi, "shuffleanswers").text = str(Qiwantshuffle).lower() # store local answernumbering policy @@ -316,7 +311,7 @@ def _scoring(self): if len(barl) > 0: amc_bl_ = scoring2dict(barl[0].text) amc_bl.update(amc_bl_) - Logger.debug(" local scoring: {}".format(amc_bl)) + Logger.debug(f" local scoring: {amc_bl}") if (float(amc_bl['b']) < 1.): Logger.warning("The grade of the good answser(s) may be < 100%, put b=1") @@ -358,7 +353,7 @@ def _scoring(self): # partial scoring is provided amc_bml_ = scoring2dict(barl[0].text) amc_bml.update(amc_bml_) - Logger.debug(" local scoring : {}".format(amc_bml)) + Logger.debug(f" local scoring : {amc_bml}") if (float(amc_bml['b']) < 1): Logger.warning("The grade of the good answser(s) may be < 100%, put b=1") @@ -502,12 +497,10 @@ class AMCQuestionDescription(AMCQuestion): def _options(self): """ Parse options and create the required elements. """ - pass def _scoring(self): """ Compute the scoring. """ - pass class _Calculated: @@ -623,7 +616,7 @@ def _datasets(self, wildcards): number_of_items.text = str(nitems) # set container for all random values dataset_items = etree.SubElement(data, 'dataset_items') - for i in range(0, nitems): + for i in range(nitems): # set container for each random value dataset_item = etree.SubElement(dataset_items, 'dataset_item') number = etree.SubElement(dataset_item, 'number') @@ -666,7 +659,7 @@ def _scoring(self): super()._scoring() # parse fp expressions wildcards = self._parsemath() - Logger.debug(' Found {} wildcards.'.format(len(wildcards))) + Logger.debug(f' Found {len(wildcards)} wildcards.') # TODO add a test to check that all wildcards starts with rand! # to control substitution # Add calcultaed question specific fields to all answers. @@ -678,13 +671,11 @@ def _scoring(self): class AMCQuestionCalcMult(_Calculated, AMCQuestionSimple): """ Parametrized Multiple choice question with single good answer. """ - pass class AMCQuestionMultCalcMult(_Calculated, AMCQuestionMult): """ Parametrized Multiple choice question with multiple good answers. """ - pass # dict of all available question @@ -718,10 +709,10 @@ def CreateQuestion(qtype, Qi, context): try: return Q_FACTORY[qtype](Qi, context) except NameError: - raise KeyError(" 'qtype' argument should be in {}".format(Q_FACTORY.keys())) + raise KeyError(f" 'qtype' argument should be in {Q_FACTORY.keys()}") -class Context(): +class Context: """ Contains the context of the quizz, like path, default options. Basically it will contains all information callected at quiz level but @@ -816,8 +807,7 @@ def __init__(self, xml, pathin, wdir, catname, deb=0): def __repr__(self): """ Change string representation. """ - rep = ("Instance of {}. {} have be converted." - .format(self.__class__.__name__, self.Qtot)) + rep = (f"Instance of {self.__class__.__name__}. {self.Qtot} have be converted.") return rep def __str__(self): @@ -884,7 +874,7 @@ def toMoodle(self, fileout): Logger.debug(" ") Logger.debug(" > global 'shuffleanswers' is {}.".format(strtobool(self.options['shuffle_all']))) Logger.debug(" > global 'answerNumberingFormat' is '{}'.".format(self.options['answer_numbering_format'])) - Logger.debug(" > {} questions converted.".format(self.Qtot)) + Logger.debug(f" > {self.Qtot} questions converted.") # Summary of logged events in convert.py only log_msg = " > Found {} Warnings and {} Errors during conversion (see above)." @@ -901,7 +891,7 @@ def _element_pre_process(self, tree): # These elements should be nodes at roots level. all_para = tree.findall('./para') if len(all_para) > 0: - Logger.warning(" > {} '\\element' blocks contain text outside ".format(len(all_para)) + Logger.warning(f" > {len(all_para)} '\\element' blocks contain text outside " + "'Question environnement'. " + "Skip them for moodle XML compatibility. " + "Increase verbosity to see them. " @@ -968,7 +958,7 @@ def _scoring(self): if len(bars) > 0: # on découpe bar[0].text et on affecte les nouvelles valeurs par défaut amc_bs = scoring2dict(bars[0].text) - Logger.debug("baremeDefautS : {}".format(amc_bs)) + Logger.debug(f"baremeDefautS : {amc_bs}") if (float(amc_bs['b']) < 1): Logger.warning("The grade of the good answser in question will be < 100%, put b=1") self.amc_bs.update(amc_bs) @@ -979,7 +969,7 @@ def _scoring(self): if len(barm) > 0: # on découpe bar[0].text et on affecte les nouvelles valeurs par défaut amc_bm = scoring2dict(barm[0].text) - Logger.debug("baremeDefautM : {}".format(amc_bm)) + Logger.debug(f"baremeDefautM : {amc_bm}") if (float(amc_bm['b']) < 1): Logger.warning("The grade of the good answser(s) in questionmult may be < 100%, put b=1") self.amc_bm.update(amc_bm) @@ -1113,7 +1103,7 @@ def to_moodle(filein, pathin, fileout='out.xml', pathout='.', wdir = workingdir # load input latexml xml file - with open(os.path.join(wdir, filein), 'r') as xml: + with open(os.path.join(wdir, filein)) as xml: # instanciate the Quiz object quiz = AMCQuiz(xml, pathin, wdir, catname, deb) # run the conversion and save the output diff --git a/amc2moodle/moodle2amc/__init__.py b/amc2moodle/moodle2amc/__init__.py index f792044..1575e5e 100755 --- a/amc2moodle/moodle2amc/__init__.py +++ b/amc2moodle/moodle2amc/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Created on Thu May 21 15:15:44 2020 @@ -8,4 +7,4 @@ # import class from ._questions import * -from ._quiz import Quiz \ No newline at end of file +from ._quiz import Quiz diff --git a/amc2moodle/moodle2amc/_questions.py b/amc2moodle/moodle2amc/_questions.py index 47530c3..1dc6a8b 100755 --- a/amc2moodle/moodle2amc/_questions.py +++ b/amc2moodle/moodle2amc/_questions.py @@ -24,20 +24,24 @@ 'QuestionNumerical', 'QuestionCalculatedMulti', 'SUPPORTED_QUESTION_TYPE', 'CreateQuestion'] -from lxml import etree -from xml.sax.saxutils import unescape import base64 +import logging +import math import os -from abc import ABC, abstractmethod -from wand.image import Image +import sys + # decode filemame unsed in moodle import urllib -import math -import sys -from ..utils.calculatedParser import * -from amc2moodle.utils.text import clean_q_name +from abc import ABC, abstractmethod +from xml.sax.saxutils import unescape + import markdown -import logging +from lxml import etree +from wand.image import Image + +from amc2moodle.utils.text import clean_q_name + +from ..utils.calculatedParser import * # list of supported moodle question type for SUPPORTED_QUESTION_TYPE = {'multichoice', 'essay', 'description', @@ -99,8 +103,7 @@ def __init__(self, q): def __repr__(self): """ Change string representation. """ - rep = ("Instance of {} containing the question '{}'." - .format(self.__class__.__name__, self.name)) + rep = (f"Instance of {self.__class__.__name__} containing the question '{self.name}'.") return rep def __str__(self): @@ -137,7 +140,7 @@ def format2tex(self, cdata_content, text_format): extensions=['extra']) text = self.html2tex(unescape('' + cdata_content + '')) else: - Logger.warning("> Unsupported format '{}'. Try with html filter.".format(text_format)) + Logger.warning(f"> Unsupported format '{text_format}'. Try with html filter.") text = self.html2tex(cdata_content) return text @@ -161,7 +164,7 @@ def html2tex(self, cdata_content): # remove manually CDATA from the string # FIXME use .text and etree.CDATA(new_text) cdata_content = (cdata_content.replace('') - .replace('%', '\%') + .replace('%', r'\%') .replace(']]>', '') .replace('
', '')) @@ -255,13 +258,11 @@ def question(self): def gettype(self): """ Determine the amc question type. """ - pass @abstractmethod def answers(self): """ Create and parse answers. """ - pass def transform(self, catname): """ Main routine, applied the xml transformation. @@ -302,7 +303,7 @@ def gettype(self): elif single.lower() == 'false': amcqtype = 'questionmult' else: - Logger.error("> Unknwon question type in '{}'".format(self.name)) + Logger.error(f"> Unknwon question type in '{self.name}'") return amcqtype @@ -378,7 +379,7 @@ def answers(self): amc_open.append(amc_good) amc_open.append(amc_wrong) else: - raise NotImplementedError() + raise NotImplementedError # print(etree.tostring(amc_open).decode()) return amc_open @@ -516,7 +517,7 @@ def question(self): """ # call the class method and add `\QuestionIndicative` text = super().question() - text.text = u"\QuestionIndicative\n" + text.text + text.text = "\\QuestionIndicative\n" + text.text return text @@ -541,7 +542,7 @@ def gettype(self): elif single.lower() in FALSE: amcqtype = 'questionmult' else: - Logger.error(" Unknwon question type in '{}'".format(self.name)) + Logger.error(f" Unknwon question type in '{self.name}'") return amcqtype @@ -680,4 +681,4 @@ def CreateQuestion(qtype, question): try: return Q_FACTORY[qtype](question) # *args,**kwargs) except: - raise KeyError(" 'qtype' argument should be in {}".format(Q_FACTORY.keys() ) ) + raise KeyError(f" 'qtype' argument should be in {Q_FACTORY.keys()}" ) diff --git a/amc2moodle/moodle2amc/_quiz.py b/amc2moodle/moodle2amc/_quiz.py index 3871de1..3b942da 100755 --- a/amc2moodle/moodle2amc/_quiz.py +++ b/amc2moodle/moodle2amc/_quiz.py @@ -19,15 +19,17 @@ along with this program. If not, see . """ -from lxml import etree -from xml.sax.saxutils import unescape -import subprocess -import os -from ._questions import * -from amc2moodle.utils.text import clean_q_name import logging +import os +import subprocess from concurrent.futures import ThreadPoolExecutor +from xml.sax.saxutils import unescape + +from lxml import etree +from amc2moodle.utils.text import clean_q_name + +from ._questions import * # output latex File LATEX_FILEOUT = 'out.tex' @@ -69,7 +71,7 @@ def __init__(self, mdl_xml_file): def __repr__(self): """ Change string representation. """ - rep = "Instance of {}.".format(self.__class__.__name__) + rep = f"Instance of {self.__class__.__name__}." return rep def __str__(self): @@ -251,7 +253,7 @@ def _reshape(self): # for tex conversion, it is done in Question __init__ qname = clean_q_name(qname) if qtype in SUPPORTED_QUESTION_TYPE: - Logger.debug("> Reshape question '{}' of type '{}'.".format(qname, qtype)) + Logger.debug(f"> Reshape question '{qname}' of type '{qtype}'.") # if no encounter category name before this question, # add the defaut catname in the cat_dict if not(cat_dict): @@ -261,7 +263,7 @@ def _reshape(self): amc_q = CreateQuestion(qtype, question).transform(catname) amc.append(amc_q) else: - Logger.error("> Question '{}' of type '{}' is not supported. Skipping.".format(qname, qtype)) + Logger.error(f"> Question '{qname}' of type '{qtype}' is not supported. Skipping.") Logger.info('> done.') @@ -301,9 +303,9 @@ def compileLatex(latexFile): latexFile : string full name of a latex file. """ - command = "pdflatex -interaction=nonstopmode -halt-on-error -file-line-error {}".format(latexFile) + command = f"pdflatex -interaction=nonstopmode -halt-on-error -file-line-error {latexFile}" #TODO: caution with 'universal_newlines=' (new syntax from Python 3.7: text=) - Logger.debug('Run command {}'.format(command)) + Logger.debug(f'Run command {command}') with subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -326,7 +328,7 @@ def compileLatex(latexFile): Logger.debug) rstdout.result() rstderr.result() - + # status = subprocess.run(command.split(), # stdout=subprocess.DEVNULL) return latexProcess @@ -338,7 +340,7 @@ def output_msg(): # Summary of logged events in convert.py only log_msg = "> Found {} Warnings and {} Errors during conversion (see above)." # Needto sum from _questions.py and _quiz.py - quest_log = logging.getLogger('amc2moodle.moodle2amc._questions') + quest_log = logging.getLogger('amc2moodle.moodle2amc._questions') warn_number = quest_log.counter['warning'] + Logger.counter['warning'] err_number = (quest_log.counter['error'] + Logger.counter['error'] + quest_log.counter['critical'] + Logger.counter['critical']) diff --git a/amc2moodle/moodle2amc/bin/moodle2amc.py b/amc2moodle/moodle2amc/bin/moodle2amc.py index a5a02a7..08d9fcd 100755 --- a/amc2moodle/moodle2amc/bin/moodle2amc.py +++ b/amc2moodle/moodle2amc/bin/moodle2amc.py @@ -18,13 +18,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from amc2moodle.utils.customLogging import customLogger -import amc2moodle as amdlpkg -import amc2moodle.moodle2amc as mdl2amc import argparse import os import sys -import logging + +import amc2moodle as amdlpkg +import amc2moodle.moodle2amc as mdl2amc +from amc2moodle.utils.customLogging import customLogger def run(): @@ -55,7 +55,7 @@ def run(): parser.add_argument("-V","--version", help='''Show the current version of moodle2amc''', action="version", - version="%(prog)s v{version}".format(version=amdlpkg.__version__)) + version=f"%(prog)s v{amdlpkg.__version__}") parser.add_argument("-v", "--verbose", help='''Show all log messages in CLI. Use -vv for more verbosity.''', required=False, action="count",default=0) @@ -78,7 +78,7 @@ def run(): fileIn = args.inputfile if args.output: fileOut = args.output - + silentMode = args.silent verboseMode = args.verbose logFileMode = args.no_log_file @@ -87,7 +87,7 @@ def run(): logObj = customLogger('amc2moodle') logObj.setupConsoleLogger(verbositylevel=verboseMode, silent=silentMode, - txtinfo=amdlpkg.__version__) + txtinfo=amdlpkg.__version__) # check input file @@ -141,10 +141,10 @@ def run(): globalReturncode = 1 #info about the log if fileInOk and logFileMode: - Logger.info("Log file of moodle2amc's run: {}".format(logFile)) + Logger.info(f"Log file of moodle2amc's run: {logFile}") # exit with error status sys.exit(globalReturncode) - + # Run autonomous diff --git a/amc2moodle/tests/test_amc2moodle.py b/amc2moodle/tests/test_amc2moodle.py index c16d12e..6a19a4a 100755 --- a/amc2moodle/tests/test_amc2moodle.py +++ b/amc2moodle/tests/test_amc2moodle.py @@ -18,14 +18,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from amc2moodle.utils.customLogging import customLogger -from amc2moodle.amc2moodle import amc2moodle_class as a2m -from amc2moodle.utils.misc import check_hash -import amc2moodle as amdlpkg import os import unittest + from lxml import etree +import amc2moodle as amdlpkg +from amc2moodle.amc2moodle import amc2moodle_class as a2m +from amc2moodle.utils.customLogging import customLogger +from amc2moodle.utils.misc import check_hash + # Load logger logObj = customLogger('amc2moodle') logObj.setupConsoleLogger(verbositylevel=2, @@ -138,7 +140,7 @@ def question_fields(self, qname, target_ans_sum, frac = float(a.attrib['fraction']) frac_list.append(frac) s += frac - Logger.debug('In {}, fraction are {}\n'.format(qname, frac_list)) + Logger.debug(f'In {qname}, fraction are {frac_list}\n') if abs(s - target_ans_sum) > TOL: ok += 1 else: @@ -430,7 +432,7 @@ class TestSuiteStyles(unittest.TestCase): def check_error(fileOut): """Parse ouput file `fileOut` and check if `ERROR` are present.""" is_error = False - with open(fileOut, 'r') as f: + with open(fileOut) as f: for line in f.readlines(): if 'ERROR' in line: print(line) diff --git a/amc2moodle/tests/test_moodle2amc.py b/amc2moodle/tests/test_moodle2amc.py index 6e6d124..4e539c5 100755 --- a/amc2moodle/tests/test_moodle2amc.py +++ b/amc2moodle/tests/test_moodle2amc.py @@ -19,14 +19,15 @@ along with this program. If not, see . """ -from amc2moodle.utils.customLogging import customLogger -from amc2moodle.utils.misc import check_hash, decorator_set_cwd -from amc2moodle.moodle2amc import Quiz -import amc2moodle as amdlpkg import os import shutil import unittest +import amc2moodle as amdlpkg +from amc2moodle.moodle2amc import Quiz +from amc2moodle.utils.customLogging import customLogger +from amc2moodle.utils.misc import check_hash, decorator_set_cwd + # Load logger logObj = customLogger("amc2moodle") logObj.setupConsoleLogger(verbositylevel=2, silent=False, txtinfo=amdlpkg.__version__) @@ -91,13 +92,13 @@ def test_mdl_bank(self): Logger.info("> Converted XML is identical to the reference: OK") # test latex compilation status = quiz.compileLatex(fileOut) - Logger.info("cwd {}".format(os.getcwd())) + Logger.info(f"cwd {os.getcwd()}") if status.returncode != 0: Logger.info("> pdflatex encounters Errors, see logs...") else: Logger.info("> pdflatex compile without Errors: OK") - Logger.info("cwd {}".format(os.getcwd())) + Logger.info(f"cwd {os.getcwd()}") self.assertEqual(status.returncode, 0) diff --git a/amc2moodle/tests/test_utils_calculatedParser.py b/amc2moodle/tests/test_utils_calculatedParser.py index b333073..b95a69e 100755 --- a/amc2moodle/tests/test_utils_calculatedParser.py +++ b/amc2moodle/tests/test_utils_calculatedParser.py @@ -19,10 +19,12 @@ along with this program. If not, see . """ -from amc2moodle.utils.calculatedParser import * -from io import StringIO import unittest +from io import StringIO from unittest.mock import patch + +from amc2moodle.utils.calculatedParser import * + # # import also text test # from amc2moodle.utils.test_text import * @@ -74,7 +76,7 @@ def test_render(self): # Create the parser parser = CreateCalculatedParser('xml2fp') for e, ref, expectedwarn in self.expr: - print("Expr = {} -> {}".format(e, ref)) + print(f"Expr = {e} -> {ref}") # mock out std ouput for testing with patch('sys.stdout', new=StringIO()) as fake_out: # parse answer @@ -116,7 +118,7 @@ def test_render(self): # Create the parser parser = CreateCalculatedParser('fp2xml') for e, ref, expectedwarn in self.expr: - print("Expr = {} -> {}".format(e, ref)) + print(f"Expr = {e} -> {ref}") # mock out std ouput for testing with patch('sys.stdout', new=StringIO()) as fake_out: # parse answer diff --git a/amc2moodle/tests/test_utils_text.py b/amc2moodle/tests/test_utils_text.py index ead0aaa..a1dbb6a 100755 --- a/amc2moodle/tests/test_utils_text.py +++ b/amc2moodle/tests/test_utils_text.py @@ -19,10 +19,12 @@ along with this program. If not, see . """ -import amc2moodle.utils.text as text import unittest -# Run by utils.test +from amc2moodle.utils import text + + +# Run by utils.test class TestText(unittest.TestCase): """ Define calculated question parser test cases for unittest. """ diff --git a/amc2moodle/utils/calculatedParser.py b/amc2moodle/utils/calculatedParser.py index da65c31..89e7e8e 100755 --- a/amc2moodle/utils/calculatedParser.py +++ b/amc2moodle/utils/calculatedParser.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Illustration of Basic Feature of Pyparsing @@ -8,10 +7,24 @@ @author: bn """ from abc import ABC, abstractmethod -from pyparsing import Word, alphas, nums, alphas, alphanums, Char, oneOf,\ - Suppress, Combine, Regex, Group, ZeroOrMore, Literal,\ - Forward, Optional, ParseResults + +from pyparsing import ( + Combine, + Forward, + Group, + Literal, + Optional, + ParseResults, + Regex, + Word, + ZeroOrMore, + alphanums, + alphas, + nums, + oneOf, +) from pyparsing import __version__ as pyparsing_version + # In pyparsing 3 _flatten function has been move in `util` submodule if int(pyparsing_version.split('.')[0]) > 2: from pyparsing.util import _flatten @@ -20,7 +33,6 @@ import logging - __all__ = ['CalculatedParserToFP', 'CalculatedParserFromFP', 'CreateCalculatedParser'] @@ -71,7 +83,7 @@ FP_UNSUPPORTED = {'atan2', 'atanh', 'bindec', 'decbin', 'decoct', 'deg2rad', 'expm1', 'fmod', 'is_finite', 'is_infinite', 'is_nan', 'log10', 'log1p', 'octdec', 'rad2deg', 'rand', 'cosh', - 'sinh', 'tanh', 'acosh', 'asinh', 'atanh', 'ceil', 'floor'} + 'sinh', 'tanh', 'acosh', 'asinh', 'ceil', 'floor'} FP_MAX = 999999999999999999.999999999999999999 @@ -96,7 +108,7 @@ MDL_UNSUPPORTED = {'clip'} -''' Usefull info for pgfmathparse +r''' Usefull info for pgfmathparse http://tug.ctan.org/tex-archive/graphics/pgf/base/doc/pgfmanual.pdf p 1033 The following functions are recognized: @@ -130,7 +142,6 @@ class CalculatedParser(ABC): @abstractmethod def varformat(self): """A formater string for rendering variable in tex. Must contains {name}""" - pass def grammar(self): """ Define the parser grammar. @@ -212,7 +223,7 @@ def variable_hook(tokens): # notpossible to use '_' in tex name out = tokens.name.replace('_', '') if out.isalpha() == False: - Logger.warning(" The variable '{}' is not compatible with LaTeX naming convention. You will need to change this name in our tex file.".format(out)) + Logger.warning(f" The variable '{out}' is not compatible with LaTeX naming convention. You will need to change this name in our tex file.") return "\\" + out +' ' @staticmethod @@ -222,7 +233,7 @@ def real_hook(tokens): # check for overflow x = float(tokens[0]) if abs(x) > FP_MAX: - Logger.warning(" This number {} will lead to overflow in FP.".format(x)) + Logger.warning(f" This number {x} will lead to overflow in FP.") # if floating point notation, need to convert to fixed point in FP if 'e' in tokens[0]: # conversion to fixed decimal format @@ -241,14 +252,12 @@ def atom_hook(tokens): """ Render atmo. Usefull to change unary minus into neg(exp) at atom level. """ - pass @staticmethod @abstractmethod def equation_hook(tokens): """ Render 'equation' expression for the LaTeX target package. """ - pass @staticmethod @abstractmethod @@ -256,7 +265,6 @@ def function_hook(tokens): """ Modify the moodle function API to conform to the LaTeX target package api. """ - pass class CalculatedParserToFP(CalculatedParser): @@ -317,14 +325,14 @@ def function_hook(tokens): # remove () out = FP_EVAL_FUNCTION['pi'] # other name are just rename - elif tokens.name in FP_EVAL_FUNCTION.keys(): + elif tokens.name in FP_EVAL_FUNCTION: out[0] = FP_EVAL_FUNCTION[tokens.name] # check that only valid expression are used elif tokens.name in FP_UNSUPPORTED: - Logger.error("Unsupported *function* '{}' by `fp` package in the expression.".format(tokens.name)) + Logger.error(f"Unsupported *function* '{tokens.name}' by `fp` package in the expression.") out = tokens else: - Logger.error("Unsupported *function* '{}' in the expression.".format(tokens.name)) + Logger.error(f"Unsupported *function* '{tokens.name}' in the expression.") out = tokens # print('In hook end :' , out) return out @@ -412,7 +420,7 @@ def variable_hook(wildcards, tokens): @staticmethod def atom_hook(tokens): """ Change unary minus into neg(exp) at atom level. - """ + """ return tokens @staticmethod @@ -458,14 +466,14 @@ def function_hook(tokens): out = tokens.asList() out[1][1], out[1][3] = out[1][3], out[1][1] # other name are just rename - elif tokens.name in MDL_FUNCTION.keys(): + elif tokens.name in MDL_FUNCTION: out[0] = MDL_FUNCTION[tokens.name] # check that only valid expression are used elif tokens.name in MDL_UNSUPPORTED: - Logger.error("Unsupported *function* '{}' by `moodle` interpreter in the expression.".format(tokens.name)) + Logger.error(f"Unsupported *function* '{tokens.name}' by `moodle` interpreter in the expression.") out = tokens else: - Logger.error("Unsupported *function* '{}' in the expression.".format(tokens.name)) + Logger.error(f"Unsupported *function* '{tokens.name}' in the expression.") out = tokens return out @@ -490,7 +498,7 @@ def CreateCalculatedParser(ptype): try: return PARSER_FACTORY[ptype]() # *args,**kwargs) except: - raise KeyError(" 'qtype' argument should be in {}".format(PARSER_FACTORY.keys() ) ) + raise KeyError(f" 'qtype' argument should be in {PARSER_FACTORY.keys()}" ) if __name__=="__main__": # Basic example of usage @@ -508,5 +516,5 @@ def CreateCalculatedParser(ptype): print('> amc2moodle:\n', out) print('> wildcards set:', parser.wildcards) - + diff --git a/amc2moodle/utils/customLogging.py b/amc2moodle/utils/customLogging.py index 45c3b2c..caf0242 100644 --- a/amc2moodle/utils/customLogging.py +++ b/amc2moodle/utils/customLogging.py @@ -4,7 +4,7 @@ # DEFAULT DEFAULT_LOG_BASIC_FORMATTER = '[%(levelname)-8s]: %(message)s ' DEFAULT_LOG_VERBA_FORMATTER = '%(relativeCreated)dms [%(levelname)-8s]: %(message)s ' -DEFAULT_LOG_VERBB_FORMATTER = lambda x: '%(relativeCreated)dms - ({}-%(name)s) [%(levelname)-8s]: %(message)s '.format(x) +DEFAULT_LOG_VERBB_FORMATTER = lambda x: f'%(relativeCreated)dms - ({x}-%(name)s) [%(levelname)-8s]: %(message)s ' DEFAULT_LOG_VERBC_FORMATTER = '%(relativeCreated)dms - (%(name)s) [%(levelname)-8s]: %(message)s ' DEFAULT_FMT_TIME = '%H:%M:%S' diff --git a/amc2moodle/utils/flatex.py b/amc2moodle/utils/flatex.py index 7be029a..b06d896 100644 --- a/amc2moodle/utils/flatex.py +++ b/amc2moodle/utils/flatex.py @@ -25,9 +25,9 @@ See http://www.gnu.org/licenses/gpl.txt for details. """ +import logging import os import re -import logging # activate logger Logger = logging.getLogger(__name__) @@ -71,7 +71,7 @@ def __init__(self, base_file, output_file, @staticmethod def is_input(line): - """ + r""" Determines whether or not a read in line contains an uncommented out \input{} statement. Allows only spaces between start of line and '\input{}'. @@ -115,7 +115,7 @@ def expand_file(self, base_file, current_path): with all the inputs replaced with the contents of the referenced file. """ output_lines = [] - with open(base_file, "r") as f: + with open(base_file) as f: for line in f: # test if it contains an '\include' or '\input' if self.is_input(line): @@ -153,6 +153,4 @@ def report(self): """ Print log info about the expansion. """ if self._magic_comments_number > 0: - Logger.info(' {0} magic comments found, in {1} tex files.'.format( - self._magic_comments_number, - len(self._included_files_list))) + Logger.info(f' {self._magic_comments_number} magic comments found, in {len(self._included_files_list)} tex files.') diff --git a/amc2moodle/utils/misc.py b/amc2moodle/utils/misc.py index 4da0cfe..7a14095 100644 --- a/amc2moodle/utils/misc.py +++ b/amc2moodle/utils/misc.py @@ -22,6 +22,7 @@ import inspect import os + def check_hash(file1, file2): """ Return the md5 sum after removing all withspace. diff --git a/amc2moodle/utils/text.py b/amc2moodle/utils/text.py index 4fee771..3c48e65 100755 --- a/amc2moodle/utils/text.py +++ b/amc2moodle/utils/text.py @@ -18,8 +18,8 @@ along with this program. If not, see . """ -import unicodedata import re +import unicodedata def remove_accent(s): @@ -30,7 +30,7 @@ def remove_accent(s): Based on Python cookbook 2.9 """ nfkd_form = unicodedata.normalize('NFKD', s) - return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) + return "".join([c for c in nfkd_form if not unicodedata.combining(c)]) def remove_non_ascii(s): diff --git a/docker/Dockerfile b/docker/Dockerfile index 5fe6635..6c93cb0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,21 +13,22 @@ ENV VENVDIR=/tmp/venv SHELL ["/bin/bash", "-c"] # install debian packages -RUN apt update && \ - apt install -yy wget \ +RUN apt-get update && \ + apt-get install -yy wget \ ghostscript \ imagemagick \ libtext-unidecode-perl \ latexml \ xmlindent \ + logrotate \ python3-pip \ python3-venv \ git && \ - apt clean &&\ + apt-get clean &&\ rm -rf /var/lib/{apt,dpkg,cache,log}/ # move policy file -RUN mv /etc/ImageMagick-6/policy.xml /etc/ImageMagick-6/policy.xml.off +# RUN mv /etc/ImageMagick-6/policy.xml /etc/ImageMagick-6/policy.xml.off # install pip and Python pkg WORKDIR /tmp @@ -36,8 +37,8 @@ WORKDIR ${INSTALL_DIR_A2M} RUN mkdir -p ${VENVDIR} && \ python -m venv ${VENVDIR} ENV PATH="${VENVDIR}/bin:$PATH" -RUN pip install -U pip && \ - pip install '.[test]' +RUN pip3 install -U pip && \ + pip3 install '.[test]' # check if amc2moodle and moodle2amc work WORKDIR / @@ -65,7 +66,9 @@ WORKDIR ${MONITOR_DIR} # copy autorun script for amc2moodle/moodle2amc COPY autorun-amc2moodle.sh /tmp/. -#RUN ["/tmp/autorun-amc2moodle.sh",">","/dev/null","2>&1&"] +# RUN ["/tmp/autorun-amc2moodle.sh"] +#,">","/dev/null","2>&1&"] + # execute script ENTRYPOINT ["/tmp/autorun-amc2moodle.sh",">","/dev/null","2>&1&"] diff --git a/docker/README.md b/docker/README.md index b373c95..25f02d3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -15,19 +15,28 @@ The container proposes to mount two volumes: - `/tmp/work` (**mandatory mount**) that must be link to a specific folder on the host computer - `/tmp/daemon` (optional mount) that store the log file of the daemon -For instance, the container can be run in CLI (in detached mode) with the following command: +For instance, the container can be run in CLI (in detached mode) with the following command (*by relacing `DIRA` and if necessary `DIRB`): ``` docker run --name amc2moodle -d -v "DIRA:/tmp/work" -v "DIRB:/tmp/daemon" ghcr.io/nennigb/amc2moodle ``` NB: `DIRA` and `DIRB` must be absolute paths. -**it is recommended to use only empty folder with no critical contents for `DIRA` and `DIRB`.** +**it is strongly recommended to use only empty folder with no critical contents for `DIRA` and `DIRB`.** + +In case of permission issue to mount folder(s), consider adding option `--user "$(id -u):$(id -g)"` such as the running command is: + +``` +docker run --name amc2moodle --user "$(id -u):$(id -g)" -d -v "DIRA:/tmp/work" -v "DIRB:/tmp/daemon" ghcr.io/nennigb/amc2moodle +``` + The container can be stopped using the following command ``` docker stop amc2moodle ``` + + # Usage as daemon/server (automatic building) We assume that `DIRA` has been mounted and bound to `/tmp/work`. diff --git a/docker/autorun-amc2moodle.sh b/docker/autorun-amc2moodle.sh index 09cacbd..b5e70f6 100755 --- a/docker/autorun-amc2moodle.sh +++ b/docker/autorun-amc2moodle.sh @@ -18,25 +18,43 @@ fi # function to run amc2moodle function run_amc2moodle() { - amc2moodle -x --silent "$1" + amc2moodle -x "$1" touch "$1".lock } # function to run moodle2amc function run_moodle2amc() { - moodle2amc --silent "$1" + moodle2amc "$1" touch "$1".lock } +# function to initialize logrotate +function init_logrotate() +{ + echo -e "${LOGFILE}{\n\tmissingok\n\trotate 3\n\tmaxsize 5M\n\tcopytruncate\n\tnotifempty\n}" > ${LOG_DIR}/logrotate.conf + cat ${LOG_DIR}/logrotate.conf + touch ${LOG_DIR}/logrotate.status +} + +# function run logrotate periodically +function run_logrotate() +{ + while true + do + sleep 60 + logrotate -s ${LOG_DIR}/logrotate.status ${LOG_DIR}/logrotate.conf + done +} #log manage now=$(date +"%Y-%m-%d_%H-%M-%s") mkdir -p ${LOG_DIR} LOGFILE=${LOG_DIR}/${now}_autorun.log +init_logrotate +run_logrotate & { - printf "Start daemon\n\n" printf "Log dir: ${LOG_DIR}\n" printf "Monitored dir: ${MONITOR_DIR}\n" @@ -56,6 +74,7 @@ do else printf " >>> Run amc2moodle on ${file}\n" run_amc2moodle "${file}" + printf " >>>>Done" fi done fi @@ -73,6 +92,7 @@ do else printf " >>> Run moodle2amc on ${file}\n" run_moodle2amc "${file}" + printf " >>>>Done" fi done fi diff --git a/pyproject.toml b/pyproject.toml index ba95065..a7cb27b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ requires-python='>=3.8' [project.optional-dependencies] test = ["pytest", "pytest-cov[all]"] -lint = [ "black", "flake8"] + [project.urls] Homepage = "https://github.com/nennigb/amc2moodle" @@ -56,3 +56,10 @@ command_line = "-m unittest discover -s amc2moodle/tests/" [tool.coverage.html] directory = "coverage_html_report" + +[tool.ruff] +# Allow lines to be as long as 120. +line-length = 120 + +[tool.ruff.format] +quote-style = "single" \ No newline at end of file