--- title: An Opinionated tox.ini File date: 2018-11-14 15:59 +0000 subtitle: Automate all of your Python project's tasks with a succinct tox.ini file. tags: tox Hypothesis redirect_from: - /post/tox/ --- This post assumes some knowledge of Tox. For a beginner's introduction see [my Tox tutorial.]({{ site.baseurl }}{% post_url 2018-09-01-tox-tutorial %}) Below is an excerpted version of the [tox.ini file from hypothesis/h as of November 2018](https://github.com/hypothesis/h/blob/471cf3e620cb0ff1a6ec8ccca710727b1e20e0e6/tox.ini) (yes we're still using Python 2). It demonstrates a particular way of using Tox in which all of the Tox commands are of the form `tox -e pyXY-`. That is: use a particular version of Python (the `pyXY` part) to run a particular development task (the ``) part: * `tox -e py27-dev` runs the app in the development server. `tox -e py36-dev` runs the app in the development server using Python 3. * `tox -e py27-tests` or `tox -e py36-tests` runs the tests in Python 2 or 3. * `tox -e py36-lint` runs the linter and prints out any warnings. * `tox -e py36-docs` builds the docs and serves them locally with live reloading. * `tox -e py36-coverage` prints the coverage report. * Not shown below, but `tox -e py36-format` formats the code using Black. * Other tasks, such as deploying a new release, could be added too. We see Tox as a general, virtualenv-based development task automation tool, and don't just use it for running tests. We use Tox to standardise and automate any development task that benefits from being run in its own isolated venv. Using Tox's factors and conditionals (see below) you can implement this `pyXY-` approach in a simple and concise `tox.ini`, without duplication or boilerplate: ```ini [tox] envlist = py27-tests skipsdist = true # Enable the venv_update extension so that changes to requirements files are # automatically detected. 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 … {tests,functests}: TEST_DATABASE_URL {tests,functests}: ELASTICSEARCH_URL … functests: BROKER_URL codecov: CI TRAVIS* deps = tests: coverage {tests,functests}: pytest {tests,functests}: factory-boy tests: mock tests: hypothesis lint: flake8 lint: flake8-future-import coverage: coverage codecov: codecov functests: webtest docs: sphinx-autobuild docs: sphinx docs: sphinx_rtd_theme {tests,functests}: -r requirements.txt dev: ipython dev: ipdb dev: -r requirements-dev.in whitelist_externals = dev: sh changedir = docs: docs commands = dev: sh bin/hypothesis --dev init dev: {posargs:sh bin/hypothesis devserver} lint: flake8 h lint: flake8 tests tests: coverage run … functests: pytest -Werror {posargs:tests/functional/} docs: sphinx-autobuild … coverage: -coverage combine coverage: coverage report codecov: codecov ``` ## How it Works The `tox.ini` file is written entirely using [Tox's factors and conditionals](https://tox.readthedocs.io/en/latest/config.html#generating-environments-conditional-settings), and doesn't use any of the separate `[testenv:NAME]` file sections that you see in most `tox.ini` files. When you pass an envlist to Tox like `tox -e py36-tests` you're telling Tox to run a single testenv, named `py36-tests`, but the testenv name contains two **factors** `py36` and `tox`. **Builtin factors:** The `py36` factor is a Tox builtin factor that sets the Python version to 3.6. Tox comes with builtin factors for all the Python versions: `py27`, `py37`, etc. You'll notice that we don't define the `pyXY`'s anywhere in our `tox.ini` file. They're builtin, so we can just go ahead and use them with `-e` on the command line. **Custom factors:** The `tests` part in `-e py36-tests` is a custom factor, defined by us in our `tox.ini` file. Some of the lines in our `tox.ini` use `tests:` as a conditional, meaning those lines should only be applied if `tests` is in the testenv name. For example the `coverage` and `mock` dependencies will only be installed if `tests` is involved (for example when running `tox -e py27-tests` or `tox -e py36-tests`), whereas `flake8` will only be installed if `lint` is in the `tox -e` command:
deps =
tests: coverage
tests: mock
lint: flake8
…
By using `tests:` and `lint:` as conditionals in the `tox.ini` file we implicitly define `tests` and `lint` factors in addition to the builtin `pyXY` factors, and can use them in commands like `tox -e py36-lint`. What Tox does when you give it a command like `tox -e py36-tests` is: 1. First, it invokes the builtin `py36` factor, which sets the version of Python to be used to 3.6. 2. Second, it parses the `tox.ini` file looking for lines that match any factor in the given testenv name `py36-tests`: 1. Any line that doesn't begin with a `factor:` prefix is unconditional, and is always applied whenever Tox runs. 2. Any line that begins with `tests:` matches the `tests` conditional, so it's applied. Any `tests:` dependencies will be installed, any `tests:` commands will be run, and so on. 3. A line like `{tests,functests}: pytest` matches _either_ `tests` or `functests`, so the `pytest` dependency will be installed for `tox -e py36-tests` too. More [complex conditionals](https://tox.readthedocs.io/en/latest/config.html#complex-factor-conditions) are possible too. 3. After collecting all the matching lines Tox runs the testenv: 1. Any matched environment variables are passed through to the test environment 2. Matched dependencies are installed in the order that they were given in the `tox.ini` file 3. Matched commands are run in file order Here's a view of the `tox.ini` file with the lines that apply when `tox -e py36-tests` is run highlighted. The non-highlighted lines are ignored:
\[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
…
{tests,functests}: TEST_DATABASE_URL
{tests,functests}: ELASTICSEARCH_URL
…
functests: BROKER_URL
codecov: CI TRAVIS*
deps =
tests: coverage
{tests,functests}: pytest
{tests,functests}: factory-boy
tests: mock
tests: hypothesis
lint: flake8
lint: flake8-future-import
coverage: coverage
codecov: codecov
functests: webtest
docs: sphinx-autobuild
docs: sphinx
docs: sphinx_rtd_theme
{tests,functests}: -r requirements.txt
dev: ipython
dev: ipdb
dev: -r requirements-dev.in
whitelist_externals =
dev: sh
changedir =
docs: docs
commands =
dev: sh bin/hypothesis --dev init
dev: {posargs:sh bin/hypothesis devserver}
lint: flake8 h
lint: flake8 tests
tests: coverage run …
functests: pytest -Werror {posargs:tests/functional/}
docs: sphinx-autobuild …
coverage: -coverage combine
coverage: coverage report
codecov: codecov
## Benefits Automating our development tasks like this has a bunch of advantages: * It simplifies things for developers, especially new developers. You don't need to learn about virtualenv and create and activate a development virtualenv and install dependencies. You don't need virtualenv management tools like virtualenvwrapper. You just run `tox`. * Tasks for running the tests, linting, releasing, etc can be defined in one place in `tox.ini` and run exactly the same everywhere: on any developer's machine or on CI. * Tox isolates every task using a Python virtualenv, `PATH` isolation and environment variable isolation, so problems caused by differences between environments are minimised. * Because Tox uses a different virtualenv for each task (it creates one virtualenv for the dev server, another for the tests, another for building the docs, and so on) the chances of conflicts between dependencies are reduced. You don't need your documentation tools installed in your devserver environment. * When Python 3.8 comes out and it's time to test your app in it, you can easily run the dev server with `tox -e py38-dev` or the tests with `tox -e py38-tests`, without having to modify `tox.ini` first. Tox isn't just for tests! Consider using it to standardise all of your project's development tasks. For details of all the Tox concepts used here and more see [my Tox tutorial]({{ site.baseurl }}{% post_url 2018-09-01-tox-tutorial %}).