Automatically rerun your tests on file modifications
npx @tessl/cli install tessl/pypi-pytest-watcher@0.4.00
# pytest-watcher
1
2
A command-line tool that automatically reruns pytest tests whenever Python files change, providing fast feedback during development with native filesystem monitoring and interactive controls.
3
4
## Package Information
5
6
- **Package Name**: pytest-watcher
7
- **Package Type**: PyPI
8
- **Language**: Python
9
- **Installation**: `pip install pytest-watcher`
10
- **Python Compatibility**: >= 3.7.0
11
- **License**: MIT
12
13
## Core Imports
14
15
For programmatic usage:
16
17
```python
18
from pytest_watcher import run
19
```
20
21
## Basic Usage
22
23
### Command Line Interface
24
25
Basic usage with current directory:
26
27
```bash
28
ptw .
29
```
30
31
Watch specific directory:
32
33
```bash
34
ptw /path/to/project
35
```
36
37
Pass arguments to pytest:
38
39
```bash
40
ptw . -x --lf --nf
41
```
42
43
### Advanced Usage
44
45
Use different test runner:
46
47
```bash
48
ptw . --runner tox
49
```
50
51
Watch specific file patterns:
52
53
```bash
54
ptw . --patterns '*.py,pyproject.toml'
55
```
56
57
Ignore certain patterns:
58
59
```bash
60
ptw . --ignore-patterns 'settings.py,__pycache__/*'
61
```
62
63
Run immediately and clear screen:
64
65
```bash
66
ptw . --now --clear
67
```
68
69
Custom delay before running tests:
70
71
```bash
72
ptw . --delay 0.5
73
```
74
75
## Configuration
76
77
Configure via `pyproject.toml`:
78
79
```toml
80
[tool.pytest-watcher]
81
now = false
82
clear = true
83
delay = 0.2
84
runner = "pytest"
85
runner_args = ["--verbose", "--tb=short"]
86
patterns = ["*.py"]
87
ignore_patterns = ["*/migrations/*", "*/venv/*"]
88
```
89
90
## Capabilities
91
92
### Command Line Interface
93
94
The primary interface for pytest-watcher, providing file watching with automatic test execution.
95
96
```python { .api }
97
def run() -> None:
98
"""
99
Main entry point that starts the file watcher and interactive loop.
100
101
Parses command line arguments, sets up filesystem monitoring,
102
and runs the interactive terminal interface until quit.
103
104
Returns:
105
None (runs until manually terminated)
106
"""
107
108
def main_loop(trigger: Trigger, config: Config, term: Terminal) -> None:
109
"""
110
Execute one iteration of the main event loop.
111
112
Checks for triggered test runs, executes tests if needed,
113
captures keystrokes, and processes interactive commands.
114
115
Args:
116
trigger (Trigger): Test execution trigger
117
config (Config): Current configuration
118
term (Terminal): Terminal interface
119
"""
120
```
121
122
**CLI Arguments:**
123
124
```bash { .api }
125
ptw <path> [options] [-- pytest_args...]
126
127
Positional Arguments:
128
path Path to watch for file changes
129
130
Optional Arguments:
131
--now Trigger test run immediately on startup
132
--clear Clear terminal screen before each test run
133
--delay DELAY Delay in seconds before triggering test run (default: 0.2)
134
--runner RUNNER Executable for running tests (default: "pytest")
135
--patterns PATTERNS Comma-separated Unix-style file patterns to watch (default: "*.py")
136
--ignore-patterns PATTERNS Comma-separated Unix-style file patterns to ignore
137
--version Show version information
138
```
139
140
### Interactive Controls
141
142
During execution, pytest-watcher provides keyboard shortcuts for controlling test execution:
143
144
```text { .api }
145
Interactive Commands:
146
Enter : Invoke test runner manually
147
r : Reset all runner arguments
148
c : Change runner arguments interactively
149
f : Run only failed tests (add --lf flag)
150
p : Drop to pdb on failure (add --pdb flag)
151
v : Increase verbosity (add -v flag)
152
e : Erase terminal screen
153
w : Show full menu
154
q : Quit pytest-watcher
155
```
156
157
### Configuration System
158
159
Configuration management with file-based and CLI override support.
160
161
```python { .api }
162
class Config:
163
"""
164
Configuration data class for pytest-watcher settings.
165
166
Attributes:
167
path (Path): Directory path to watch for changes
168
now (bool): Whether to run tests immediately on startup
169
clear (bool): Whether to clear screen before test runs
170
delay (float): Seconds to delay before triggering tests
171
runner (str): Test runner executable name
172
runner_args (List[str]): Additional arguments for test runner
173
patterns (List[str]): File patterns to watch for changes
174
ignore_patterns (List[str]): File patterns to ignore
175
"""
176
177
@classmethod
178
def create(cls, namespace: Namespace, extra_args: Optional[List[str]] = None) -> "Config":
179
"""
180
Create Config instance from parsed arguments and configuration file.
181
182
Args:
183
namespace (Namespace): Parsed command line arguments
184
extra_args (Optional[List[str]]): Additional runner arguments
185
186
Returns:
187
Config: Configured instance with merged settings
188
"""
189
```
190
191
```python { .api }
192
def find_config(cwd: Path) -> Optional[Path]:
193
"""
194
Find pyproject.toml configuration file in directory hierarchy.
195
196
Args:
197
cwd (Path): Starting directory for search
198
199
Returns:
200
Optional[Path]: Path to config file if found, None otherwise
201
"""
202
203
def parse_config(path: Path) -> Mapping:
204
"""
205
Parse pytest-watcher configuration from pyproject.toml file.
206
207
Args:
208
path (Path): Path to pyproject.toml file
209
210
Returns:
211
Mapping: Configuration dictionary from [tool.pytest-watcher] section
212
213
Raises:
214
SystemExit: If file parsing fails or contains invalid options
215
"""
216
```
217
218
### File System Monitoring
219
220
Efficient file change detection using native OS APIs via the watchdog library.
221
222
```python { .api }
223
class EventHandler:
224
"""
225
Filesystem event handler for triggering test runs on file changes.
226
227
Monitors file creation, modification, deletion, and move events
228
matching specified patterns while ignoring excluded patterns.
229
"""
230
231
EVENTS_WATCHED = {
232
events.EVENT_TYPE_CREATED,
233
events.EVENT_TYPE_DELETED,
234
events.EVENT_TYPE_MODIFIED,
235
events.EVENT_TYPE_MOVED
236
}
237
238
def __init__(
239
self,
240
trigger: Trigger,
241
patterns: Optional[List[str]] = None,
242
ignore_patterns: Optional[List[str]] = None
243
):
244
"""
245
Initialize event handler with file patterns.
246
247
Args:
248
trigger (Trigger): Trigger instance for test execution
249
patterns (Optional[List[str]]): Unix-style patterns to watch (default: ["*.py"])
250
ignore_patterns (Optional[List[str]]): Unix-style patterns to ignore
251
"""
252
253
@property
254
def patterns(self) -> List[str]:
255
"""File patterns being watched."""
256
257
@property
258
def ignore_patterns(self) -> List[str]:
259
"""File patterns being ignored."""
260
261
def dispatch(self, event: events.FileSystemEvent) -> None:
262
"""
263
Handle filesystem event and trigger test run if pattern matches.
264
265
Args:
266
event (events.FileSystemEvent): Filesystem event from watchdog
267
"""
268
```
269
270
### Test Execution Triggering
271
272
Thread-safe test execution coordination with configurable delays.
273
274
```python { .api }
275
class Trigger:
276
"""
277
Thread-safe trigger for coordinating test execution timing.
278
279
Manages delayed test execution to accommodate file processing
280
tools like formatters that may modify files after save.
281
"""
282
283
def __init__(self, delay: float = 0.0):
284
"""
285
Initialize trigger with optional delay.
286
287
Args:
288
delay (float): Delay in seconds before test execution
289
"""
290
291
def emit(self) -> None:
292
"""Schedule test run with configured delay."""
293
294
def emit_now(self) -> None:
295
"""Schedule immediate test run without delay."""
296
297
def is_active(self) -> bool:
298
"""Check if trigger is currently active."""
299
300
def release(self) -> None:
301
"""Reset trigger to inactive state."""
302
303
def check(self) -> bool:
304
"""Check if trigger should fire based on timing."""
305
```
306
307
### Terminal Interface
308
309
Cross-platform terminal handling for interactive controls and display.
310
311
```python { .api }
312
class Terminal:
313
"""
314
Abstract base class for terminal interface implementations.
315
316
Provides screen clearing, message printing, menu display,
317
and keystroke capture for interactive control.
318
"""
319
320
def clear(self) -> None:
321
"""Clear the terminal screen."""
322
323
def print(self, msg: str) -> None:
324
"""Print message to terminal."""
325
326
def print_header(self, runner_args: List[str]) -> None:
327
"""Print header with current runner arguments."""
328
329
def print_short_menu(self, runner_args: List[str]) -> None:
330
"""Print abbreviated menu with key information."""
331
332
def print_menu(self, runner_args: List[str]) -> None:
333
"""Print full interactive menu with all available commands."""
334
335
def enter_capturing_mode(self) -> None:
336
"""Enable single keystroke capture mode."""
337
338
def capture_keystroke(self) -> Optional[str]:
339
"""Capture single keystroke without blocking."""
340
341
def reset(self) -> None:
342
"""Reset terminal to original state."""
343
344
class PosixTerminal(Terminal):
345
"""
346
POSIX-compatible terminal implementation with interactive features.
347
348
Supports keystroke capture, screen clearing, and terminal state management
349
on Unix-like systems.
350
"""
351
352
def __init__(self) -> None:
353
"""Initialize terminal and save initial state."""
354
355
class DummyTerminal(Terminal):
356
"""
357
Fallback terminal implementation for unsupported platforms.
358
359
Provides basic functionality without interactive features.
360
"""
361
362
def get_terminal() -> Terminal:
363
"""
364
Factory function to get appropriate Terminal implementation.
365
366
Returns:
367
Terminal: PosixTerminal on Unix systems, DummyTerminal on others
368
"""
369
```
370
371
### Interactive Command System
372
373
Pluggable command system for handling keyboard input during file watching.
374
375
```python { .api }
376
class Manager:
377
"""
378
Registry and dispatcher for interactive commands.
379
380
Manages keyboard shortcuts and command execution during interactive mode.
381
"""
382
383
@classmethod
384
def list_commands(cls) -> List[Command]:
385
"""Get list of all registered commands."""
386
387
@classmethod
388
def get_command(cls, character: str) -> Optional[Command]:
389
"""Get command by keyboard character."""
390
391
@classmethod
392
def run_command(
393
cls, character: str, trigger: Trigger, term: Terminal, config: Config
394
) -> None:
395
"""Execute command for given character input."""
396
397
class Command:
398
"""
399
Abstract base class for interactive commands.
400
401
Each command responds to a specific keyboard character and can
402
modify test execution or configuration state.
403
"""
404
405
character: str # Keyboard character that triggers this command
406
caption: str # Display name for menus
407
description: str # Description for help
408
show_in_menu: bool = True # Whether to show in interactive menu
409
410
def run(self, trigger: Trigger, term: Terminal, config: Config) -> None:
411
"""Execute the command with current context."""
412
```
413
414
### Argument Parsing
415
416
Command line argument parsing with support for passing through pytest arguments.
417
418
```python { .api }
419
def parse_arguments(args: Sequence[str]) -> Tuple[Namespace, List[str]]:
420
"""
421
Parse command line arguments for pytest-watcher.
422
423
Separates pytest-watcher specific arguments from arguments
424
that should be passed through to the test runner.
425
426
Args:
427
args (Sequence[str]): Command line arguments to parse
428
429
Returns:
430
Tuple[Namespace, List[str]]: Parsed namespace and runner arguments
431
"""
432
```
433
434
## Types
435
436
```python { .api }
437
from pathlib import Path
438
from typing import List, Optional, Mapping, Sequence, Tuple, Dict, Type
439
from argparse import Namespace
440
from watchdog import events
441
import abc
442
import threading
443
444
# Configuration constants
445
CONFIG_SECTION_NAME = "pytest-watcher"
446
CLI_FIELDS = {"now", "clear", "delay", "runner", "patterns", "ignore_patterns"}
447
CONFIG_FIELDS = CLI_FIELDS | {"runner_args"}
448
449
# Version and timing constants
450
VERSION = "0.4.3"
451
DEFAULT_DELAY = 0.2
452
LOOP_DELAY = 0.1
453
```
454
455
## Examples
456
457
### Basic File Watching
458
459
```python
460
# Start watching current directory
461
import subprocess
462
subprocess.run(["ptw", "."])
463
```
464
465
### Custom Configuration
466
467
Create `pyproject.toml`:
468
469
```toml
470
[tool.pytest-watcher]
471
now = true
472
clear = true
473
delay = 0.1
474
runner = "python -m pytest"
475
runner_args = ["--verbose", "--tb=short", "--durations=10"]
476
patterns = ["*.py", "*.pyi", "pyproject.toml"]
477
ignore_patterns = ["**/migrations/**", "**/venv/**", "**/__pycache__/**"]
478
```
479
480
Then run:
481
482
```bash
483
ptw src/
484
```
485
486
### Programmatic Usage
487
488
```python
489
from pytest_watcher import run
490
import sys
491
492
# Set up arguments as if from command line
493
sys.argv = ["pytest-watcher", ".", "--now", "--clear"]
494
495
# Start watching
496
run()
497
```
498
499
## Error Handling
500
501
Common error conditions:
502
503
- **Invalid path**: SystemExit if watch path doesn't exist
504
- **Config parsing errors**: SystemExit with detailed error message for malformed pyproject.toml
505
- **Terminal initialization**: Falls back to DummyTerminal on unsupported platforms
506
- **Filesystem monitoring**: Logs warnings for inaccessible files or directories
507
508
The tool is designed to be resilient and continue operation when possible, providing informative error messages when intervention is needed.