Smart test selection based on AST-level code dependency analysis
Project description
pytest-depper
Smart test selection based on AST-level code dependency analysis.
Run only the tests you need, not all the tests you have.
The Problem
Imagine you have a 3,000-line Python file with 2,000 individual tests covering various parts of it. You make a small change—just 8 lines. Should you run all 2,000 tests? No.
Most test selection tools either:
- Run everything (slow)
- Use coverage data from previous runs (requires pre-computed data, breaks with new code)
- Rely on naming conventions (fragile, incomplete)
The Solution
pytest-depper analyzes your code's actual import dependencies at the AST level to determine exactly which tests are affected by your changes.
When you change those 8 lines, pytest-depper:
- Diffs your branch against main to find changed files
- Builds a complete dependency graph of your codebase using AST parsing
- Traces which tests import (directly or transitively) the changed code
- Runs only those ~20 affected tests instead of all 2,000
No pre-computation. No cached state. No guessing.
Key Advantages Over Similar Tools
vs pytest-testmon
| Feature | pytest-depper | pytest-testmon |
|---|---|---|
| Requires previous run data | ❌ No | ✅ Yes (.testmon file) |
| Works on fresh branches | ✅ Yes | ❌ No (needs to build cache first) |
| AST-level analysis | ✅ Yes | ✅ Yes |
| Git integration | ✅ Built-in | ❌ Manual |
| Dependency graph visualization | ✅ Yes (--depper-debug) |
⚠️ Limited |
Why this matters: pytest-testmon requires running your full test suite at least once on the main branch to build its cache. If you check out a fresh branch or clone a new repo, you have no cache. pytest-depper works immediately by analyzing your code structure.
vs pytest-picked
pytest-picked only looks at changed test files and changed source files—it doesn't analyze the dependency graph. If you change models/user.py, and services/auth.py imports it, and tests/test_api.py tests the auth service, pytest-picked won't detect this transitive relationship.
vs Manual Test Selection
Running tests manually is error-prone. You might:
- Forget which tests cover which code
- Miss transitive dependencies (A imports B, B imports C, you changed C)
- Over-select tests out of caution (wasting time)
Installation
pip install pytest-depper
Or with poetry:
poetry add --group dev pytest-depper
Quick Start
As a Pytest Plugin
Simply add the --depper flag to your pytest command:
# Run only tests affected by changes compared to main branch
pytest --depper
# Compare against a different branch
pytest --depper --depper-base-branch=develop
# Show detailed dependency analysis
pytest --depper --depper-debug
As a Standalone CLI
Analyze dependencies without running tests:
# Show which tests would run
pytest-depper
# Compare to a different branch
pytest-depper --base-branch develop
# Show detailed dependency information
pytest-depper --debug
How It Works
Step 1: Find Changed Files
git diff --name-only origin/main...HEAD
Output:
src/models/user.py
src/utils/validators.py
Step 2: Build Dependency Graph
pytest-depper parses all Python files in your project using AST analysis to understand imports:
# src/models/user.py
from .base import BaseModel # → src/models/base.py
from ..utils.validators import validate_email # → src/utils/validators.py
# src/services/auth.py
from ..models.user import User # → src/models/user.py
# tests/test_auth.py
from src.services.auth import AuthService # → src/services/auth.py
This creates both forward and reverse dependency graphs:
- Forward: What does each file import?
- Reverse: What imports each file?
Step 3: Trace Affected Tests
For each changed file, pytest-depper:
- Finds all files that import it (directly or transitively)
- Filters to only test files
- Returns the minimal set of tests that must run
user.py changed
↓ imported by services/auth.py
↓ imported by tests/test_auth.py
✅ Run tests/test_auth.py
validators.py changed
↓ imported by models/user.py
↓ imported by services/auth.py
↓ imported by tests/test_auth.py
✅ Run tests/test_auth.py
↓ also imported by tests/test_validators.py
✅ Run tests/test_validators.py
Result: Only 2 test files run instead of your entire suite.
Configuration
Pytest Plugin Options
--depper
Enable smart test selection (default: False)
--depper-base-branch=BRANCH
Base branch to compare against (default: main)
--depper-debug
Print detailed dependency analysis (default: False)
--depper-run-all-on-error
Run all tests if no changed files detected or analysis fails (default: False)
Programmatic Usage
from pathlib import Path
from pytest_depper import DependencyAnalyzer
# Initialize analyzer
analyzer = DependencyAnalyzer(
project_root=Path("."),
exclusion_patterns=["venv", ".venv", "build"],
test_patterns=["test_", "_test.py", "/tests/"]
)
# Get changed files (you provide this)
changed_files = ["src/models/user.py", "src/utils/validators.py"]
# Get affected tests
affected_tests = analyzer.get_affected_tests(changed_files)
print(f"Run these tests: {affected_tests}")
# Debug: Print dependency info
analyzer.print_dependency_info(changed_files)
Real-World Example
Before pytest-depper
$ pytest
======================== test session starts ========================
collected 2847 items
tests/test_auth.py::test_login PASSED [ 0%]
tests/test_auth.py::test_logout PASSED [ 0%]
tests/test_auth.py::test_register PASSED [ 0%]
... (2844 more tests) ...
===================== 2847 passed in 284.32s =======================
⏱️ 4 minutes 44 seconds
After pytest-depper
$ pytest --depper
Depper: Found 2 changed files
- src/models/user.py
- src/utils/validators.py
Depper: 2 test files affected by changes
Depper: Selected 23 tests, deselected 2824 tests
======================== test session starts ========================
collected 23 items
tests/test_auth.py::test_login PASSED [ 4%]
tests/test_auth.py::test_logout PASSED [ 8%]
tests/test_auth.py::test_register PASSED [12%]
tests/test_validators.py::test_email_validation PASSED [16%]
... (19 more tests) ...
===================== 23 passed in 3.71s =======================
⏱️ 3.71 seconds
76x faster for a typical small change.
CI/CD Integration
GitHub Actions
name: Tests
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Need full history for git diff
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest-depper
- name: Run affected tests
run: pytest --depper --depper-run-all-on-error
GitLab CI
test:
script:
- pip install -r requirements.txt
- pip install pytest-depper
- pytest --depper --depper-base-branch=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
Advanced Usage
Custom Test Patterns
analyzer = DependencyAnalyzer(
test_patterns=[
"test_", # files starting with test_
"_test.py", # files ending with _test.py
"/tests/", # files in tests/ directories
"/integration/", # custom: integration test directory
"spec.py", # custom: spec files
]
)
Custom Exclusion Patterns
analyzer = DependencyAnalyzer(
exclusion_patterns=[
"venv",
".venv",
"node_modules",
"build",
"dist",
".git",
"__pycache__",
"migrations", # custom: ignore migration files
"scripts", # custom: ignore utility scripts
]
)
Debug Output
$ pytest --depper --depper-debug
=== Dependency Analysis ===
Changed file: src/models/user.py
Dependencies: {'src/models/base.py', 'src/utils/validators.py'}
Files that depend on this: {'src/services/auth.py', 'src/services/user_service.py', 'tests/test_auth.py', 'tests/test_user.py'}
Direct test coverage: {'tests/test_user.py'}
Changed file: src/utils/validators.py
Dependencies: set()
Files that depend on this: {'src/models/user.py', 'tests/test_validators.py'}
Direct test coverage: {'tests/test_validators.py'}
Depper: Selected 4 test files
✓ tests/test_auth.py
✓ tests/test_user.py
✓ tests/test_user_service.py
✓ tests/test_validators.py
Limitations
What pytest-depper CAN'T detect:
-
Dynamic imports
module = __import__("my_module") # Not detected importlib.import_module("my_module") # Not detected
-
String-based references
class_name = "UserModel" cls = globals()[class_name] # Not detected
-
Data-driven changes
- Changing a JSON config file won't trigger tests that read it
- Modifying database migrations won't trigger related tests
- Updating
.envfiles won't trigger affected tests
-
Indirect behavioral dependencies
- If you change a function's behavior but not its signature, tests that depend on that behavior will run only if they import the function
Recommended approach:
- Use
--depper-run-all-on-errorin CI to be safe - Run full test suite on main/develop branches
- Use pytest-depper for PR/feature branches to speed up development
- Combine with coverage tools to ensure you have adequate test coverage
FAQ
Q: Does this replace pytest-cov or coverage.py?
A: No. pytest-depper selects which tests to run. Coverage tools measure how much of your code is tested. Use them together.
Q: What if I rename a file?
A: Git treats renames as delete + add. pytest-depper will see the new file as changed and run all tests that import it.
Q: Does it work with monorepos?
A: Yes. Set project_root to your package directory:
analyzer = DependencyAnalyzer(project_root=Path("packages/my-package"))
Q: What Python versions are supported?
A: Python 3.8+. The AST analysis uses sys.stdlib_module_names (Python 3.10+) but falls back gracefully.
Q: Can I use this with tox or nox?
A: Yes:
# tox.ini
[testenv]
deps = pytest-depper
commands = pytest --depper
Q: Why "depper"?
A: Dependency-based test runner, or "deeper" analysis. Plus it's short and memorable.
Contributing
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
Development Setup
# Clone the repo
git clone https://github.com/ryx2/pytest-depper.git
cd pytest-depper
# Install dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run linters
ruff check .
mypy pytest_depper
License
MIT License. See LICENSE for details.
Credits
Inspired by:
- pytest-testmon - for the concept of dependency-based test selection
- pytest-picked - for git integration with pytest
Built with ❤️ for developers who value their time.
Star this repo if pytest-depper saves you time!
Report issues at https://github.com/ryx2/pytest-depper/issues
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_depper-0.1.0.tar.gz.
File metadata
- Download URL: pytest_depper-0.1.0.tar.gz
- Upload date:
- Size: 18.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58b3bef44f252c546ab50a83bfb6ec2f3a7336ac18f18d14444c48a34fe31187
|
|
| MD5 |
df3ae5a52eefc7fc90ddbd639c6f3acf
|
|
| BLAKE2b-256 |
961139f13bf2d6ec5f1162de5c11ad5040d80b2de853d72f8e594754e64fe2b8
|
File details
Details for the file pytest_depper-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pytest_depper-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4aee801fa099253cc62de3765c4b91c3e64f327192e16bbb14de0f8ccdfaafea
|
|
| MD5 |
869149c1e2a9f0ebb7be3796b7b1248d
|
|
| BLAKE2b-256 |
0d9bb4dc7ab7f271364567a87a616ac5558c93a67300fff93faadb2e0676ba3c
|