Plugin How-to Guides¶
Plugin hook lifecycle¶
The diagram below shows when each plugin hook fires during a tox run:
flowchart TD
start(( )) --> register[tox_register_tox_env]
register --> add_option[tox_add_option]
add_option --> extend[tox_extend_envs]
extend --> core_config[tox_add_core_config]
core_config --> env_config[tox_add_env_config]
env_config --> envloop
subgraph envloop [for each environment]
direction TB
on_install[tox_on_install]
on_install --> before[tox_before_run_commands]
before --> cmds[run commands]
cmds --> after[tox_after_run_commands]
after --> teardown[tox_env_teardown]
end
teardown --> done(( ))
classDef setupStyle fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a5f
classDef envStyle fill:#dcfce7,stroke:#22c55e,stroke-width:2px,color:#14532d
classDef cmdStyle fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#3b0764
classDef installStyle fill:#ffedd5,stroke:#f97316,stroke-width:2px,color:#7c2d12
class register,add_option,extend,core_config,env_config setupStyle
class before,after,teardown envStyle
class cmds cmdStyle
class on_install installStyle
Add custom config to an environment¶
Use tox_add_env_config to register new configuration keys on every tox
environment. Users can then set these keys in tox.toml or tox.ini like any built-in setting.
from tox.config.sets import EnvConfigSet
from tox.plugin import impl
from tox.session.state import State
@impl
def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None:
env_conf.add_config(
keys=["my_timeout"],
of_type=int,
default=30,
desc="timeout in seconds for custom checks",
)
Access the value later via env_conf["my_timeout"].
Run code before or after commands¶
Use tox_before_run_commands and tox_after_run_commands to execute logic around the commands phase. The tox_after_run_commands
hook receives the exit code and the list of Outcome objects for each command.
import time
from tox.execute import Outcome
from tox.plugin import impl
from tox.tox_env.api import ToxEnv
_start: float = 0
@impl
def tox_before_run_commands(tox_env: ToxEnv) -> None:
global _start # noqa: PLW0603
_start = time.monotonic()
@impl
def tox_after_run_commands(
tox_env: ToxEnv, exit_code: int, outcomes: list[Outcome]
) -> None:
elapsed = time.monotonic() - _start
tox_env.conf.core["report"].verbosity0(
f"{tox_env.conf['env_name']} finished in {elapsed:.1f}s "
f"(exit code {exit_code})",
)
Intercept package installations¶
Use tox_on_install to run logic whenever tox installs packages (deps, the
project itself, etc.). The section and of_type parameters identify which install phase triggered the call.
from typing import Any
from tox.plugin import impl
from tox.tox_env.api import ToxEnv
@impl
def tox_on_install(tox_env: ToxEnv, arguments: Any, section: str, of_type: str) -> None:
print(f"Installing [{section}] {of_type}: {arguments}") # noqa: T201
Clean up after an environment¶
Use tox_env_teardown to run cleanup logic after an environment finishes,
regardless of whether commands succeeded or failed.
from tox.plugin import impl
from tox.tox_env.api import ToxEnv
@impl
def tox_env_teardown(tox_env: ToxEnv) -> None:
cache_dir = tox_env.env_dir / ".cache"
if cache_dir.exists():
import shutil
shutil.rmtree(cache_dir)
Register a custom environment runner¶
Use tox_register_tox_env to register a custom run or package environment
type. This is the hook used by plugins like tox-uv to replace the default virtualenv-based runner.
from tox.plugin import impl
from tox.tox_env.python.virtual_env.runner import VirtualEnvRunner
from tox.tox_env.register import ToxEnvRegister
class MyRunner(VirtualEnvRunner):
@staticmethod
def id() -> str:
return "my-runner"
# override methods to customize behavior
@impl
def tox_register_tox_env(register: ToxEnvRegister) -> None:
register.add_run_env(MyRunner)
Set runner = my-runner in a tox environment to use it.
Package a plugin for distribution¶
While toxfile.py works for project-local plugins, distributable plugins are standard Python packages that declare
the tox entry point.
In pyproject.toml:
[project]
name = "tox-myplugin"
version = "1.0.0"
dependencies = ["tox>=4"]
[project.entry-points.tox]
myplugin = "tox_myplugin"
In src/tox_myplugin/__init__.py, define your hooks exactly as in toxfile.py:
from tox.plugin import impl
@impl
def tox_append_version_info() -> str:
return "myplugin-1.0.0"
After pip install tox-myplugin, tox discovers the plugin automatically via the entry point.
Logging¶
tox uses Python’s standard logging module. Plugin authors can use it to emit messages that integrate with tox’s
output. The root logger is configured by tox at startup, so any logging call from plugin code is automatically
handled.
Getting a logger¶
Use logging.getLogger() with your plugin’s package name:
import logging
logger = logging.getLogger(__name__)
You can also call module-level functions like logging.info(...) directly, but using a named logger makes it easier
for users to filter output.
Verbosity levels¶
tox maps its -v / -q flags to standard logging levels:
Flags |
Logging level |
Notes |
|---|---|---|
|
|
Almost nothing is shown. |
(default) |
|
Only errors are shown. |
|
|
Warnings and above are shown. This is the first level that appears in normal output. |
|
|
Informational messages appear. |
|
|
Debug messages appear, along with timestamps and source file locations. |
Tip
For messages that should be visible during normal usage (e.g. tox -v), use logging.warning(). Use
logging.info() for detail that is only useful when diagnosing issues.
Coloring¶
tox applies color automatically based on the log level (when color output is enabled):
ERROR and above: red
WARNING: cyan
INFO and below: white
There is a special convention for the WARNING level: if the message format string is "%s%s> %s" the second
argument is rendered in a dimmed style and the > acts as a separator. This is the pattern tox itself uses to show
the tox-environment prefix:
# The second argument (plugin name) appears dimmed, followed by "> message"
logging.warning("%s%s> %s", "", "my-plugin", "This is a warning message")
For other levels (including ERROR), the entire line is colored, so inline formatting is not possible.
Note
Color is controlled by the --colored flag and the NO_COLOR / FORCE_COLOR environment variables. Plugin
code does not need to handle color escapes manually.
Debug output¶
At -vvv (DEBUG level), tox adds extra metadata to every log line:
elapsed time in milliseconds
one-letter level indicator (
D,I,W,E,C)source file and line number
This is useful for profiling and tracing plugin behavior.
Complete example¶
import logging
from tox.plugin import impl
from tox.tox_env.api import ToxEnv
logger = logging.getLogger(__name__)
@impl
def tox_before_run_commands(tox_env: ToxEnv) -> None:
# Visible at -vv or higher
logger.info("preparing environment %s", tox_env.name)
# Visible at -v or higher, with dimmed plugin name prefix
logger.warning(
"%s%s> %s", "", __package__, f"running pre-checks for {tox_env.name}"
)
Extension points¶
Plugin management for tox using pluggy.
Pluggy discovers a plugin by looking up for entry-points named tox, for example in a pyproject.toml:
[project.entry-points.tox]
your_plugin = "your_plugin.hooks"
Therefore, to start using a plugin, you solely need to install it in the same environment tox is running in and it will
be discovered via the defined entry-point (in the example above, tox will load your_plugin.hooks).
A plugin is created by implementing extension points in the form of hooks. For example the following code snippet would
define a new --magic command line interface flag the user can specify:
from tox.config.cli.parser import ToxParser
from tox.plugin import impl
@impl
def tox_add_option(parser: ToxParser) -> None:
parser.add_argument("--magic", action="store_true", help="magical flag")
You can define such hooks either in a package installed alongside tox or within a toxfile.py found alongside your
tox configuration file (root of your project).
- tox.plugin.NAME = 'tox'¶
the name of the tox hook
- tox.plugin.impl¶
decorator to mark tox plugin hooks
- tox.plugin.spec.tox_add_core_config(core_conf, state)¶
Called when the core configuration is built for a tox environment.
- tox.plugin.spec.tox_add_env_config(env_conf, state)¶
Called when configuration is built for a tox environment.
- Parameters:
env_conf (
EnvConfigSet) – the core configuration objectstate (
State) – the global tox state object
- Return type:
- tox.plugin.spec.tox_add_option(parser)¶
Add a command line argument.
This is the first hook to be called, right after the logging setup and config source discovery.
- tox.plugin.spec.tox_after_run_commands(tox_env, exit_code, outcomes)¶
Called after the commands set is executed.
- tox.plugin.spec.tox_before_run_commands(tox_env)¶
Called before the commands set is executed.
- tox.plugin.spec.tox_env_teardown(tox_env)¶
Called after a tox environment has been teared down.
- tox.plugin.spec.tox_extend_envs()¶
Declare additional environment names.
Added in version 4.29.0.
This hook is called without any arguments early in the lifecycle. It is expected to return an iterable of strings with environment names for tox to consider. It can be used to facilitate dynamic creation of additional environments from within tox plugins.
This is ideal to pair with
tox_add_core_configthat has access tostate.conf.memory_seed_loadersallowing to extend it with instances oftox.config.loader.memory.MemoryLoaderearly enough before tox starts caching configuration values sourced elsewhere.
- tox.plugin.spec.tox_on_install(tox_env, arguments, section, of_type)¶
Called before executing an installation command.
- tox.plugin.spec.tox_register_tox_env(register)¶
Register new tox environment type. You can register:
run environment: by default this is a local subprocess backed virtualenv Python
packaging environment: by default this is a PEP-517 compliant local subprocess backed virtualenv Python
- Parameters:
register (
ToxEnvRegister) – a object that can be used to register new tox environment types- Return type:
Adopting a plugin under the tox-dev organization¶
You’re free to host your plugin on your favorite platform. However, the core tox development happens on GitHub under the
tox-dev organization. We are happy to adopt tox plugins under the tox-dev organization if:
the plugin solves a valid use case and is not malicious,
it’s released on PyPI with at least 100 downloads per month (to ensure it’s actively used).
What’s in it for you:
you get owner rights on the repository under the tox-dev organization,
exposure of your plugin under the core umbrella,
backup maintainers from other tox plugin developers.
How to apply:
create an issue under the
tox-dev/toxGitHub repository with the title Adopt plugin <name>,wait for the green light by one of the maintainers (see Current maintainers),
follow the guidance by GitHub,
(optionally) add at least one other person as co-maintainer on PyPI.