0
# Execution System
1
2
Command execution infrastructure for running commands within tox environments. The execution system supports subprocess execution, output capture, execution status tracking, and provides abstractions for different execution backends.
3
4
## Capabilities
5
6
### Execution Request
7
8
Specification for command execution including command, environment, and execution options.
9
10
```python { .api }
11
class ExecuteRequest:
12
"""Specification for command execution."""
13
14
def __init__(
15
self,
16
cmd: list[str],
17
cwd: Path | None = None,
18
env: dict[str, str] | None = None,
19
stdin: str | None = None,
20
run_id: str | None = None
21
) -> None:
22
"""
23
Initialize execution request.
24
25
Args:
26
cmd: Command and arguments to execute
27
cwd: Working directory for execution
28
env: Environment variables
29
stdin: Standard input data
30
run_id: Unique identifier for this execution
31
"""
32
33
@property
34
def cmd(self) -> list[str]:
35
"""Command and arguments."""
36
37
@property
38
def cwd(self) -> Path | None:
39
"""Working directory."""
40
41
@property
42
def env(self) -> dict[str, str] | None:
43
"""Environment variables."""
44
45
@property
46
def stdin(self) -> str | None:
47
"""Standard input data."""
48
49
@property
50
def run_id(self) -> str | None:
51
"""Execution run identifier."""
52
```
53
54
Usage example:
55
```python
56
from tox.execute.request import ExecuteRequest
57
58
# Simple command execution
59
request = ExecuteRequest(['pytest', '--verbose'])
60
61
# Command with working directory
62
request = ExecuteRequest(
63
cmd=['python', 'setup.py', 'test'],
64
cwd=Path('/project/root')
65
)
66
67
# Command with environment variables
68
request = ExecuteRequest(
69
cmd=['pytest'],
70
env={'PYTHONPATH': '/src', 'DEBUG': '1'}
71
)
72
```
73
74
### Execute Interface
75
76
Abstract interface for command execution backends.
77
78
```python { .api }
79
class Execute:
80
"""Command execution abstraction."""
81
82
def __call__(self, request: ExecuteRequest) -> ExecuteStatus:
83
"""
84
Execute command request.
85
86
Args:
87
request: Execution specification
88
89
Returns:
90
ExecuteStatus: Execution result
91
"""
92
93
def __enter__(self):
94
"""Context manager entry."""
95
96
def __exit__(self, exc_type, exc_val, exc_tb):
97
"""Context manager exit."""
98
```
99
100
### Execution Status
101
102
Result of command execution including exit code, output, and metadata.
103
104
```python { .api }
105
class ExecuteStatus:
106
"""Result of command execution."""
107
108
@property
109
def exit_code(self) -> int:
110
"""Process exit code."""
111
112
@property
113
def out(self) -> str:
114
"""Standard output."""
115
116
@property
117
def err(self) -> str:
118
"""Standard error output."""
119
120
@property
121
def duration(self) -> float:
122
"""Execution duration in seconds."""
123
124
@property
125
def success(self) -> bool:
126
"""Whether execution was successful (exit code 0)."""
127
128
class Outcome:
129
"""Execution outcome with additional metadata."""
130
131
@property
132
def status(self) -> ExecuteStatus:
133
"""Execution status."""
134
135
@property
136
def exception(self) -> Exception | None:
137
"""Exception if execution failed."""
138
```
139
140
Usage example:
141
```python
142
# Execute command and check results
143
status = executor(ExecuteRequest(['pytest']))
144
145
print(f"Exit code: {status.exit_code}")
146
print(f"Success: {status.success}")
147
print(f"Duration: {status.duration:.2f}s")
148
149
if not status.success:
150
print(f"Error output: {status.err}")
151
```
152
153
### Local Subprocess Execution
154
155
Concrete implementation for local subprocess execution.
156
157
```python { .api }
158
class LocalSubProcessExecute(Execute):
159
"""Local subprocess executor."""
160
161
def __init__(self, colored: bool = True) -> None:
162
"""
163
Initialize subprocess executor.
164
165
Args:
166
colored: Whether to preserve ANSI color codes
167
"""
168
169
def __call__(self, request: ExecuteRequest) -> ExecuteStatus:
170
"""Execute command as local subprocess."""
171
172
def interrupt(self) -> None:
173
"""Interrupt running subprocess."""
174
175
def terminate(self) -> None:
176
"""Terminate running subprocess."""
177
```
178
179
## Command Execution Patterns
180
181
### Basic Execution
182
183
Simple command execution within an environment:
184
185
```python
186
from tox.execute.request import ExecuteRequest
187
from tox.execute.local_sub_process import LocalSubProcessExecute
188
189
# Create executor
190
executor = LocalSubProcessExecute()
191
192
# Execute command
193
request = ExecuteRequest(['python', '--version'])
194
status = executor(request)
195
196
print(f"Python version output: {status.out}")
197
```
198
199
### Environment Integration
200
201
Execution integrated with tox environments:
202
203
```python
204
# Within a ToxEnv implementation
205
def execute(self, request: ExecuteRequest) -> ExecuteStatus:
206
"""Execute command in this environment."""
207
208
# Prepare environment-specific execution context
209
env_vars = dict(os.environ)
210
env_vars.update(self.conf.get('setenv', {}))
211
212
# Create request with environment context
213
env_request = ExecuteRequest(
214
cmd=request.cmd,
215
cwd=self.conf.get('changedir', self.work_dir),
216
env=env_vars,
217
run_id=request.run_id
218
)
219
220
# Execute with environment's executor
221
return self._executor(env_request)
222
```
223
224
### Output Streaming
225
226
Handle real-time output during execution:
227
228
```python
229
class StreamingExecutor(Execute):
230
"""Executor with streaming output."""
231
232
def __call__(self, request: ExecuteRequest) -> ExecuteStatus:
233
"""Execute with streaming output."""
234
process = subprocess.Popen(
235
request.cmd,
236
stdout=subprocess.PIPE,
237
stderr=subprocess.PIPE,
238
cwd=request.cwd,
239
env=request.env,
240
text=True,
241
bufsize=1,
242
universal_newlines=True
243
)
244
245
# Stream output in real-time
246
for line in process.stdout:
247
print(line.rstrip())
248
249
exit_code = process.wait()
250
return ExecuteStatus(exit_code, "", "")
251
```
252
253
## Process Management
254
255
### Process Lifecycle
256
257
The execution system manages the complete process lifecycle:
258
259
1. **Preparation**: Setup working directory, environment variables
260
2. **Execution**: Start subprocess with specified parameters
261
3. **Monitoring**: Track process status and capture output
262
4. **Completion**: Collect exit code and output
263
5. **Cleanup**: Release resources and handle interrupts
264
265
### Signal Handling
266
267
Handle interrupts and termination gracefully:
268
269
```python
270
class ManagedExecutor(Execute):
271
"""Executor with signal handling."""
272
273
def __init__(self):
274
self._current_process = None
275
276
def __call__(self, request):
277
"""Execute with signal handling."""
278
try:
279
self._current_process = subprocess.Popen(...)
280
return self._wait_for_completion()
281
except KeyboardInterrupt:
282
self.interrupt()
283
raise
284
285
def interrupt(self):
286
"""Handle interrupt signal."""
287
if self._current_process:
288
self._current_process.terminate()
289
# Wait for graceful termination
290
try:
291
self._current_process.wait(timeout=5)
292
except subprocess.TimeoutExpired:
293
self._current_process.kill()
294
```
295
296
### Resource Management
297
298
Proper resource management for subprocess execution:
299
300
```python
301
def execute_with_resources(self, request: ExecuteRequest) -> ExecuteStatus:
302
"""Execute with proper resource management."""
303
304
with tempfile.TemporaryDirectory() as temp_dir:
305
# Setup temporary resources
306
log_file = Path(temp_dir) / "execution.log"
307
308
try:
309
# Execute command
310
process = subprocess.Popen(...)
311
312
# Monitor and log
313
with open(log_file, 'w') as log:
314
# Capture output
315
pass
316
317
finally:
318
# Cleanup resources
319
if process and process.poll() is None:
320
process.terminate()
321
```
322
323
## Execution Context
324
325
### Environment Variables
326
327
Control execution environment through variables:
328
329
```python
330
def create_execution_env(self, tox_env: ToxEnv) -> dict[str, str]:
331
"""Create execution environment."""
332
333
# Start with system environment
334
env = dict(os.environ)
335
336
# Add tox-specific variables
337
env.update({
338
'TOX_ENV_NAME': tox_env.name,
339
'TOX_ENV_DIR': str(tox_env.env_dir),
340
'TOX_WORK_DIR': str(tox_env.work_dir),
341
})
342
343
# Add environment-specific variables
344
env.update(tox_env.conf.get('setenv', {}))
345
346
# Filter through passenv
347
passenv = tox_env.conf.get('passenv', [])
348
if passenv:
349
filtered_env = {}
350
for key in passenv:
351
if key in env:
352
filtered_env[key] = env[key]
353
env = filtered_env
354
355
return env
356
```
357
358
### Working Directory
359
360
Manage working directory for command execution:
361
362
```python
363
def determine_working_dir(self, tox_env: ToxEnv, request: ExecuteRequest) -> Path:
364
"""Determine working directory for execution."""
365
366
if request.cwd:
367
return request.cwd
368
369
# Check environment configuration
370
changedir = tox_env.conf.get('changedir')
371
if changedir:
372
return Path(changedir)
373
374
# Default to environment directory
375
return tox_env.env_dir
376
```
377
378
## Error Handling
379
380
### Execution Errors
381
382
Handle various execution error conditions:
383
384
```python { .api }
385
class ExecuteError(Exception):
386
"""Base execution error."""
387
388
class CommandNotFoundError(ExecuteError):
389
"""Command not found in PATH."""
390
391
class TimeoutError(ExecuteError):
392
"""Command execution timeout."""
393
394
class InterruptError(ExecuteError):
395
"""Command execution interrupted."""
396
```
397
398
### Error Recovery
399
400
Implement error recovery strategies:
401
402
```python
403
def execute_with_retry(self, request: ExecuteRequest, max_retries: int = 3) -> ExecuteStatus:
404
"""Execute command with retry logic."""
405
406
for attempt in range(max_retries):
407
try:
408
status = self._executor(request)
409
410
# Return on success
411
if status.success:
412
return status
413
414
# Check if retry is appropriate
415
if self._should_retry(status):
416
print(f"Retrying command (attempt {attempt + 1}/{max_retries})")
417
continue
418
else:
419
return status
420
421
except ExecuteError as e:
422
if attempt == max_retries - 1:
423
raise
424
print(f"Execution failed, retrying: {e}")
425
426
return status
427
```
428
429
## Integration with Tox Environments
430
431
The execution system integrates closely with tox environments:
432
433
- **Command Preparation**: Environments prepare commands with substitutions
434
- **Environment Setup**: Execution context includes environment variables and paths
435
- **Output Handling**: Environments control output formatting and logging
436
- **Error Propagation**: Execution errors are handled by environment lifecycle
437
- **Resource Cleanup**: Environments ensure proper cleanup after execution
438
439
This tight integration ensures that commands execute correctly within the isolated environment context while maintaining proper resource management and error handling.