Understanding the purpose of tox.ini

Introduction

I was confused by the Python tool tox for a long time, and was unsure what it was really used for considering lots of different Python packages appeared to rely on tox’s configuration file (e.g. tox.ini).

In this post I’m going to briefly explain what the tox tool is, and what a tox.ini configuration file looks like and why that configuration file can be used for more than just serving the purposes of the tox tool.

What is tox?

Tox is a tool that creates virtual environments, and installs the configured dependencies for those environments, for the purpose of testing a Python package (i.e. something that will be shared via PyPi, and so it only works with code that defines a setup.py).

PyPi == Python Package Index (i.e. where you typically install all your Python packages from, when executing pip install).

The file that you use to configure tox can be one of the following…

Note: these ^^ are all ini file formats – which is information that will become more relevant/important later on.

Example tox.ini

All configuration options for tox can be found here: tox.readthedocs.io/en/latest/config.html but the below example is a simple demonstration of how you might configure tox for a real package (e.g. this is an actual tox.ini file I’ve used).

[tox]
envlist = 
    py37, lint
toxworkdir = 
    {env:TOX_WORK_DIR}

[testenv]
setenv =
    PYTHONDONTWRITEBYTECODE = 1
whitelist_externals =
    /bin/bash
deps = 
    -rrequirements-dev.txt
commands =
    py.test --cov={envsitepackagesdir}/bf_tornado -m "not integration"

[testenv:dev]
usedevelop=True
recreate = False
commands =
    # to run arbitrary commands: tox -e dev -- bash
    {posargs:py.test --cov=bf_tornado}

[testenv:lint]
deps =
    flake8==3.8.3
    mypy==0.782
commands =
    flake8 bf_tornado
    mypy --verbose --ignore-missing-imports --package bf_tornado

Note: the name after testenv: is the name of the virtual environment that will be created (e.g. testenv:foo will create a “foo” virtual environment).

Configuring other packages

A tox.ini file can be used to configure different types of packages, which is confusing at first because the tox home page suggests that tox is used to test your own packages you plan on distributing to PyPi.

What the tox authors mean by that description, is that the tox command itself is used to handle testing your packages, while the tox.ini configuration file is just one such file that can be used to contain configuration information.

This is why other packages, such as Flake8 allow you to configure Flake8 using the tox.ini file, as well as alternatives such as: setup.cfg or .flake8.

The key to understanding why this works is as follows: each of these files conform to the ini file format. So you’re free to use whatever file name you feel best suits your project, while the format of the file will stay consistent to what is expected of an .ini file.

Why do multiple tools support tox.ini?

The benefit of these various tools supporting the tox.ini filename specifically is to help reduce the number of configuration files a project requires.

Imagine you needed a unique configuration file for every Python command line tool you use in your project. How many would that result in? How hard would it be to remember all the various file names?

With that in mind, below is an example that shows various Python packages being configured within a tox.ini file.

And in case it’s unclear, the configuration inside of the tox.ini file is used instead of having to pass those configuration values via the command line.

So in the case of a tool such as flake8, instead of using flake8 --max-line-length=120 you could just call flake8 and the flag value is extracted from the configuration file.

[flake8]
max_line_length = 120
ignore = E261,E265,E402  # http://pep8.readthedocs.org/en/latest/intro.html#error-codes

[coverage:run]
branch = True

[coverage:report]
show_missing = True
exclude_lines =
    raise NotImplementedError
    return NotImplemented
    def __repr__
omit = bf_tornado/testing.py

[pytest]
addopts = 
    --strict -p no:cacheprovider --showlocals
markers =
    integration: mark a test as an integration test that makes http calls.

Bonus Section

Although not directly related to the conversation about tox.ini I thought it worth mentioning that I discovered recently the Python logging library allows you to be able to configure logging via an ini configuration file!

Reference: docs.python.org/3/howto/logging.html

The ini file might look something like the following:

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

The Python program that uses this file might look like the following:

import logging
import logging.config

logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

Notice though, the name of the configuration file we imported was logging.conf (no .ini extension). It doesn’t matter that the file doesn’t use the .ini extension as long as the format of the file itself conforms to the ini format.