Automatically rerun your tests on file modifications
npx @tessl/cli install tessl/pypi-pytest-watcher@0.4.0A command-line tool that automatically reruns pytest tests whenever Python files change, providing fast feedback during development with native filesystem monitoring and interactive controls.
pip install pytest-watcherFor programmatic usage:
from pytest_watcher import runBasic usage with current directory:
ptw .Watch specific directory:
ptw /path/to/projectPass arguments to pytest:
ptw . -x --lf --nfUse different test runner:
ptw . --runner toxWatch specific file patterns:
ptw . --patterns '*.py,pyproject.toml'Ignore certain patterns:
ptw . --ignore-patterns 'settings.py,__pycache__/*'Run immediately and clear screen:
ptw . --now --clearCustom delay before running tests:
ptw . --delay 0.5Configure via pyproject.toml:
[tool.pytest-watcher]
now = false
clear = true
delay = 0.2
runner = "pytest"
runner_args = ["--verbose", "--tb=short"]
patterns = ["*.py"]
ignore_patterns = ["*/migrations/*", "*/venv/*"]The primary interface for pytest-watcher, providing file watching with automatic test execution.
def run() -> None:
"""
Main entry point that starts the file watcher and interactive loop.
Parses command line arguments, sets up filesystem monitoring,
and runs the interactive terminal interface until quit.
Returns:
None (runs until manually terminated)
"""
def main_loop(trigger: Trigger, config: Config, term: Terminal) -> None:
"""
Execute one iteration of the main event loop.
Checks for triggered test runs, executes tests if needed,
captures keystrokes, and processes interactive commands.
Args:
trigger (Trigger): Test execution trigger
config (Config): Current configuration
term (Terminal): Terminal interface
"""CLI Arguments:
ptw <path> [options] [-- pytest_args...]
Positional Arguments:
path Path to watch for file changes
Optional Arguments:
--now Trigger test run immediately on startup
--clear Clear terminal screen before each test run
--delay DELAY Delay in seconds before triggering test run (default: 0.2)
--runner RUNNER Executable for running tests (default: "pytest")
--patterns PATTERNS Comma-separated Unix-style file patterns to watch (default: "*.py")
--ignore-patterns PATTERNS Comma-separated Unix-style file patterns to ignore
--version Show version informationDuring execution, pytest-watcher provides keyboard shortcuts for controlling test execution:
Interactive Commands:
Enter : Invoke test runner manually
r : Reset all runner arguments
c : Change runner arguments interactively
f : Run only failed tests (add --lf flag)
p : Drop to pdb on failure (add --pdb flag)
v : Increase verbosity (add -v flag)
e : Erase terminal screen
w : Show full menu
q : Quit pytest-watcherConfiguration management with file-based and CLI override support.
class Config:
"""
Configuration data class for pytest-watcher settings.
Attributes:
path (Path): Directory path to watch for changes
now (bool): Whether to run tests immediately on startup
clear (bool): Whether to clear screen before test runs
delay (float): Seconds to delay before triggering tests
runner (str): Test runner executable name
runner_args (List[str]): Additional arguments for test runner
patterns (List[str]): File patterns to watch for changes
ignore_patterns (List[str]): File patterns to ignore
"""
@classmethod
def create(cls, namespace: Namespace, extra_args: Optional[List[str]] = None) -> "Config":
"""
Create Config instance from parsed arguments and configuration file.
Args:
namespace (Namespace): Parsed command line arguments
extra_args (Optional[List[str]]): Additional runner arguments
Returns:
Config: Configured instance with merged settings
"""def find_config(cwd: Path) -> Optional[Path]:
"""
Find pyproject.toml configuration file in directory hierarchy.
Args:
cwd (Path): Starting directory for search
Returns:
Optional[Path]: Path to config file if found, None otherwise
"""
def parse_config(path: Path) -> Mapping:
"""
Parse pytest-watcher configuration from pyproject.toml file.
Args:
path (Path): Path to pyproject.toml file
Returns:
Mapping: Configuration dictionary from [tool.pytest-watcher] section
Raises:
SystemExit: If file parsing fails or contains invalid options
"""Efficient file change detection using native OS APIs via the watchdog library.
class EventHandler:
"""
Filesystem event handler for triggering test runs on file changes.
Monitors file creation, modification, deletion, and move events
matching specified patterns while ignoring excluded patterns.
"""
EVENTS_WATCHED = {
events.EVENT_TYPE_CREATED,
events.EVENT_TYPE_DELETED,
events.EVENT_TYPE_MODIFIED,
events.EVENT_TYPE_MOVED
}
def __init__(
self,
trigger: Trigger,
patterns: Optional[List[str]] = None,
ignore_patterns: Optional[List[str]] = None
):
"""
Initialize event handler with file patterns.
Args:
trigger (Trigger): Trigger instance for test execution
patterns (Optional[List[str]]): Unix-style patterns to watch (default: ["*.py"])
ignore_patterns (Optional[List[str]]): Unix-style patterns to ignore
"""
@property
def patterns(self) -> List[str]:
"""File patterns being watched."""
@property
def ignore_patterns(self) -> List[str]:
"""File patterns being ignored."""
def dispatch(self, event: events.FileSystemEvent) -> None:
"""
Handle filesystem event and trigger test run if pattern matches.
Args:
event (events.FileSystemEvent): Filesystem event from watchdog
"""Thread-safe test execution coordination with configurable delays.
class Trigger:
"""
Thread-safe trigger for coordinating test execution timing.
Manages delayed test execution to accommodate file processing
tools like formatters that may modify files after save.
"""
def __init__(self, delay: float = 0.0):
"""
Initialize trigger with optional delay.
Args:
delay (float): Delay in seconds before test execution
"""
def emit(self) -> None:
"""Schedule test run with configured delay."""
def emit_now(self) -> None:
"""Schedule immediate test run without delay."""
def is_active(self) -> bool:
"""Check if trigger is currently active."""
def release(self) -> None:
"""Reset trigger to inactive state."""
def check(self) -> bool:
"""Check if trigger should fire based on timing."""Cross-platform terminal handling for interactive controls and display.
class Terminal:
"""
Abstract base class for terminal interface implementations.
Provides screen clearing, message printing, menu display,
and keystroke capture for interactive control.
"""
def clear(self) -> None:
"""Clear the terminal screen."""
def print(self, msg: str) -> None:
"""Print message to terminal."""
def print_header(self, runner_args: List[str]) -> None:
"""Print header with current runner arguments."""
def print_short_menu(self, runner_args: List[str]) -> None:
"""Print abbreviated menu with key information."""
def print_menu(self, runner_args: List[str]) -> None:
"""Print full interactive menu with all available commands."""
def enter_capturing_mode(self) -> None:
"""Enable single keystroke capture mode."""
def capture_keystroke(self) -> Optional[str]:
"""Capture single keystroke without blocking."""
def reset(self) -> None:
"""Reset terminal to original state."""
class PosixTerminal(Terminal):
"""
POSIX-compatible terminal implementation with interactive features.
Supports keystroke capture, screen clearing, and terminal state management
on Unix-like systems.
"""
def __init__(self) -> None:
"""Initialize terminal and save initial state."""
class DummyTerminal(Terminal):
"""
Fallback terminal implementation for unsupported platforms.
Provides basic functionality without interactive features.
"""
def get_terminal() -> Terminal:
"""
Factory function to get appropriate Terminal implementation.
Returns:
Terminal: PosixTerminal on Unix systems, DummyTerminal on others
"""Pluggable command system for handling keyboard input during file watching.
class Manager:
"""
Registry and dispatcher for interactive commands.
Manages keyboard shortcuts and command execution during interactive mode.
"""
@classmethod
def list_commands(cls) -> List[Command]:
"""Get list of all registered commands."""
@classmethod
def get_command(cls, character: str) -> Optional[Command]:
"""Get command by keyboard character."""
@classmethod
def run_command(
cls, character: str, trigger: Trigger, term: Terminal, config: Config
) -> None:
"""Execute command for given character input."""
class Command:
"""
Abstract base class for interactive commands.
Each command responds to a specific keyboard character and can
modify test execution or configuration state.
"""
character: str # Keyboard character that triggers this command
caption: str # Display name for menus
description: str # Description for help
show_in_menu: bool = True # Whether to show in interactive menu
def run(self, trigger: Trigger, term: Terminal, config: Config) -> None:
"""Execute the command with current context."""Command line argument parsing with support for passing through pytest arguments.
def parse_arguments(args: Sequence[str]) -> Tuple[Namespace, List[str]]:
"""
Parse command line arguments for pytest-watcher.
Separates pytest-watcher specific arguments from arguments
that should be passed through to the test runner.
Args:
args (Sequence[str]): Command line arguments to parse
Returns:
Tuple[Namespace, List[str]]: Parsed namespace and runner arguments
"""from pathlib import Path
from typing import List, Optional, Mapping, Sequence, Tuple, Dict, Type
from argparse import Namespace
from watchdog import events
import abc
import threading
# Configuration constants
CONFIG_SECTION_NAME = "pytest-watcher"
CLI_FIELDS = {"now", "clear", "delay", "runner", "patterns", "ignore_patterns"}
CONFIG_FIELDS = CLI_FIELDS | {"runner_args"}
# Version and timing constants
VERSION = "0.4.3"
DEFAULT_DELAY = 0.2
LOOP_DELAY = 0.1# Start watching current directory
import subprocess
subprocess.run(["ptw", "."])Create pyproject.toml:
[tool.pytest-watcher]
now = true
clear = true
delay = 0.1
runner = "python -m pytest"
runner_args = ["--verbose", "--tb=short", "--durations=10"]
patterns = ["*.py", "*.pyi", "pyproject.toml"]
ignore_patterns = ["**/migrations/**", "**/venv/**", "**/__pycache__/**"]Then run:
ptw src/from pytest_watcher import run
import sys
# Set up arguments as if from command line
sys.argv = ["pytest-watcher", ".", "--now", "--clear"]
# Start watching
run()Common error conditions:
The tool is designed to be resilient and continue operation when possible, providing informative error messages when intervention is needed.