tox is a great tool for standardising and automating any development task that
benefits from being run in an isolated virtualenv. But tox suffers from an
educational problem: tox appears to be just for running tests, and that’s all that
many projects use it for. It can also be unclear how best to use the tox.ini
file format at first. This tutorial aims to clear up the confusion and teach
you how to automate a project’s development tasks with tox. It covers
everything you need to know to understand the real hypothesis/h tox.ini file.
Why tox is confusing at first
tox can appear to be just a tool for running tests, rather than for development tasks in general:
-
tox was originally for running your tests in an isolated virtual environment, but has grown into a more general project virtualenv / command management tool.
-
For many developers their first introduction to tox will be when they come across it being used in some project to run the tests, while the project confusingly uses manually-created virtualenvs for other things that tox could be used for, like building the documentation and running the development server.
- tox’s own documentation is unclear on the scope of tox too, stating tox’s vision as
to automate and standardize testing in Python
. But the same docs later describe tox asa generic virtualenv management and test command line tool
and include examples of using tox to build documentation and to run development environments. tox’s GitHub project’s description isCommand line driven CI frontend and development task automation tool
. - Internal terminology used by tox also suggests that it’s just for testing:
tox refers to the virtualenvs that it creates as
testenvs
. We’ll be using bothtestenv
andvirtualenv
in this guide too. Eventually we’ll get around to explaining exactly what a testenv is relative to a virtualenv.
In addition:
-
The
tox.ini
file format is just complicated enough that it requires some knowledge of how tox works to understand how to write a good one. -
The relationship between tox and similar tools such as Docker, GNU Make, virtualenv, virtualenvwrapper, etc is unclear.
So what is tox and what is it for?
tox is a project virtualenv management and development task automation tool.
You create a tox.ini
file for your project, and in this file you define
virtualenvs (tox calls them testenvs
) for all your project tasks that
you want to standardize and automate with tox: running the tests, running the
development server, running the linters, building the documentation, publishing
a release, etc. Here’s an example tox.ini
file that’ll be explained in detail later:
[tox]
envlist = tests
skipsdist = true
[testenv]
deps =
tests: -r requirements.txt
lint: flake8
docs: sphinx-autobuild
commands =
tests: pytest tests/
lint: flake8 src
docs: sphinx ...
You run tox telling it which of your testenvs to run, e.g. tox -e docs
to
build the documentation or tox -e tests,lint
to run the tests followed by
the linter, and tox creates a virtualenv, installs the
dependencies that you’ve listed in tox.ini
into it, and runs the commands
that you’ve listed in tox.ini
in it.
tox’s workflow
More concretely, tox is a tool that automates a certain workflow, as described in the System overview in the tox docs:
- Read tox config settings from a
tox.ini
file, command line options and environment variables. - Optionally create a Python package (sdist) of your project. (We skip this step for Hypothesis projects.)
- Create a Python virtual environment using the version of Python selected in
tox.ini
. - Install any Python dependencies listed in
tox.ini
(thedeps
setting) into the virtualenv. If ansdist
of your project was created in step 2 install that too. - Run the commands listed in
tox.ini
(thecommands
setting) in the virtualenv. By default fail if any of the commands exit with a non-zero exit code. - Print out a report of the virtualenvs that were run and whether each succeeded or failed.
So tox is really just a tool for creating virtualenvs and running commands in them, and any development task that can be automated by running commands in virtualenvs can be automated with tox.
If you ask tox to run more than one virtualenv at once it will loop repeating steps 3, 4 and 5 for each virtualenv, and finally report on them all in step 6.
If you run the same virtualenv again tox will speed things up by reusing the previously created virtualenv. It’ll skip
steps 3 and 4 and avoid unnecessarily recreating the virtualenv or reinstalling the dependencies unless you pass the
tox --recreate
option or unless tox knows that the virtualenv needs to be recreated (for example because the deps
in
tox.ini
have changed).
Benefits of using tox
By defining all of your project’s tasks in a tox.ini
file tox simplifies things
for developers. A developer only needs to install and run tox, they don’t need
to bother with creating and activating virtualenvs and installing dependencies themselves.
tox also simplifies CI integration: CI scripts that just run tox are much simpler than scripts that handle virtualenvs and dependencies themselves.
tox also standardizes things, reducing differences between different
development environments and between dev envs and CI and production.
The tox.ini
file defines exactly what dependencies to install into each
virtualenv and tox takes extra steps to isolate its virtualenvs from the
outside system.
tox can also reduce the chance of dependency conflicts by using a separate virtualenv for each task, rather than installing everything in one big virtualenv. The packages needed to build your documentation or run your linters don’t need to be installed in the virtualenv that runs your dev server.
Finally, tox provides one place to define and document all of the
available projects tasks – in the tox.ini
file – with a simple and
consistent command line interface for running those tasks.
I call tox project
virtualenv management because it uses a per-project
configuration file, the tox.ini
file, that you add to your project’s version
control repository. This is different than other tools, such as
virtualenvwrapper, that are more personal-use virtualenv managers. Your text
editor is your personal text editing tool and text editor configuration or
standardizing on one particular text editor isn’t usually part of a project.
GNU Make on the other hand is a project build automation tool because its
Makefile
is tracked as part of the project. tox is a project tool, like Make.
tox’s original use-case
The original purpose of tox was to run tests in isolated environments and (later) to
automate running tests with multiple combinations of different versions of
Python and different versions of different dependencies. It uses some more
recent tox features, but this example partial tox.ini
file for
Compressing a dependency matrix
from tox’s docs illustrates tox’s originally intended use-case:
[tox]
envlist = py{27,34,36}-django{15,16}-{sqlite,mysql}
[testenv]
deps =
django15: Django>=1.5,<1.6
django16: Django>=1.6,<1.7
py34-mysql: PyMySQL ; use if both py34 and mysql are in an env name
py27,py36: urllib3 ; use if any of py36 or py27 are in an env name
py{27,36}-sqlite: mock ; mocking sqlite in python 2.x
Running the command tox
with this tox.ini
file will generate the “cross product”
of the different vetsions of Python, Django and SQLite or MySQL and run the project’s
tests many times over: with Python 2.7, Django 15, and SQLite, then with Python 2.7,
Django 15 and MySQL, then with Python 2.7, Django 16, and SQLite, and so on,
for all combinations. More on this “generative envlist” feature
below. The deps
setting defines how the dependencies to be
installed should change depending on the versions of Python and Django being
tested and on SQLite or MySQL.
We don’t have a need at Hypothesis to test our code against many different versions like this. We’re writing a web application that gets deployed to a production environment that we control, with pinned versions of Python and all dependencies. We use tox instead for its isolation and task automation features.
Where we’re going: our use-case for tox
Here’s an excerpted version of Hypothesis’s tox.ini
file.
It defines a few different testenvs named dev
(for running the dev server),
tests
(for running the tests), docs
(for building the docs), etc. One
testenv for each development task. And tells tox what dependencies to install
and what commands to run in each testenv. The rest of this tutorial will cover
everything you need to understand this tox.ini
file in detail.
[tox]
envlist = tests
skipsdist = true
requires = tox-pip-extensions
tox_pip_extensions_ext_venv_update = true
[testenv]
skip_install = true
deps =
dev: -r requirements-dev.in
tests: -r requirements.txt
lint: flake8
docs: sphinx-autobuild
whitelist_externals =
dev: sh
commands =
dev: sh bin/hypothesis devserver
tests: pytest -Werror tests/h/
lint: flake8 h
docs: sphinx-autobuild -BqT -b dirhtml -d {envtmpdir}/doctrees . {envtmpdir}/html
Installing tox
tox is like both GNU Make and your text editor in that you should install tox system-wide and use that one single copy of tox for each project that you work on. You don’t install tox in your project’s virtualenv, or list it as a dependency in your project’s requirements files. tox is the thing that creates your project virtualenvs and installs their dependencies for you.
On Ubuntu, just install tox once system-wide and be done with it:
$ sudo pip install tox
Getting started: a minimal tox.ini
file
Here’s the smallest possible tox.ini
file that will run without crashing:
[tox]
skipsdist = true
If you save this as tox.ini
in an empty directory and then run the command
tox
in that directory you should see something like this:
$ tox
python create: /home/seanh/Dropbox/Desktop/tox/.tox/python
python run-test-pre: PYTHONHASHSEED='3201412928'
__________________________________________________________________ summary ___________________________________________________________________
python: commands succeeded
congratulations :)
$
tox created a virtualenv (at .tox/python
) using the default version of Python
on your system (Python 2.7 on my Ubuntu system). Since the tox.ini
file
didn’t list any dependencies or commands tox didn’t install any dependencies or
run any commands in the virtualenv. It just did nothing.
The skipsdist = true
tells tox not to try and build an sdist (source distribution) of your project.
An sdist is a way of packaging up a Python project so that it can be installed
on a system, published to https://pypi.org/, etc. Since our project at this
point is an empty directory there’s nothing to package up. At Hypothesis we
don’t build and publish installable Python packages anyway – we build web apps
that we deploy to production environments – so we always use
skipsdist = true
.
Commands
Lets add a command to our tox.ini
file using the commands
setting.
We’ll make it print out the version of Python being used:
[tox]
skipsdist = true
[testenv]
commands = python --version
Now if you run tox
you’ll see the output of our command that it ran in the virtualenv:
$ tox
python run-test-pre: PYTHONHASHSEED='3697789845'
python runtests: commands[0] | python --version
Python 2.7.15rc1
__________________________________________________________________ summary ___________________________________________________________________
python: commands succeeded
congratulations :)
$
We’ve now split the tox.ini
file into two sections [tox]
and [testenv]
.
The [tox]
section contains global settings
that affect the global behavior of tox itself, such as whether or not to build
an sdist, the minimum version of tox that this tox.ini
file requires, etc.
The [testenv]
section defines the testenv(s) that we want tox to create for
us: what versions of Python to use to create the testenvs, what dependencies to
install into them, what environment variables, what commands to run in the
testenvs, etc.
We’re gonna cover the most important settings that you can put in the
[testenv]
section of your tox.ini
file, starting with commands
. For full
documentation of all the available settings run tox --help-ini
or see
the docs.
External commands and whitelist_externals
One of the things that tox does to isolate testenvs from the system is to
prepend the virtuenv’s bin
directory onto the PATH
envvar in the subshell
that the testenv’s commands are run in. You can see this by having tox run a
command that prints out the value of PATH
:
[tox]
skipsdist = true
[testenv]
commands = sh -c "echo $PATH"
$ tox
python run-test-pre: PYTHONHASHSEED='3100919638'
python runtests: commands[0] | sh -c 'echo $PATH'
WARNING: test command found but not installed in testenv
cmd: /bin/sh
env: /home/seanh/Dropbox/Desktop/tox/.tox/python
Maybe you forgot to specify a dependency? See also the whitelist_externals envconfig setting.
/home/seanh/Dropbox/Desktop/tox/.tox/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
__________________________________________________________________ summary ___________________________________________________________________
python: commands succeeded
congratulations :)
$
The PATH
envvar in the testenv’s subshell is my usual PATH
envvar but with
the .tox/python/bin
dir prepended to the front. This is so that executables
in the virtualenv’s bin
are always chosen instead of any executables with the
same name elsewhere on the system PATH
. This is why when we’ve been putting
python --version
in our tox.ini
file its been printing out the version of
Python installed in the virtualenv instead of the system version of Python:
it’s running the copy of python
at .tox/python/bin/python
. Similarly pip
in a tox.ini
file (or any file run by a tox.ini
file) will be the
virtualenv’s copy of pip, and the same goes for any executables that your
package installs.
Notice that tox printed out a test command found but not installed in testenv
warning. This is because the tox.ini
file is running the executable sh
which is a system executable – there’s no sh
in the virtualenv. By default
tox allows executables from the system PATH
to be run if they aren’t shadowed
by a virtualenv executable, but it prints this warning. We can silence it with
the whitelist_externals
setting, which is a list of allowed system executables:
[tox]
skipsdist = true
[testenv]
whitelist_externals = sh
commands = sh -c "echo $PATH"
Now tox will run sh
without a warning:
$ tox
python run-test-pre: PYTHONHASHSEED='3100919638'
python runtests: commands[0] | sh -c 'echo $PATH'
/home/seanh/Dropbox/Desktop/tox/.tox/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
__________________________________________________________________ summary ___________________________________________________________________
python: commands succeeded
congratulations :)
$
Running multiple commands in sequence
You can easily have tox run a sequence of commands in order by using multiple
commands in the commands
setting, one command per line:
[tox]
skipsdist = true
[testenv]
whitelist_externals = echo
commands =
echo This is the first command
echo This is the second command
echo This is the third command
Many settings in tox can take a list of lines like this, including
whitelist_externals
, deps
, passenv
and setenv
, etc.
Command exit codes and tox’s exit code
If one of the commands exits with a non-zero exit code tox will stop there
and not continue on to the next commands. For example the UNIX command false
exits with code 1. This tox.ini
file will stop at false
and not continue
on to the final command:
[tox]
skipsdist = true
[testenv]
whitelist_externals =
echo
false
commands =
echo This is the first command
false
echo This is the third command
tox will move on to the next testenv and run that though, if multiple
testenvs are given in envlist
/ -e
/ TOXENV
setting.
When a command from any one of the testenvs run exits with nonzero then tox itself will also exit with nonzero, whereas if all commands exited with zero tox will exit with zero.
You can tell tox to ignore the exit code of a command – continuing on to the
next command even if that command exits with zero, and not failing the tox
command itself if that command fails – by prepending the command with a -
.
This tox.ini
file will run all three commands and exit with zero:
[tox]
skipsdist = true
[testenv]
whitelist_externals =
echo
false
commands =
echo This is the first command
-false
echo This is the third command
Passing command line arguments to commands using posargs
You can use tox’s posargs
substitution to allow the user to pass command line
arguments through to the command(s) that tox is running. This is particularly
useful when using tox to run a test runner, such as pytest
, that has many
useful command line arguments. The user can run tox -- -x
to run pytest
with pytest’s -x
option, for example.
Here’s a tox.ini
file that installs and runs pytest
and passes any command
line args through:
[tox]
skipsdist = true
[testenv]
deps = pytest
commands = pytest {posargs} tests/
(Here we’re using the deps
setting to tell tox to install pytest
into the
virtualenv before running the command(s). More on deps
later.)
If you just run tox
this will have pytest run all the tests in your tests/
dir (it’ll run pytest tests/
). If you want to pass any command line arguments
to pytest you put them after a --
in the tox
command. If no --
is given
then {posargs}
evaluates to the empty string. For example to have pytest
print out all its command line arguments and options:
$ tox -- --help
You can also give a default value for posargs
following a :
within the {…}
.
When no --
is used on the command line the default value will be substituted in place of
the {posargs:DEFAULT_VALUE}
. For example this will run pytest tests/
by
default but allow the user to narrow down what tests to run with a command like
tox -- tests/foo/bar
:
commands = pytest {posargs:tests/}
{posargs}
can be used anywhere in in tox.ini
and can even be used multiple
times to pass the same positional arguments to multiple commands.
{posargs}
is an example of a tox substitution, more on substitution later.
envlist
So far tox has just been creating a single virtualenv for us named python
,
using the system’s default version of Python. By adding the envlist
setting
we can get it to create virtualenvs using different versions of Python and to
create and run multiple testenvs at once:
[tox]
skipsdist = true
envlist = py36
[testenv]
commands = python --version
If you run tox
with this tox.ini
file it’ll create a .tox/py36
virtualenv and run the commands using Python 3.6:
$ tox
py36 create: /home/seanh/Dropbox/Desktop/tox/.tox/py36
py36 run-test-pre: PYTHONHASHSEED='2938378655'
py36 runtests: commands[0] | python --version
Python 3.6.6
__________________________________________________________________ summary ___________________________________________________________________
py36: commands succeeded
congratulations :)
$
This requires Python 3.6 to be installed on the system where tox
is run. You
need Python 3.6 to create a Python 3.6 virtualenv. The envlist
setting is a
comma-separated list of testenvs that will be run when you run tox
. If you
want to run your commands in multiple versions of Python at once you can just
give an envlist
with multiple Pythons:
[tox]
skipsdist = true
envlist = py27,py36,py37
[testenv]
commands = python --version
tox
will create py27
, py36
and py37
virtualenvs in .tox
and run your
commands in each of them in turn (in my case the Python 3.7 testenv fails
because I don’t have Python 3.7 on my system):
$ tox
py27 create: /home/seanh/Dropbox/Desktop/tox/.tox/py27
py27 run-test-pre: PYTHONHASHSEED='3564108623'
py27 runtests: commands[0] | python --version
Python 2.7.15rc1
py36 run-test-pre: PYTHONHASHSEED='3564108623'
py36 runtests: commands[0] | python --version
Python 3.6.6
py37 create: /home/seanh/Dropbox/Desktop/tox/.tox/py37
ERROR: InterpreterNotFound: python3.7
__________________________________________________________________ summary ___________________________________________________________________
py27: commands succeeded
py36: commands succeeded
ERROR: py37: InterpreterNotFound: python3.7
$
The envlist
setting is the default testenv(s) to run when tox
is run with
no -e
argument. envlist
can be overridden using the -e
command line
argument. tox -e py27,py36
will run the py27
and py36
testenvs
regardless of what the envlist
setting in tox.ini
says.
You can also override envlist
by setting the TOXENV
environment variable,
and you can set the envvsr TOX_SKIP_ENV
to a regular expression and any testenvs
that match the regex will be skipped.
py27
, py36
, etc are builtin “factors” that set the version of Python to
use. These’ll be explained properly later,
for now they’ll have to remain a little mysterious.
Dependencies
You can have tox install the dependencies that your commands need into the
virtualenvs before running the commands, using the deps
setting
in tox.ini
. For example this tox.ini
file will install pytest
and all
the requirements listed in your app’s requirements.txt
file and then run
pytest
:
[tox]
skipsdist = true
[testenv]
deps =
pytest
-r requirements.txt
commands = pytest {posargs:tests/}
If the deps
setting in your tox.ini
file changes tox will automatically
recreate the virtualenv and reinstall the dependencies the next time you run it.
By default, though, tox doesn’t detect changes to dependencies listed in files when things like
-r requirements.txt
are used. You would have to tell tox to recreate the virtualenv (with the tox -r
option)
whenever requirements.txt
changes. Fortunately the venv_update
extension, part of the
tox-pip-extensions
package, automates this.
Environment variables
Another thing tox does to help isolate testenvs from the system is that it
removes most of your shell’s environment variables from the subshell that it
runs the testenv commands in. You can see this by having a tox.ini
file print
out all the environment variables available in the testenv using the standard
UNIX env
command:
[tox]
skipsdist = true
[testenv]
whitelist_externals = env
commands = env
The output of running tox
will be something like this:
$ tox
python run-test-pre: PYTHONHASHSEED='3478754311'
python runtests: commands[0] | env
LANG=en_GB.UTF-8
VIRTUAL_ENV=/home/seanh/Dropbox/Desktop/tox/.tox/python
TOX_WORK_DIR=/home/seanh/Dropbox/Desktop/tox/.tox
TOX_ENV_NAME=python
TOX_ENV_DIR=/home/seanh/Dropbox/Desktop/tox/.tox/python
PYTHONHASHSEED=3478754311
PATH=/home/seanh/Dropbox/Desktop/tox/.tox/python/bin:...
___________________________________ summary ____________________________________
python: commands succeeded
congratulations :)
$
A handful of environment variables are allowed to pass through by default,
including PATH
(with the virtuenv’s bin
dir prepended). You can also see
that
tox injects a few environment variables of its own
that you can make use of, such as TOX_ENV_NAME
(the name of the testenv being run).
If you want to pass more envvars from your shell through, for example to
configure your application, you can do this using the passenv
setting
which is a list of additional envvars that can be set in the environment where
tox
is being run and will be passed in to the testenv where the commands are run:
[testenv]
passenv =
DATABASE_URL
ELASTICSEARCH_URL
PYTEST_ADDOPTS
…
You can also use setenv
to set the values of additional envvars in the tox.ini
file, rather than
reading them from the outside environment:
[testenv]
setenv =
DATABASE_URL = postgresql://postgres@localhost/postgres
ELASTICSEARCH_URL = …
…
Substitutions
As well as being available to the commands that get run, environment variable
values can also be used in the tox.ini
file itself by using tox substitution.
Anywhere in tox.ini
(for example in a dependency, or in a command) you can
use {env:ENVVAR_NAME}
to substitute the value of an environment variable.
For example this tox.ini
file allows you to set the command to be run by setting an environment variable named COMMAND
.
Test it out with a command like env COMMAND="echo hi" tox
:
[tox]
skipsdist = true
[testenv]
passenv = COMMAND
commands = {env:COMMAND}
If no COMMAND
envvar is present tox will exit with an error. You can provide
a default value for this case using a second :
:
commands = {env:COMMAND:pytest}
Substitutions can be nested, which can allow one envvar to be used as a fallback for another.
This will read the command from the COMMAND
envvar or, failing that, from FALLBACK_COMMAND
,
or finally default to pytest
if neither envvar is set:
commands = {env:COMMAND:{env:FALLBACK_COMMAND:pytest}}
As well as environment variables,
tox provides a bunch of builtin variables that can be used in substitution.
For example {envtmpdir}
is the path to the virtuenv’s temporary directory (which is automatically deleted after each run of tox
).
This tox.ini
file uses Sphinx to build a project’s documentation (located in
the docs
dir) and serve it locally for previewing. It uses {envtmpdir}
as
the directory for Sphinx’s output files (the built documentation HTML files) to
avoid messing up the user’s working directory, and so that those files are
automatically cleaned up each run:
[tox]
skipsdist = true
[testenv]
skip_install = true
deps =
sphinx-autobuild
sphinx
sphinx_rtd_theme
changedir = docs
commands =
sphinx-autobuild -BqT -b dirhtml -d {envtmpdir}/doctrees . {envtmpdir}/html
Factors and conditionals
tox’s factors
and the conditional settings they enable are one of the most useful and flexible features
of tox. They’re the basis of our tox.ini
file at Hypothesis.
But to understand them you have to grock a few tox concepts first:
- Testenvs
A tox testenv is a bunch of settings that define how to create and run a virtuenv: what version of Python to use, what dependencies to install, what environment variables to set and to pass through, what commands to run in the venv once it’s ready, etc.
Whenever you run
tox
you’rerunning
one or more testenvs (the testenvs listed in theenvlist
setting,-e
command line argument, orTOXENV
environment variable).- Testenv names are lists of factors
- A testenv name is a minus-separated list of factors.
A testenv named
py27
contains a single factor,py27
. A more complex testenv name likepy27-tests
contains two factorspy27
andtests
. - Factors
A factor is a collection of testenv settings. A testenv like
py27-tests
pulls in all of the settings from thepy27
factor and thetests
factor, and those settings combined become the testenv’s settings.py27
,py36
etc are builtin factors. tox comes with builtin factors for all the versions of Python, and you can just use these in yourtox.ini
files. These are factors that contain a single setting: the version of Python to use to create the virtualenv with (thebasepython
setting).You can also define custom factors in your
tox.ini
file.test
in the testenv namepy27-test
is a custom factor.- Conditionals
Whenever you give a list of things in your
tox.ini
file (for example the lists of environment variables to pass through, dependencies to install, and commands to run) items in the list can be made conditional on a factor by prefixing them withfactorname:
. For example in thistox.ini
snippet:[testenv] deps = tests: pytest
the
tests: pytest
implicitly creates a custom factor namedtests
and tells tox to install thepytest
dependency only when thetests
factor is invoked.tox -e py27-tests
will install pytest buttox -e py27-docs
won’t.
Below is a simplified version of the Hypothesis tox.ini
file
that uses factors and conditionals to define separate testenvs for running the
dev server, running the linter, running the tests, and building the docs. This
enables the following tox
commands:
-
tox
runs the tests in Python 2.7 (py27-tests
is the defaultenvlist
) -
You can run the tests in any version of Python (as long as you have that version of Python installed on your system) by using different testenv names:
tox -e py27-tests
runs the tests in Python 2.7, whereastox -e py36-tests
ortox -e py37-tests
run the tests in Python 3.6 or 3.7. -
tox -e py27-dev
runs the dev server in Python 2.7. As with the tests you can run the dev server in any version of Python using commands liketox -e py36-dev
. -
tox -e py36-lint
runs the linter, andtox -e py36-docs
builds the docs and serves them locally.
As you can see the tox.ini
file is designed to enable pyXY-COMMAND
testenvs, where pyXY
is any version of Python and COMMAND
is any of the
provided commands (tests
, dev
, lint
, etc). It can be extended by adding
as many commands as you like, automating all of your development tasks.
Here’s the tox.ini
file:
[tox]
envlist = py27-tests
skipsdist = true
requires = tox-pip-extensions
tox_pip_extensions_ext_venv_update = true
[testenv]
skip_install = true
passenv =
dev: AUTHORITY
dev: BOUNCER_URL
dev: CLIENT_OAUTH_ID
dev: CLIENT_RPC_ALLOWED_ORIGINS
dev: CLIENT_URL
dev: SENTRY_DSN
dev: USE_HTTPS
dev: WEBSOCKET_URL
tests: TEST_DATABASE_URL
tests: ELASTICSEARCH_URL
tests: PYTEST_ADDOPTS
deps =
dev: ipython
dev: ipdb
dev: -r requirements-dev.in
lint: flake8
lint: flake8-future-import
tests: coverage
tests: pytest
tests: factory-boy
tests: mock
tests: hypothesis
tests: -r requirements.txt
docs: sphinx-autobuild
docs: sphinx
docs: sphinx_rtd_theme
whitelist_externals = dev: sh
changedir = docs: docs
commands =
python --version
dev: sh bin/hypothesis --dev init
dev: sh bin/hypothesis devserver {posargs}
lint: flake8 src tests
tests: pytest {posargs:tests/h/}
docs: sphinx-autobuild -BqT -b dirhtml -d {envtmpdir}/doctrees . {envtmpdir}/html
To explain how this works, consider how tox treats the commands
section of
the tox.ini
file when you run tox -e py36-dev
:
commands =
python --version
dev: sh bin/hypothesis --dev init
dev: sh bin/hypothesis devserver {posargs}
lint: flake8 src tests
tests: pytest {posargs:tests/h/}
docs: sphinx-autobuild -BqT -b dirhtml -d {envtmpdir}/doctrees . {envtmpdir}/html
tox reads the commands
list from top to bottom and runs only the commands
that match the testenv name. python --version
is an unconditional command (no
factor:
prefix), so that always gets run. The two dev:
commands match the dev
factor in py36-dev
so they get run. The other commands
conditions don’t match so they’re ignored. The matching commands are run in the
order that they appear in the file.
This same matching procedure is applied to all lists in the tox.ini
file:
passenv
, deps
, etc. When you run tox -e py36-dev
tox first collects all
the settings in the builtin py36
factor (the Python version: 3.6), then it
parses the tox.ini
file and collects all settings that are either
unconditional or whose condition matches one of the factors py36
or dev
.
These collected settings form the testenv definition, and tox runs it.
More complex conditions are also possible. The most useful of these is to
reduce duplication by making a single line match multiple factors by giving a
comma-separated list of factors in {…}
‘s. This will install
requirements.txt
if either the tests
or the dev
factor is invoked:
deps =
{tests,dev}: -r requirements.txt
Negation, logic, etc are also possible. See Complex factor conditions in the tox docs for details.
Debugging your tox.ini
file with --showconfig
Once you’ve started adding conditionals to your tox.ini
file it can be useful
to have a good way to debug it. Given a tox.ini
file like the one above, and
a command like tox -e py27-dev
, adding the --showconfig
option will get tox
to print out the entire collected py27-dev
testenv definition instead of
running the testenv’s commands:
$ tox -e py27-dev --showconfig
tool-versions: tox-3.5.2 virtualenv-16.0.0
config-file:
toxinipath: /home/seanh/Projects/h/tox.ini
toxinidir: /home/seanh/Projects/h
toxworkdir: /home/seanh/Projects/h/.tox
setupdir: /home/seanh/Projects/h
distshare: /home/seanh/.tox/distshare
skipsdist: True
[testenv:py27-dev]
envdir = /home/seanh/Projects/h/.tox/py27-dev
setenv = SetenvDict: {'TOX_ENV_NAME': 'py27-dev', 'TOX_ENV_DIR': '/home/seanh/Projects/h/.tox/py27-dev', 'PYTHONHASHSEED': '2540689914'}
basepython = python2.7
description =
envtmpdir = /home/seanh/Projects/h/.tox/py27-dev/tmp
envlogdir = /home/seanh/Projects/h/.tox/py27-dev/log
downloadcache = None
changedir = /home/seanh/Projects/h
args_are_paths = True
skip_install = True
ignore_errors = False
recreate = False
passenv = set(['LANG', 'PIP_INDEX_URL', 'LANGUAGE', 'TOX_WORK_DIR', 'SENTRY_DSN', 'CLIENT_URL', 'AUTHORITY', 'CLIENT_RPC_ALLOWED_ORIGINS', 'WEBSOCKET_URL', 'USE_HTTPS', 'BOUNCER_URL', 'CLIENT_OAUTH_ID', 'PATH', 'LD_LIBRARY_PATH', 'TMPDIR'])
whitelist_externals = ['sh']
platform = .*
sitepackages = False
alwayscopy = False
pip_pre = False
usedevelop = False
install_command = [u'pip-faster', u'install', u'{opts}', u'{packages}', 'venv-update>=2.1.3']
list_dependencies_command = ['python', '-m', 'pip', 'freeze']
deps = [ipython, ipdb, -rrequirements-dev.in]
commands = [['python', '--version'], ['sh', 'bin/hypothesis', '--dev', 'init'], ['sh', 'bin/hypothesis', 'devserver']]
commands_pre = []
commands_post = []
ignore_outcome = False
extras = []
Generative envlists
So far we’ve mostly ran one or two testenvs at a time with commands like tox
-e py36-tests
or default envlist
settings like envlist = py27-tests,py36-tests
.
It isn’t much use to us at Hypothesis but for completeness sake tox also has a
Generative envlist
feature that allows you to run many testenvs at once without having to spell
out every single testenv name. This is useful for projects that need to support
a lot of combinations of different versions of Python, different versions of
dependencies, and different platforms at once. A single tox
command can run
your tests in with all the different combinations. Here’s an example from the docs:
envlist = {py27,py36}-django{15,16}, docs, flake
With this envlist
setting a single tox
command will generate the
“cross product” of the different versions of Python and Django and run
each possible combination. It will run all of these testenvs:
py27-django15
py27-django16
py36-django15
py36-django16
docs
flake
Defining testenvs using [testenv:NAME]
sections
So far we’ve had a single [testenv]
section in our tox.ini
and we’ve used
conditionals to define multiple testenvs. There’s also an entirely different
way to define your testenvs in your tox.ini
: using a separate [testenv:NAME]
section for each testenv. Here’s an example of a tox.ini
file that supports
tox
or tox -e py27
to run the tests in Python 2.7, and tox -e py27-dev
and tox -e py36-dev
to run the dev server in Python 2.7 or 3.6:
[tox]
envlist = py27-tests
skipsdist = true
[testenv]
skip_install = true
[testenv:py27-tests]
description = Run the tests in Python 2.7
deps =
coverage
mock
…
-rrequirements.txt
passenv =
TEST_DATABASE_URL
ELASTICSEARCH_URL
PYTEST_ADDOPTS
commands =
coverage run --parallel --source h,tests/h -m pytest -Werror {posargs:tests/h/}
[dev]
deps = -rrequirements-dev.in
passenv =
ALLOWED_ORIGINS
AUTHORITY
BOUNCER_URL
…
whitelist_externals = sh
commands =
sh bin/hypothesis --dev init
{posargs:sh bin/hypothesis devserver}
[testenv:py27-dev]
description = Run the dev server in Python 2.7
deps = {[dev]deps}
passenv = {[dev]passenv}
whitelist_externals = {[dev]whitelist_externals}
commands = {[dev]commands}
[testenv:py36-dev]
description = Run the dev server in Python 3.6
deps = {[dev]deps}
passenv = {[dev]passenv}
whitelist_externals = {[dev]whitelist_externals}
commands = {[dev]commands}
Any settings in the [testenv]
section apply to all testenvs, unless
overridden. You then define each testenv in its own section, for example the
[testenv:py27-tests]
section contains the settings for the py27-tests
env
(tox -e py27-tests
).
The [dev]
section doesn’t define a testenv and isn’t used by tox. It’s just a bag of settings
to be used in substitution by the [testenv:py27-dev]
and [testenv:py36-dev]
sections below.
For example when [testenv:py27-dev]
does deps = {[dev]deps}
that means pull
in all the deps from the [dev]
section. You can also pull in some deps and then add more:
[testenv:py27-dev]
deps =
{[dev]deps}
ipython
ipdb
These are called section substitutions.
Using substitutions like this is a way to avoid duplication. It’s almost like
inheritance, where [testenv:py27-dev]
and [testenv:py36-dev]
are child
sections and [dev]
is the base section they inherit from. But there’s no way
to automatically inherit all the settings from a base section at once: you have
to inherit each section individually with a substitution.
One advantage of writing your tox.ini
file this way is that you can add a
description
to each testenv, and if you run tox -av
it’ll list all the
available testenvs and their descriptions:
$ tox -av
using tox.ini: /home/seanh/Projects/h/tox.ini
using tox-3.5.2 from /usr/local/lib/python2.7/dist-packages/tox/__init__.pyc
default environments:
py27-tests -> Run the tests in Python 2.7
additional environments:
py27-dev -> Run the dev server in Python 2.7
py36-dev -> Run the dev server in Python 3.6
Overall, though, we found this way of writing tox.ini
files produced much
larger files that contained a lot more boilerplate and duplication, and were
more difficult to read and to work with. The file above only supports running
the dev server in Pythons 2.7 and 3.6, for example. To run it in Python 3.7
you’d have to add a new [testenv:py37-dev]
section.
The factors and conditionals approach produces a much smaller tox.ini
file
while also enabling any command to be run with any version of Python.
You can also combine both approaches in a single tox.ini
.
Grab bag
Some other cool stuff that tox can do:
-
Interactive shell substitution. You can use substitutions like
{tty:ON_VALUE:OFF_VALUE}
to haveON_VALUE
be used when tox is run in an interactive shell (e.g. on a developer’s laptop) andOFF_VALUE
be used when its run non-interactively (e.g. on CI). You could use this, for example, to pass the--pdb
argument topytest
to make it drop in to a debugger prompt when a test fails, but only when tox is being run interactively. -
Jenkins support. You can add a
[tox:jenkins]
section containing global settings overrides that should be used only when tox is running in Jenkins. See Jenkins override. -
Force a particular version of a dependency just once with
--force-dep
.