Title: Managing a Project's Virtualenvs with tox Subheading: A comprehensive beginner's introduction to tox. Tags: tox, Hypothesis Alias: /post/tox-tutorial/ 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](https://github.com/hypothesis/h/blob/471cf3e620cb0ff1a6ec8ccca710727b1e20e0e6/tox.ini).
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 asto automate and standardize testing in Python. But the same docs later describe tox as
a generic virtualenv management and test command line tooland include examples of using tox to build documentation and to run development environments. tox's GitHub project's description is
Command 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 both
testenvand
virtualenvin this guide too. Eventually we'll [get around to explaining exactly what a testenv is](#factors-and-conditionals) 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: ```ini [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](https://tox.readthedocs.io/en/latest/#system-overview) in the tox docs: 1. Read tox config settings from a `tox.ini` file, command line options and environment variables. 2. Optionally create a Python package (sdist) of your project. (We skip this step for Hypothesis projects.) 3. Create a Python virtual environment using the version of Python selected in `tox.ini`. 4. Install any Python dependencies listed in `tox.ini` (the `deps` setting) into the virtualenv. If an `sdist` of your project was created in step 2 install that too. 5. Run the commands listed in `tox.ini` (the `commands` setting) in the virtualenv. By default fail if any of the commands exit with a non-zero exit code. 6. 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
projectvirtualenv 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](https://tox.readthedocs.io/en/latest/example/basic.html#compressing-dependency-matrix) from tox's docs illustrates tox's originally intended use-case: ```ini [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](#generative-envlists) 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](https://github.com/hypothesis/h/blob/master/tox.ini). 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. ```ini [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: ```console $ sudo pip install tox ``` Getting started: a minimal `tox.ini` file ----------------------------------------- Here's the smallest possible `tox.ini` file that will run without crashing: ```ini [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: ```console $ 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`](https://tox.readthedocs.io/en/latest/config.html#conf-skipsdist) 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
$ 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](https://tox.readthedocs.io/en/latest/config.html#tox-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](https://tox.readthedocs.io/en/latest/config.html#tox-environment-settings).
### 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`](https://tox.readthedocs.io/en/latest/config.html#conf-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:
```console
$ 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:
```ini
[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](#envlist).
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:
```ini
[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:
```ini
[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.](#dependencies))
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:
```console
$ 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`:
```ini
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](#substitutions).
`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](https://tox.readthedocs.io/en/latest/config.html#conf-envlist)
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:
```console
$ 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):
```console
$ 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](#factors-and-conditionals),
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](https://tox.readthedocs.io/en/latest/config.html#conf-deps)
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`:
```ini
[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](https://github.com/tox-dev/tox-pip-extensions/), 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:
```ini
[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](https://tox.readthedocs.io/en/latest/config.html#injected-environment-variables)
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](https://tox.readthedocs.io/en/latest/config.html#conf-passenv)
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:
```ini
[testenv]
passenv =
DATABASE_URL
ELASTICSEARCH_URL
PYTEST_ADDOPTS
…
```
You can also use [`setenv`](https://tox.readthedocs.io/en/latest/config.html#conf-setenv)
to set the values of additional envvars in the `tox.ini` file, rather than
reading them from the outside environment:
```ini
[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.](https://tox.readthedocs.io/en/latest/config.html#substitutions)
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`:
```ini
[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 `:`:
```ini
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:
```ini
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](https://tox.readthedocs.io/en/latest/config.html#globally-available-substitutions).
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](https://tox.readthedocs.io/en/latest/config.html#generating-environments-conditional-settings) 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](https://github.com/hypothesis/h/blob/master/tox.ini). But to understand them you have to grock a few tox concepts first:
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're running
one or more testenvs
(the testenvs listed in the envlist
setting, -e
command line
argument, or TOXENV
environment variable).
py27
contains a single factor, py27
.
A more complex testenv name like py27-tests
contains two factors
py27
and tests
.A factor is a collection of testenv settings. A testenv like
py27-tests
pulls in all of the settings from the py27
factor and the tests
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 your tox.ini
files.
These are factors that contain a single setting: the version of Python to use to
create the virtualenv with
(the basepython
setting).
You can also define custom factors in your tox.ini
file. test
in the
testenv name py27-test
is a custom factor.
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 with factorname:
.
For example in this tox.ini
snippet:
[testenv]
deps =
tests: pytest
the tests: pytest
implicitly creates a custom factor named tests
and tells tox to install the pytest
dependency only when the tests
factor
is invoked. tox -e py27-tests
will install pytest but tox -e py27-docs
won't.
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:
```ini
deps =
{tests,dev}: -r requirements.txt
```
Negation, logic, etc are also possible. See [Complex factor conditions](https://tox.readthedocs.io/en/latest/config.html#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:
```console
$ 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](https://tox.readthedocs.io/en/latest/config.html#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:
```ini
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:
```ini
[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:
```ini
[testenv:py27-dev]
deps =
{[dev]deps}
ipython
ipdb
```
These are called [section substitutions](https://tox.readthedocs.io/en/latest/config.html#substitution-for-values-from-other-sections).
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:
```console
$ 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`](https://tox.readthedocs.io/en/latest/config.html#factors-and-values-substitution-are-compatible).
Grab bag
--------
Some other cool stuff that tox can do:
* [Interactive shell substitution](https://tox.readthedocs.io/en/latest/config.html#interactive-shell-substitution).
You can use substitutions like `{tty:ON_VALUE:OFF_VALUE}` to have `ON_VALUE` be used when tox
is run in an interactive shell (e.g. on a developer's laptop) and `OFF_VALUE` be used when its
run non-interactively (e.g. on CI). You could use this, for example, to pass the `--pdb` argument
to `pytest` 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](https://tox.readthedocs.io/en/latest/config.html#jenkins-override).
* Force a particular version of a dependency just once with [`--force-dep`](https://tox.readthedocs.io/en/latest/config.html#cmdoption-tox-force-dep).