0
# Plugin System
1
2
Hook-based extensibility system using pluggy that allows plugins to extend CLI options, configuration, environment types, and execution hooks. Tox's plugin system supports both entry-point plugins and toxfile.py plugins for maximum flexibility.
3
4
## Capabilities
5
6
### Plugin Hook Decorator
7
8
The core decorator for marking plugin hook implementations.
9
10
```python { .api }
11
impl: Callable[[_F], _F]
12
"""
13
Decorator to mark tox plugin hooks.
14
15
Usage:
16
@impl
17
def hook_function(...): ...
18
"""
19
20
NAME: str = "tox"
21
"""The name of the tox hook system."""
22
```
23
24
Usage example:
25
```python
26
from tox.plugin import impl
27
28
@impl
29
def tox_add_option(parser):
30
parser.add_argument("--my-option", help="Custom option")
31
```
32
33
### Hook Specifications
34
35
The complete set of hooks available for plugin development.
36
37
```python { .api }
38
class ToxHookSpecs:
39
"""Hook specification definitions for tox plugins."""
40
41
def tox_add_option(self, parser: ToxParser) -> None:
42
"""
43
Add command line options.
44
45
Args:
46
parser: Command line parser to extend
47
"""
48
49
def tox_add_core_config(self, core_conf: CoreConfigSet) -> None:
50
"""
51
Add core configuration options.
52
53
Args:
54
core_conf: Core configuration set to extend
55
"""
56
57
def tox_add_env_config(self, env_conf: EnvConfigSet) -> None:
58
"""
59
Add environment-specific configuration options.
60
61
Args:
62
env_conf: Environment configuration set to extend
63
"""
64
65
def tox_register_tox_env(self, register) -> None:
66
"""
67
Register new environment types.
68
69
Args:
70
register: Environment registry to extend
71
"""
72
73
def tox_extend_envs(self) -> list[str]:
74
"""
75
Extend the list of available environments.
76
77
Returns:
78
list[str]: Additional environment names
79
"""
80
81
def tox_before_run_commands(self, tox_env: ToxEnv, run_conf, opts) -> None:
82
"""
83
Hook called before running commands in environment.
84
85
Args:
86
tox_env: Environment where commands will run
87
run_conf: Run configuration
88
opts: Additional options
89
"""
90
91
def tox_after_run_commands(self, tox_env: ToxEnv, run_conf, opts, outcome: Outcome) -> None:
92
"""
93
Hook called after running commands in environment.
94
95
Args:
96
tox_env: Environment where commands ran
97
run_conf: Run configuration
98
opts: Additional options
99
outcome: Execution outcome
100
"""
101
```
102
103
### Plugin Manager
104
105
The plugin manager coordinates plugin discovery and hook execution.
106
107
```python { .api }
108
MANAGER: PluginManager
109
"""Global plugin manager instance."""
110
```
111
112
## Plugin Development
113
114
### Basic Plugin Structure
115
116
Create a plugin by implementing hook functions:
117
118
```python
119
# my_tox_plugin.py
120
from tox.plugin import impl
121
from tox.config.cli.parser import ToxParser
122
123
@impl
124
def tox_add_option(parser: ToxParser) -> None:
125
"""Add custom CLI option."""
126
parser.add_argument(
127
"--coverage-report",
128
action="store_true",
129
help="Generate coverage report after tests"
130
)
131
132
@impl
133
def tox_add_core_config(core_conf) -> None:
134
"""Add core configuration option."""
135
core_conf.add_config(
136
keys="coverage_threshold",
137
desc="Minimum coverage threshold",
138
of_type=float,
139
default=80.0
140
)
141
142
@impl
143
def tox_after_run_commands(tox_env, run_conf, opts, outcome) -> None:
144
"""Generate coverage report if requested."""
145
if getattr(opts, 'coverage_report', False):
146
print(f"Generating coverage report for {tox_env.name}")
147
# Coverage report generation logic
148
```
149
150
### Environment Type Plugins
151
152
Register custom environment types:
153
154
```python
155
from tox.plugin import impl
156
from tox.tox_env.api import ToxEnv
157
158
class DockerToxEnv(ToxEnv):
159
"""Docker-based tox environment."""
160
161
def create(self) -> None:
162
"""Create Docker container."""
163
print(f"Creating Docker container for {self.name}")
164
165
def execute(self, request):
166
"""Execute command in Docker container."""
167
# Docker execution logic
168
return ExecuteStatus(0, "success", "")
169
170
@impl
171
def tox_register_tox_env(register) -> None:
172
"""Register Docker environment type."""
173
register.add_env_type(
174
name="docker",
175
factory=DockerToxEnv,
176
description="Docker container environment"
177
)
178
```
179
180
### Configuration Extension
181
182
Extend tox configuration with plugin-specific options:
183
184
```python
185
@impl
186
def tox_add_env_config(env_conf) -> None:
187
"""Add environment configuration options."""
188
env_conf.add_config(
189
keys="docker_image",
190
desc="Docker image to use",
191
of_type=str,
192
default="python:3.11"
193
)
194
195
env_conf.add_config(
196
keys="docker_volumes",
197
desc="Docker volumes to mount",
198
of_type=list,
199
default=[]
200
)
201
```
202
203
## Plugin Discovery
204
205
### Entry Point Plugins
206
207
Register plugins via setuptools entry points:
208
209
```python
210
# setup.py or pyproject.toml
211
[project.entry-points.tox]
212
my_plugin = "my_tox_plugin"
213
docker_plugin = "tox_docker.plugin"
214
```
215
216
### Toxfile Plugins
217
218
Create `toxfile.py` in your project root:
219
220
```python
221
# toxfile.py
222
from tox.plugin import impl
223
224
@impl
225
def tox_add_option(parser):
226
"""Project-specific plugin hook."""
227
parser.add_argument("--project-flag", help="Project specific option")
228
```
229
230
## Advanced Plugin Patterns
231
232
### Multi-Hook Plugins
233
234
Plugins can implement multiple hooks:
235
236
```python
237
from tox.plugin import impl
238
239
class MyToxPlugin:
240
"""Comprehensive tox plugin."""
241
242
@impl
243
def tox_add_option(self, parser):
244
"""Add CLI options."""
245
parser.add_argument("--verbose-timing", action="store_true")
246
247
@impl
248
def tox_before_run_commands(self, tox_env, run_conf, opts):
249
"""Pre-execution hook."""
250
if getattr(opts, 'verbose_timing', False):
251
print(f"Starting {tox_env.name} at {time.time()}")
252
253
@impl
254
def tox_after_run_commands(self, tox_env, run_conf, opts, outcome):
255
"""Post-execution hook."""
256
if getattr(opts, 'verbose_timing', False):
257
print(f"Finished {tox_env.name} at {time.time()}")
258
259
# Register plugin instance
260
plugin = MyToxPlugin()
261
```
262
263
### Conditional Plugin Behavior
264
265
Implement conditional logic in plugins:
266
267
```python
268
@impl
269
def tox_before_run_commands(tox_env, run_conf, opts):
270
"""Conditional pre-execution logic."""
271
272
# Only run for Python environments
273
if hasattr(tox_env, 'python_executable'):
274
print(f"Python environment: {tox_env.name}")
275
276
# Only run in CI environment
277
if os.getenv('CI'):
278
print("Running in CI environment")
279
280
# Environment-specific logic
281
if tox_env.name.startswith('py3'):
282
print("Python 3 environment detected")
283
```
284
285
### Plugin Configuration
286
287
Plugins can define their own configuration:
288
289
```python
290
@impl
291
def tox_add_core_config(core_conf):
292
"""Add plugin configuration."""
293
core_conf.add_config(
294
keys="my_plugin_enabled",
295
desc="Enable my plugin functionality",
296
of_type=bool,
297
default=True
298
)
299
300
@impl
301
def tox_before_run_commands(tox_env, run_conf, opts):
302
"""Check plugin configuration."""
303
if tox_env.core["my_plugin_enabled"]:
304
print("Plugin is enabled")
305
else:
306
print("Plugin is disabled")
307
```
308
309
## Plugin Testing
310
311
Test plugins using tox's testing utilities:
312
313
```python
314
# test_my_plugin.py
315
import pytest
316
from tox.plugin import impl
317
from tox.run import main
318
319
def test_plugin_option():
320
"""Test custom CLI option."""
321
result = main(['--my-option', '--help'])
322
# Assert option is present in help output
323
324
def test_plugin_environment():
325
"""Test custom environment type."""
326
result = main(['-e', 'docker'])
327
# Assert Docker environment runs correctly
328
```
329
330
## Plugin Error Handling
331
332
Handle errors gracefully in plugins:
333
334
```python
335
@impl
336
def tox_register_tox_env(register):
337
"""Register with error handling."""
338
try:
339
# Check if Docker is available
340
import docker
341
register.add_env_type(
342
name="docker",
343
factory=DockerToxEnv,
344
description="Docker environment"
345
)
346
except ImportError:
347
print("Docker not available, skipping Docker environment")
348
```
349
350
## Built-in Plugin Examples
351
352
Tox includes several built-in plugins that demonstrate best practices:
353
354
- **Virtual environment plugin**: Creates Python virtual environments
355
- **Pip plugin**: Handles package installation via pip
356
- **Configuration plugins**: Extend configuration options
357
- **Reporting plugins**: Handle output formatting and logging
358
359
These serve as reference implementations for developing custom plugins with similar functionality.