Skip to content

Plugins

python-checkup supports two plugin surfaces:

  1. analyzer plugins for new analysis tools
  2. 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

  1. You create a class that satisfies the Analyzer protocol (structural typing -- no import needed)
  2. You register it as an entry point in your package's pyproject.toml
  3. After uv tool install your-package or uvx --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

uv tool install python-checkup-my-analyzer
python-checkup .

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:

WARNING: Plugin 'bad-plugin' does not implement the Analyzer protocol, skipping

Error Handling

  • is_available() should return False if the tool is not installed -- never raise
  • analyze() 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

[project.entry-points."python_checkup.frameworks"]
tornado = "checkup_tornado:TornadoPlugin"

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:

  • FrameworkCategory
  • FrameworkInfo
  • DetectionContext
  • StubsInfo
  • WhitelistEntry

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.