Checks

Plugins provide checks; repo-review requires at least one plugin providing checks to operate; there are no built-in checks.

Writing a check

A check is an object following a specific Protocol:

class Check:
    """
    Short description.
    """

    family: str
    requires: Set[str] = frozenset()  # Optional
    url: str = ""  # Optional

    def check(self) -> bool | str | None:
        """
        Error message if returns False.
        """
        ...

You need to implement family, which is a string indicating which family it is grouped under, and check(), which can take Fixtures, and returns True if the check passes, or False if the check fails. If you want a dynamic error explanation instead of the check() docstring, you can return a non-empty string from the check instead of False. Returning None makes a check “skipped”. Docstrings/error messages can access their own object with {self} and check name with {name} (these are processed with .format(), so escape {} as {{}}). The error message is in markdown format.

Changed in version 0.9: The string return value is not processed via .format. You can use self and the name fixture directly when constructing the return string.

If the check named in requires does not pass, the check is skipped.

A suggested convention for easily writing checks is as follows:

class General:
    family = "general"


class PY001(General):
    "Has a pyproject.toml"

    @staticmethod
    def check(package: Traversable) -> bool:
        """
        All projects should have a `pyproject.toml` file to support a modern
        build system and support wheel installs properly.
        """
        return package.joinpath("pyproject.toml").is_file()


class PyProject:
    family = "pyproject"


class PP002(PyProject):
    "Has a proper build-system table"

    requires = {"PY001"}
    url = "https://peps.python.org/pep-0517"

    @staticmethod
    def check(pyproject: dict[str, Any]) -> bool:
        """
        Must have `build-system.requires` *and* `build-system.backend`. Both
        should be present in all modern packages.
        """

        match pyproject:
            case {"build-system": {"requires": list(), "build-backend": str()}}:
                return True
            case _:
                return False

Key features:

  • The base class allows setting the family once, and gives a quick shortcut for accessing all the checks via .__subclasses__.

  • The name of the check class itself is the check code.

  • The check method is a classmethod since it has no state.

  • Likewise, all attributes are set on the class (family, requires, url) since there is no state.

  • requires is used so that the pyproject checks are skipped if the pyproject file is missing.

Registering checks

You register checks with a function that returns a dict of checks, with the code of the check (letters + number) as the key, and check instances as the values. This function can take Fixtures, as well, allowing customization of checks based on repo properties.

Here is the suggested function for the above example:

def repo_review_checks() -> dict[str, General | PyProject]:
    general = {p.__name__: p() for p in General.__subclasses__()}
    pyproject = {p.__name__: p() for p in PyProject.__subclasses__()}
    return general | pyproject

You tell repo-review to use this function via an entry-point:

[project.entry-points."repo_review.checks"]
general_pyproject = "my_plugin_package.my_checks_module:repo_review_checks"

The entry-point name doesn’t matter.

Customizable checks

You can customize checks, as well, using this system. Here is an example, using the (synthetic) case were we want to add a check based on the build-backend, and we want to require that tool.<build-backend> is present, where this depends on which build-backend we recognized. (Don’t actually do this, you don’t have to have a tool section to use the backends shown below!)

import dataclasses
from typing import ClassVar


@dataclasses.dataclass
class PP003(PyProject):
    "Has a tool section for the {self.name!r} build backend"

    requires: ClassVar[set[str]] = {"PY001"}
    url: ClassVar[str] = "https://peps.python.org/pep-0517"

    name: str

    def check(self, pyproject: dict[str, Any]) -> bool:
        """
        Must have a {self.name!r} section.
        """
        match pyproject:
            case {"tool": {self.name: object()}}:
                return True
            case _:
                return False


def repo_review_checks(pyproject: dict[str, Any]) -> dict[str, PyProject]:
    backends = {
        "setuptools.build_api": "setuptools",
        "scikit_build_core.build": "scikit-build",
    }
    match pyproject:
        case {"build-system": {"build-backend": str(x)}} if x in backends:
            return {"PP003": PP003(name=backends[x])}
        case _:
            return {}

Handling empty generation

If repo-review is listing all checks, a repo_review.ghpath.EmptyTraversable is passed for root and package. This will appear to be a directory with no contents. If you have conditional checks, you should handle this case to support being listed as a possible check. As a helper for this case, a list_all() fixture is provided that returns True only if a list-all operation is being performed. The above can then be written:

def repo_review_checks(
    list_all: bool, pyproject: dict[str, Any]
) -> dict[str, PyProject]:
    backends = {
        "setuptools.build_api": "setuptools",
        "scikit_build_core.build": "scikit-build",
    }

    if list_all:
        return {"PP003": PP003(name="<backend>")}

    match pyproject:
        case {"build-system": {"build-backend": str(x)}} if x in backends:
            return {"PP003": PP003(name=backends[x])}
        case _:
            return {}

Added in version 0.8: The list_all() fixture.