Jason Stitt

Test a Python project and an associated library with Tox

The problem: You want to test a Python project that specifies its dependencies via a requirements.txt, and one of those dependencies is an unreleased package. And you want to automate the creation of virtualenv testing environments, perhaps across different Python versions, so you’re using Tox. But how does Tox know how to install your dependency?

First of all, let’s go over how to get Tox to use your requirements.txt at all. The basic example of Tox usage looks like this:

## content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py26,py27
[testenv]
deps=pytest       # install pytest in the venvs
commands=py.test  # or 'nosetests' or ...

The deps argument here is a package name, or list of package names. And you can see from the comment that this is meant to be used with a package that has a setup.py. Tox will actually call setup.py sdist on your package by default — but if you’re using requirements.txt it’s probably because you’re not making a Python package. Let’s change this configuration:

[tox]
envlist=py27,py34
usesdist=false
[testenv]
deps=
    -r{toxinidir}/requirements.txt
    pytest
commands=py.test

Besides changing to a more reasonable list of Python versions (did you spot that?) we’re also telling Tox not to bother trying to run setup.py sdist (as it doesn’t exist) and to read our dependencies from requirements.txt. See that deps argument? It’s actually being passed right through to pip, so the -r syntax is just the pip command-line argument.

But there’s still a problem. Remember how we have a dependency on an in-development unreleased package? When pip tries to read our requirements.txt, it’s not going to be able to find that and the whole build will fail.

It turns out that Tox has a feature that saves us here, which I didn’t know about until very recently. When you test a package with Tox, it saves a zip file of the distribution to the $HOME/.tox/distshare/ directory. So if you test your dependency first (with Tox), you can then install that distribution as part of your other project’s tests, just by referring to its location using the {distshare} variable.

It should look something like this…

deps=
    {distshare}/mylibrary-*.zip
    -r{toxinidir}/requirements.txt
    pytest

… but that won’t work.

The reason is that pip goes ahead and reads your requirements.txt before it installs the first package, so it will still fail. In order to fix this, you need to break out two separate runs of pip, like this:

deps=
    {distshare}/mylibrary-*.zip
    pytest
commands=
    pip install -qr{toxinidir}/requirements.txt
    pytest

Now, when pip runs the second time, your library is already installed, which means the requirement is already satisfied and won’t be searched for in the package index.

© 2009-2024 Jason Stitt. These are my personal views and don't represent any past or present employer.