Skip to main content

A pytest plugin that selectively runs tests impacted by code changes via git introspection, AST parsing, and dependency graph analysis.

Project description

pytest-impacted

CI GitHub License PyPI - Version PyPI - Python Version

Run only the tests that matter. A pytest plugin that uses git diff, AST parsing, and dependency graph analysis to selectively run tests impacted by your code changes.

pytest --impacted --impacted-module=my_package     # unstaged changes
pytest --impacted --impacted-module=my_package \
       --impacted-git-mode=branch \
       --impacted-base-branch=main                 # branch changes vs main

Key Features

Feature Details
:zap: Fast feedback Only runs tests affected by your changes — skip the rest
:deciduous_tree: Dependency-aware Follows import chains transitively, not just direct file changes
:gear: No imports at analysis time Filesystem discovery + AST parsing — no module-level side effects
:test_tube: pytest-native Works as a standard pytest plugin with familiar CLI options
:wrench: conftest.py aware Changes to conftest.py automatically impact all tests in scope
:package: Dependency-file aware Changes to uv.lock, requirements.txt, pyproject.toml etc. trigger all tests
:building_construction: CI-friendly Standalone impacted-tests CLI for two-stage CI pipelines
:rocket: Rust-accelerated Optional Rust extension for 37-65x faster import parsing on large codebases
:electric_plug: Extensible Third-party strategies installable as plugins via Python entry points
:shield: Helpful errors Validates config early with clear messages and suggestions

[!CAUTION] This project is currently in beta. Please report bugs via the Issues tab.


Installation

pip install pytest-impacted

Or with uv:

uv add pytest-impacted

For 37-65x faster import parsing on large codebases, install with the optional Rust extension:

pip install pytest-impacted[fast]

Requires Python 3.11+.


Quick Start

1. Run tests impacted by uncommitted changes:

pytest --impacted --impacted-module=my_package

2. Run tests impacted by branch changes (vs main):

pytest --impacted \
       --impacted-module=my_package \
       --impacted-git-mode=branch \
       --impacted-base-branch=main

3. Include tests outside the package directory:

pytest --impacted \
       --impacted-module=my_package \
       --impacted-tests-dir=tests

That's it. Unaffected tests are automatically skipped.


How It Works

Git diff → Changed files → Module resolution → AST import parsing → Dependency graph → Impacted tests
                         ↘ Dependency file detection → All tests (if dep files changed)
  1. Git introspection identifies which files changed (unstaged edits or branch diff)
  2. Filesystem discovery maps file paths to Python module names — without importing anything
  3. AST parsing (via astroid, or the optional Rust extension using ruff's parser) extracts import relationships from source files
  4. Dependency graph (via NetworkX) traces transitive dependencies from changed modules to test modules
  5. Dependency file detection — if files like uv.lock, requirements.txt, or pyproject.toml changed, all tests are marked as impacted regardless of import analysis
  6. Test filtering skips tests whose modules are not in the impact set

The philosophy is to err on the side of caution: we favor false positives (running a test that didn't need to run) over false negatives (missing a test that should have run).

Strategy-Based Architecture

Impact analysis is pluggable via a strategy pattern. The default pipeline combines three strategies:

Strategy What it does
ASTImpactStrategy Traces transitive import dependencies through the dependency graph
PytestImpactStrategy Extends AST analysis with pytest-specific knowledge — when a conftest.py file changes, all tests in its directory and subdirectories are marked as impacted
DependencyFileImpactStrategy When dependency files change (uv.lock, requirements.txt, pyproject.toml, etc.), all tests are marked as impacted

All strategies are combined via CompositeImpactStrategy, which deduplicates and merges their results. Dependency file detection is enabled by default and can be disabled with --no-impacted-dep-files.

Custom Strategy Extensions

Third-party packages can register custom strategies as installable plugins via Python entry points. Once installed, they are automatically discovered and composed into the analysis pipeline:

# In your extension's pyproject.toml
[project.entry-points."pytest_impacted.strategies"]
my_strategy = "my_package.strategy:MyCustomStrategy"
from pytest_impacted import ImpactStrategy, ConfigOption, resolve_impacted_tests

class MyCustomStrategy(ImpactStrategy):
    config_options = [
        ConfigOption(name="threshold", help="Min score to consider", type=int, default=80),
    ]

    def __init__(self, threshold: int = 80):
        self.threshold = threshold

    def find_impacted_tests(self, changed_files, impacted_modules, ns_module, *, dep_tree, **kwargs):
        # dep_tree is the pre-built dependency graph (nx.DiGraph), shared across all strategies.
        # resolve_impacted_tests provides standard graph traversal.
        return resolve_impacted_tests(impacted_modules, dep_tree)

Users can configure extensions via CLI (--impacted-ext-my-strategy-threshold 90) or pyproject.toml, and disable them with --impacted-disable-ext my_strategy. Extensions can alternatively use duck-typing (any class with a find_impacted_tests method — no inheritance required) and can set a priority class variable to control execution order. For extensions that need to walk the source tree or reuse the core AST parser, discover_submodules and parse_file_imports are also exported from pytest_impacted.

Strategies may also override three optional lifecycle hooks (all with no-op defaults, so existing extensions keep working unchanged):

  • enrich_dep_tree(dep_tree, *, ns_module, tests_package, root_dir, session) — inject synthetic edges for relationships invisible to static import analysis (DI bindings, codegen, plugin discovery). The hook receives full context so scan-based enrichers can walk the source tree with discover_submodules + parse_file_imports and add edges the built-in AST traversal will then follow automatically.
  • setup(*, ns_module, tests_package, root_dir, session, dep_tree) — one-time per-run warm-up; the right place for expensive O(source-tree) indexing instead of lazy-init inside find_impacted_tests.
  • teardown() — release per-run state; always fires, even if find_impacted_tests raises.

See the Usage Guide for the full reference and worked examples.

You can also supply a custom strategy programmatically via the get_impacted_tests() API:

from pytest_impacted.api import get_impacted_tests

impacted = get_impacted_tests(
    impacted_git_mode="branch",
    impacted_base_branch="main",
    root_dir=Path("."),
    ns_module="my_package",
    strategy=MyCustomStrategy(),
)

Usage

Git Modes

Mode Flag What it compares
unstaged (default) --impacted-git-mode=unstaged Working directory changes + untracked files
branch --impacted-git-mode=branch All commits on current branch vs base branch

The --impacted-base-branch flag accepts any valid git ref, including expressions like HEAD~4.

External Tests Directory

When your tests live outside the namespace package (a common layout), use --impacted-tests-dir so the dependency graph includes them:

pytest --impacted \
       --impacted-module=my_package \
       --impacted-tests-dir=tests

Monorepo / src-Layout Support

The plugin works in monorepos where the Python project is nested in a subdirectory (the .git directory doesn't need to be in the working directory — parent directories are searched automatically).

For src-layout projects (e.g. src/my_package/), point --impacted-module at the full path including the src/ prefix:

# From the project directory (e.g. monorepo/backend/)
pytest --impacted \
       --impacted-module=src/my_package \
       --impacted-tests-dir=tests

The plugin automatically detects that src/ is not a Python package and uses the correct importable module name (my_package) for dependency analysis.

CI Integration

For CI pipelines where git access and test execution happen in separate stages, use the impacted-tests CLI to generate the test file list:

# Stage 1: identify impacted tests
impacted-tests --module=my_package --git-mode=branch --base-branch=main > impacted_tests.txt

# Stage 2: run only those tests
pytest $(cat impacted_tests.txt)

Configuration via pyproject.toml

All CLI options can be set as defaults in your pyproject.toml (or pytest.ini):

[tool.pytest.ini_options]
impacted = true
impacted_module = "my_package"
impacted_git_mode = "branch"
impacted_base_branch = "main"
impacted_tests_dir = "tests"
# no_impacted_dep_files = true  # uncomment to disable dep file detection

CLI flags override these defaults.

All Options

Option Default Description
--impacted false Enable the plugin
--impacted-module (required) Top-level Python package to analyze
--impacted-git-mode unstaged Git comparison mode: unstaged or branch
--impacted-base-branch (required for branch mode) Base branch/ref for branch-mode comparison
--impacted-tests-dir None Directory containing tests outside the package
--no-impacted-dep-files false Disable dependency file change detection
--impacted-disable-ext [] Disable a strategy extension by name (repeatable)

Alternatives

Project Notes
pytest-testmon Most popular option. Uses coverage-based granular change tracking. More precise but heavier; may conflict with other plugins.
pytest-picked Runs tests from directly modified files only — no transitive dependency analysis.
pytest-affected Appears unmaintained, no source repository.

Performance: Optional Rust Acceleration

For large codebases, install the optional Rust extension to accelerate import parsing by 37-65x:

pip install pytest-impacted[fast]

This installs pytest-impacted-rs, a pre-built Rust extension using ruff's parser and rayon for parallel file processing. The extension is automatically detected at runtime — no configuration needed. When unavailable, the pure-Python (astroid) implementation is used.


Development

This project uses uv for dependency management.

# Setup
uv sync --all-extras --dev

# Run tests
uv run python -m pytest

# Run tests with coverage
uv run python -m pytest --cov=pytest_impacted --cov-branch tests

# Lint + format + type check
pre-commit run --all-files

# Install with Rust acceleration (pre-built wheels, no Rust toolchain needed)
pip install pytest-impacted[fast]

# Or build from source (requires Rust toolchain)
pip install maturin
cd rust && maturin develop --release

# Run parsing benchmarks
python -m benchmarks.bench_parsing

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_impacted-0.23.0.tar.gz (722.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_impacted-0.23.0-py3-none-any.whl (672.5 kB view details)

Uploaded Python 3

File details

Details for the file pytest_impacted-0.23.0.tar.gz.

File metadata

  • Download URL: pytest_impacted-0.23.0.tar.gz
  • Upload date:
  • Size: 722.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_impacted-0.23.0.tar.gz
Algorithm Hash digest
SHA256 9fa857501e8a4d3446d23e9a0a6dc6731e03417489804831aa41f0a41f3bd76b
MD5 d3eb73a6feb5ac389817e3e5b45cb446
BLAKE2b-256 d78bb0dd1fd5ec17c21ffc8dbac80281678456375594782e34d8de5522f74402

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_impacted-0.23.0.tar.gz:

Publisher: publish-to-pypi.yml on promptromp/pytest-impacted

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_impacted-0.23.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_impacted-0.23.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1f423980999a9fdd6eec023dedf3153575651eb7d0160ec4caa42e737a5e605c
MD5 6310721f6ecb895227bd6f33275525de
BLAKE2b-256 864c21a26d47dcd3a3f7527baa7bac59f3007d90a6e904b924b48b443020b2eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_impacted-0.23.0-py3-none-any.whl:

Publisher: publish-to-pypi.yml on promptromp/pytest-impacted

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page