Plugins¶
python-checkup supports two plugin surfaces:
- analyzer plugins for new analysis tools
- framework plugins for new framework detection, framework-aware checks, whitelists, and stubs metadata
Both are loaded via Python entry points.
Analyzer Plugins¶
Analyzer plugins let you add new tools that produce Diagnostic items and participate in scoring.
How Analyzer Plugins Work¶
- You create a class that satisfies the
Analyzerprotocol (structural typing -- no import needed) - You register it as an entry point in your package's
pyproject.toml - After
uv tool install your-packageoruvx --from 'your-package' ..., python-checkup auto-discovers it
Creating an Analyzer Plugin¶
1. Implement the analyzer¶
# my_package/__init__.py
from __future__ import annotations
import asyncio
import json
import shutil
from pathlib import Path
class MyAnalyzer:
"""Custom analyzer for python-checkup."""
@property
def name(self) -> str:
return "my-analyzer"
@property
def category(self):
from python_checkup.models import Category
return Category.QUALITY
async def is_available(self) -> bool:
return shutil.which("my-tool") is not None
async def analyze(self, request) -> list:
from python_checkup.models import Diagnostic, Severity, Category
cmd = ["my-tool", "--json"] + [str(f) for f in request.files]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await proc.communicate()
diagnostics = []
for item in json.loads(stdout.decode()):
diagnostics.append(Diagnostic(
file_path=Path(item["file"]),
line=item["line"],
column=item.get("column", 0),
severity=Severity.WARNING,
rule_id=item["code"],
tool="my-analyzer",
category=Category.QUALITY,
message=item["message"],
))
return diagnostics
2. Register via entry point¶
# pyproject.toml
[project]
name = "python-checkup-my-analyzer"
dependencies = ["python-checkup"]
[project.entry-points."python-checkup.analyzers"]
my-analyzer = "my_package:MyAnalyzer"
3. Publish and install¶
Protocol Requirements¶
Your analyzer class must have these methods/properties:
| Member | Type | Description |
|---|---|---|
name |
str (property) |
Unique identifier |
category |
Category (property) |
Primary scoring category |
is_available() |
async -> bool |
Check if the tool is installed |
analyze(request) |
async -> list[Diagnostic] |
Run analysis |
Categories¶
from python_checkup.models import Category
Category.QUALITY # Code quality (Ruff, pylint, etc.)
Category.TYPE_SAFETY # Type checking (mypy, pyright, etc.)
Category.SECURITY # Security (Bandit, detect-secrets, etc.)
Category.COMPLEXITY # Complexity (Radon, etc.)
Category.DEAD_CODE # Dead code (Vulture, etc.)
Category.DEPENDENCIES # Dependencies (pip-audit, deptry, etc.)
Protocol Conformance¶
python-checkup validates plugins at load time using isinstance(analyzer, Analyzer). The Analyzer protocol is @runtime_checkable, so it uses structural typing -- your class does not need to explicitly inherit from or import Analyzer.
Plugins that fail the protocol check are logged and skipped:
Error Handling¶
is_available()should returnFalseif the tool is not installed -- never raiseanalyze()should catch tool errors and return an empty list, not raise- If your plugin raises during load or analysis, it is skipped with a warning
Framework Plugins¶
Framework plugins let you extend python-checkup's framework awareness without changing core code. They can participate in:
- framework detection
- Vulture false-positive suppression
- Ruff rule expansion
- implicit dependency hints for deptry
- framework-specific checks
- mypy stub/plugin metadata
Framework plugins register under the python_checkup.frameworks entry-point group.
Framework Plugin Entry Point¶
Framework Plugin Protocol Surface¶
Framework plugins are structurally typed against python_checkup.frameworks.FrameworkPlugin.
| Member | Type | Description |
|---|---|---|
name |
str (property) |
Framework name such as django or tornado |
category |
FrameworkCategory (property) |
Framework family such as web or orm |
detect(context) |
tuple[float, str \| None] |
Return confidence and detected version |
get_vulture_whitelist() |
list[WhitelistEntry] |
Framework dead-code suppressions |
get_ruff_rules() |
list[str] |
Extra Ruff rule prefixes |
get_implicit_dependencies() |
set[str] |
Packages used implicitly by the framework |
check(root, files, framework) |
list[Diagnostic] |
Framework-specific checks |
get_stubs_info() |
StubsInfo \| None |
Mypy stubs and plugin metadata |
Minimal Framework Plugin Example¶
from python_checkup.frameworks import FrameworkCategory, WhitelistEntry
class TornadoPlugin:
@property
def name(self) -> str:
return "tornado"
@property
def category(self) -> FrameworkCategory:
return FrameworkCategory.WEB
def detect(self, context):
score = 0.0
version = context.installed_packages.get("tornado")
if version:
score += 0.5
if "tornado" in context.dependency_packages:
score += 0.3
return score, version
def get_vulture_whitelist(self):
return [
WhitelistEntry(
name_pattern=r"^(get|post|put|delete|patch)$",
item_type="function",
parent_pattern=r"Handler$",
reason="Tornado request handler methods",
)
]
def get_ruff_rules(self):
return []
def get_implicit_dependencies(self):
return set()
def check(self, root, files, framework):
return []
def get_stubs_info(self):
return None
Framework Plugin Helper Types¶
Framework plugin authors will usually import these from python_checkup.frameworks:
FrameworkCategoryFrameworkInfoDetectionContextStubsInfoWhitelistEntry
Framework Plugin Testing Helpers¶
python-checkup ships small testing helpers for external framework plugins in
python_checkup.frameworks.testing.
Available Helpers¶
assert_detects_framework(plugin, project_root, expected_confidence_min=...)assert_check_finds(plugin, project_root, expected_rule_ids)assert_whitelist_suppresses(plugin, diagnostic_names, expected_suppressed)
Example¶
from python_checkup.frameworks.testing import (
assert_check_finds,
assert_detects_framework,
assert_whitelist_suppresses,
)
def test_plugin_detects(tmp_path):
(tmp_path / "requirements.txt").write_text("tornado\n")
(tmp_path / "app.py").write_text("import tornado.web\n")
info = assert_detects_framework(TornadoPlugin(), tmp_path, 0.3)
assert info.name == "tornado"
def test_plugin_checks(tmp_path):
(tmp_path / "app.py").write_text("print('todo')\n")
assert_check_finds(TornadoPlugin(), tmp_path, ["FW-TN-001"])
def test_plugin_whitelist():
assert_whitelist_suppresses(TornadoPlugin(), ["get"], ["get"])
Choosing Between Analyzer and Framework Plugins¶
Use an analyzer plugin when you are integrating a new external tool.
Use a framework plugin when you want python-checkup to understand a framework's:
- detection signals
- generated or framework-owned symbols
- framework-specific diagnostics
- mypy plugin or stubs requirements
Some ecosystems may need both.