Skip to content

Getting started with uvoxen

Overview

This is a description of how to modify the pyproject.toml file of an existing Python library so that the tests may be run using the uvoxen tool. Since uvoxen was written to ease the workflow of its author, some of the steps may need modification for projects with different workflow (e.g. not using Tox for testing, a different file layout, etc).

Add the tool.uvoxen section

The first step in using uvoxen is to add a tool.uvoxen table to the pyproject.toml file, and specify the version of its file format. We also add an empty "environments list" variable and an empty "environment definitions" section:

[tool.uvoxen]
envlist = [
]

[tool.uvoxen.format.version]
major = 0
minor = 1

[tool.uvoxen.env]

This documentation describes version 0.1 of the configuration file format; later uvoxen versions may support more settings or change some of them in incompatible ways.

These configuration settings should be enough so that uvoxen can be run in query mode to make sure it can at least parse the pyproject.toml file:

uvoxen list reqs
uvoxen list envs
uvoxen list pythons

The first two commands should output nothing; the third one should list all of the Python versions declared as supported in the project.classifiers list of the pyproject.toml file.

Add a standalone test environment

Let us start with a test environment that does not require either the project itself or its dependencies to be installed. To add a ruff test environment that will run the Ruff linter, we need to first declare what packages should be installed. The uvoxen tool relies on uv supporting PEP 735 dependency groups, so let us add one of those:

[dependency-groups]
testenv-ruff = [
  "ruff == 0.9.9",
]

Note that the dependency-groups section is outside of the tool.uvoxen one.

At this point, uvoxen should see this set of requirements; the uvoxen list reqs command should output a single line, testenv-ruff.

Now we are ready to define the test environment itself:

[tool.uvoxen.env.ruff]
dependency-groups = [
  "testenv-ruff",
]
commands = [
  "ruff check -- src/divert-and-modify tests/unit",
]

Now the uvoxen list envs command should output ruff.

With these definitions, uvoxen should be able to invoke uv to create a virtual environment in the .venv/ directory, install only the packages listed in the testenv-ruff dependency group (i.e. only Ruff itself), and then run the specified commands:

uvoxen uv run -e ruff

Once this works, we can add ruff to the list of environments to be run by default:

[tool.uvoxen]
envlist = [
  "ruff",
]

And now all the defined environments can be run in the order specified in that list:

uvoxen uv run

Use variables for parameters common to several test environments

If we now want to add a test environment that runs another linter, we may need to specify the same list of directories - src/divert-and-modify and tests/unit - in the commands to be run there. The uvoxen tool supports variables of two types: strings and lists of strings. They are defined in the tool.uvoxen.defs section:

[tool.uvoxen.defs]
pyfiles = [
  "src/divert-and-modify",
  "tests/unit",
]

The variables may then be referenced in the commands using the Tox syntax:

commands = [
  "ruff check -- {[defs]pyfiles}",
]

This is one of the leaky abstractions of uvoxen - since it started as a way to run the same tests that can be run in a tox.ini file, it uses syntax and concepts borrowed from Tox in several places. It tries to expand the values of the variables in the same way Tox does; it is possible that sometimes the results may vary, e.g. regarding shell quoting of values containing special characters. Also, uvoxen does not support the full Tox syntax or all the predefined variables yet; see the Configuration section for details.

Now we are ready to add a second test environment that also uses Ruff to check the formatting of the Python source files:

[tool.uvoxen]
envlist = [
  "format",
  "ruff",
]

[tool.uvoxen.env.format]
dependency-groups = [
  "testenv-ruff",
]
commands = [
  "ruff check --config ruff-base.toml --select=D,I --diff -- {[defs]pyfiles}",
  "ruff format --config ruff-base.toml --check -- {[defs]pyfiles}",
]

Now uvoxen uv run should run the commands for both environments, one after the other, recreating the .venv virtual environment between them. With the peformance of the uv tool with cached module metadata, recreating the environments should not be too slow.

Run the tests using different Python versions

The uvoxen tool allows the Python version to be specified when running tests using the --python (-p) option before the first subcommand is specified:

uvoxen -p 3.10 uv run

Also, it parses the project.classifiers list in the pyproject.toml file, so it can be instructed to run the test environments with all supported Python versions in order:

uvoxen -p supported uv run

Reuse dependency lists in other dependency groups

In some cases it may be useful to list the same Python libraries in several different sets of requirements. The PEP 735 dependency group specification defines the include-group syntax:

[dependency-groups]
testenv-mypy = [
  {include-group = "testenv-unit-tests"},
  "mypy >= 1.7, < 2",
]
testenv-unit-tests = [
  "pytest >= 7, < 8",
]

The full list of requirements for a dependency group may be viewed using the req generate subcommand; more about it later.

uvoxen req generate -o - -g testenv-mypy

Now the mypy test environment may be defined:

[tool.uvoxen.env.mypy]
dependency-groups = [
  "testenv-mypy",
]
commands = [
  "mypy -- {[defs]pyfiles}",
]

Note that the uv tool will always install the dependencies of the project, even for environments that do not need the project itself installed. This is the reason why the mypy type checker can now see the Python modules that the project source files import in addition to the source files themselves. This differs from the way Tox normally works; more on that later.

Run a test that needs the project itself to be installed

Some test environments, e.g. unit tests, functional tests, continuous integration tests, etc., will also need the project itself to be built and installed into the virtual environment before the test can be run. The install-project environment parameter will take care of that:

[tool.uvoxen.env.unit-tests]
dependency-groups = [
  "testenv-unit-tests",
]
install-project = true
commands = [
  "pytest {posargs} tests/unit",
]

Pass command-line arguments to the test commands

Another feature uvoxen has copied over from Tox is the use of the {posargs} variable in a command string; it is replaced by the command-line arguments passed to the uvoxen uv run subcommand itself. Thus, with the above definition of the unit-tests environment, uvoxen will pass the -vv and -s parameters to pytest:

uvoxen uv run -- -vv -s

As with Tox, there is currently no way to specify command-line arguments to be passed to specific environments if more than one of those is run.

Export dependency groups into requirements files for other tools

Some external tools, e.g. the ReadTheDocs mkdocs generator, do not yet understand dependency groups and expect to read requirements files. The req generate subcommand will generate such a file for the specified dependency group; if the --output (-o) option is not specified, a requirements/<groupname>.txt file will be generated:

uvoxen req generate -g docs

Sometimes it is useful to place some "hardcoded" text at the top or the bottom of a generated file. The req generate subcommand will honor the header and footer parameters in the tool.uvoxen.req section:

[tool.uvoxen.req]
header = """
# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: BSD-2-Clause

"""

The --check option will make uvoxen exit with a non-zero code if the file contents differ from what it expects, without modifying the file itself:

uvoxen req generate --check -g docs

If the output filename is specified as a single dash, uvoxen will not write to any files, but display the generated text on its standard output stream instead:

uvoxen req generate -o - -g testenv-mypy

There is no --diff option yet, although one is planned.

Generate a Tox configuration file

For people who still like to use Tox as a test runner, uvoxen can export the test environment settings into a tox.ini file using the tox generate subcommand:

uvoxen tox generate

As with req, the --output (-o) and --check command-line options are supported:

uvoxen tox generate -o - | diff -u tox.ini -

As with req, the header and footer parameters in the uvoxen.tox section are honored; the footer one, in particular, may be used to define additional Tox environments that uvoxen will not handle. For that purpose, there is also the uvoxen.tox.envlist-append list that specifies the names of Tox environments to be added to the tox.envlist setting in the generated tox.ini file.

By default, uvoxen will output the Tox environment definitions in the order they are listed in tool.uvoxen.envlist with any additional ones placed at the end in lexicographical order of their names. If preferred, the tool.uvoxen.env-order list may be used to specify the order of some environments to be placed first; the rest will still be output in lexicographical order. This may be useful when first converting a tox.ini file to pyproject.toml definitions for uvoxen - it can minimize the difference between the generated tox.ini file and the original one.

Specify environment variables to be set for or passed into the environments

Like Tox, uvoxen supports the passenv list and the setenv table for test environments:

[tool.uvoxen.env.docker]
dependency-groups = [
  "testenv-docker",
]
install-project = true
passenv = [
  "TEST_DOCKER_OPTS",
]
commands = [
  "python3 -m test_docker run {env:TEST_DOCKER_OPTS}",
]

[tool.uvoxen.env.docker.setenv]
PYTHONPATH = "{toxinidir}/tests/docker/python"

Some Tox predefined variables, e.g. {envdir} or {toxinidir}, may be used in setenv value strings; see the configuration section for a full list.

Add tags for running environments in stages

An environment may specify a tags list:

[tool.uvoxen.env.reuse]
tags = [
  "check",
  "quick",
]
dependency-groups = [
  "testenv-reuse",
]
commands = [
  "reuse lint",
]

It is currently ignored by the uv runner, but it will be copied into the tox.ini file by tox generate, so that the Tox environments may later be run in parallel stages using the tox-stages tool:

uvoxen tox generate
tox-stages run

An additional uv run-stages subcommand is planned.