Development Guide¶
How to work on python-checkup itself.
Prerequisites¶
- Python 3.12+
- uv
Setup¶
Running tests¶
uv run pytest # full suite
uv run pytest tests/test_ruff.py -v # single file
uv run pytest -k "test_name" # single test by name
Project layout¶
python_checkup/
cli.py # Click CLI entry point
runner.py # Parallel analyzer runner
plan.py # ScanPlan: decides what to run
models.py # Core data types (Diagnostic, HealthReport, etc.)
config.py # pyproject.toml config loader
analysis_request.py # AnalysisRequest passed to every analyzer
analyzer_catalog.py # ANALYZER_CATALOG: maps names to classes
discovery.py # Python file discovery
detection.py # Framework and Python version detection
cache.py # Per-file result cache
dedup.py # Cross-analyzer deduplication
diff.py # Git diff integration
analyzers/
registry.py # Entry-point plugin loader
ruff.py # Ruff (quality, security, complexity)
bandit.py # Bandit (security)
mypy.py # mypy (type safety)
basedpyright.py # basedpyright (type safety, optional)
radon.py # Radon (complexity)
vulture.py # Vulture (dead code)
deptry.py # deptry (dependency hygiene)
dependency_vulns.py # OSV vulnerability scanner + advisory cache
detect_secrets.py # detect-secrets (security, optional)
typos.py # typos (quality, optional)
cached.py # CachedAnalyzer wrapper
dependencies/
discovery.py # Lockfile/manifest discovery and parsing
scoring/
engine.py # Weight redistribution + per-category scoring
formatters/
human.py # Rich terminal output
json_fmt.py # JSON output
mcp/
server.py # MCP server for AI editor integration
Architecture¶
Data flow¶
- CLI (
cli.py) parses args, loads config, callsbuild_scan_plan() - ScanPlan (
plan.py) decides which analyzers and categories to run - Runner (
runner.py) executes analyzers in parallel viaasyncio - Each analyzer receives an
AnalysisRequestand returnslist[Diagnostic] - Dedup merges overlapping findings across tools
- Scoring engine computes per-category scores with weight redistribution
- Formatter renders the
HealthReportto terminal or JSON
Key types¶
Diagnostic-- a single finding (file, line, severity, rule, message, fix)HealthReport-- the top-level result (score, label, category scores, diagnostics)AnalysisRequest-- input to every analyzer (files, config, categories, project root)ScanPlan-- what analyzers/categories to run, which are skippedCoverageInfo-- how complete the analysis was (full/partial/limited)
Analyzer protocol¶
Every analyzer (built-in or plugin) must satisfy:
class MyAnalyzer:
@property
def name(self) -> str: ...
@property
def category(self) -> Category: ...
async def is_available(self) -> bool: ...
async def analyze(self, request: AnalysisRequest) -> list[Diagnostic]: ...
See docs/plugins.md for the full plugin development guide.
Scoring¶
Weights default to quality=25, types=20, security=20, complexity=15, dead_code=10, dependencies=10. When a category has no active analyzer, its weight is redistributed proportionally to the remaining categories. The output always explains when this happens.
Caching¶
- Per-file cache:
.python-checkup-cache/v1/-- keyed by file content hash, skips re-analysis of unchanged files - Advisory cache:
.python-checkup-cache/v2/advisories/-- caches OSV vulnerability responses for 24 hours
Both are bypassed with --no-cache.
Adding a new built-in analyzer¶
- Create
python_checkup/analyzers/my_tool.pyimplementing the analyzer protocol - Add the class to
ANALYZER_CATALOGinanalyzer_catalog.py - Map the analyzer name to categories in
scoring/engine.py_categories_from_analyzers() - If the tool is optional, add it to
ANALYZER_EXTRAinformatters/human.pyand create a pip extra inpyproject.toml - Add tests in
tests/test_my_tool.py - Run the full suite:
uv run pytest
Code conventions¶
- All analyzers are async
- Use
AnalysisRequestas the sole input -- do not pass raw file lists - Return
list[Diagnostic]-- never raise fromanalyze() is_available()should returnFalseif the tool is missing, not raise- Tests use
tmp_pathfixtures and mock subprocess/HTTP calls - The
Diagnosticdataclass is frozen -- construct new instances, don't mutate