Writing a moulinette with external Python modules

In some cases, the inspection steps of a moulinette might want to perform advanced checks, using tools that are not provided as built-ins by Quixote.

Let’s take an example for which we want to inspect a student’s delivery containing an HTML file, and check whether it has a <title> tag.

For that, we can use the BeautifulSoup Python module, which allows analyzing HTML files. However, that module is not part of the Python Standard Library, and thus must be installed manually. This tutorial will explain how Quixote helps addressing these issues.

Installing the module

Regarding the installation of the module, we will of course create a builder step, and use the quixote.build.pip built-in.

import quixote.build.pip as pip

@quixote.builder
def install_beautifulsoup():
    pip.install("beautifulsoup4")

Using the module

The problem

Naively, we would use the installed module simply by importing it at the top of our blueprint.py file, as we already do with regular modules. However, that does not work since Python first needs to run the builder steps to be able to import BeautifulSoup (otherwise, the module is not yet installed !).

This section provides two different solutions to this problem.

Solution 1: Using local imports

The first solution is to use local imports, that is, import statements inside functions instead of at the global scope. Thus, the modules will only be loaded when the function is executed.

Unlike the previous solution, this one allows keeping the whole moulinette inside a single file.

import quixote
from quixote import get_context
from quixote.inspection.check import assert_true, fail

@quixote.inspector
def check_title():
    from bs4 import BeautifulSoup # Local import

    try:
        delivery_path = get_context()["delivery_path"]
        with open(f"{delivery_path}/index.html", 'r') as f:
            data = f.read()
    except (OSError, IOError) as e:
        fail("unable to open the file")

    soup = BeautifulSoup(data, features="html.parser")
    assert_true(soup.title is not None, "no <title> tag found")

Note

Note that using this technique, the imported modules will only be available in the functions that imported them.

[Deprecated] Solution 2: Using an inspection_file

Another solution is to put the code using BeautifulSoup in a dedicated file, that will be loaded only after the dependencies are installed.

For our example, we will then create a file called inspectors.py, in which we write code that uses BeautifulSoup:

import quixote
from quixote import get_context
from quixote.inspection.check import assert_true, fail
from bs4 import BeautifulSoup

@quixote.inspector
def check_title():
    try:
        delivery_path = get_context()["delivery_path"]
        with open(f"{delivery_path}/index.html", 'r') as f:
            data = f.read()
    except (OSError, IOError) as e:
        fail("unable to open the file")

    soup = BeautifulSoup(data, features="html.parser")
    assert_true(soup.title is not None, "no <title> tag found")

The last thing we need to do now is to tell Quixote about our new file. This can be achieved when creating the Blueprint object in the blueprint.py file, by specifying the name of the file as value for the inspection_file parameter.

blueprint = quixote.Blueprint(
    name="using_beautifulsoup",
    author="Clément Doumergue",
    inspection_file="inspectors.py"
)

Wrap-up

You are free to choose either of the two solutions explained above. If you want to play with them, you can download both, along with test deliveries here.