Skip to content

check_python_version

rhiza_hooks.check_python_version

Check that Python version is consistent across project files.

check_version_consistency(repo_root)

Check Python version consistency across project files.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository

required

Returns:

Type Description
list[str]

List of error messages (empty if consistent)

Source code in rhiza_hooks/check_python_version.py
def check_version_consistency(repo_root: Path) -> list[str]:
    """Check Python version consistency across project files.

    Args:
        repo_root: Root directory of the repository

    Returns:
        List of error messages (empty if consistent)
    """
    python_version = get_python_version_file(repo_root)
    requires_python = get_pyproject_requires_python(repo_root)

    if python_version is None or requires_python is None:
        # One or both files don't specify a version, that's okay
        return []

    # Every clause of a (possibly compound) specifier must be satisfied.
    unsatisfied = any(
        not version_satisfies_constraint(python_version, operator, constraint_version)
        for operator, constraint_version in requires_python
    )
    if not unsatisfied:
        return []

    return [
        f"Python version mismatch: .python-version has {python_version}, "
        f"but pyproject.toml requires-python is {_format_specifier(requires_python)}"
    ]

get_pyproject_requires_python(repo_root)

Read requires-python constraint(s) from pyproject.toml.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository

required

Returns:

Type Description
list[tuple[str, str]] | None

List of (operator, version) clauses, or None if not specified or

list[tuple[str, str]] | None

unparseable. A compound specifier yields one entry per comma-separated

list[tuple[str, str]] | None

clause, e.g. ">=3.11,<3.14" -> [(">=", "3.11"), ("<", "3.14")].

Source code in rhiza_hooks/check_python_version.py
def get_pyproject_requires_python(repo_root: Path) -> list[tuple[str, str]] | None:
    """Read requires-python constraint(s) from pyproject.toml.

    Args:
        repo_root: Root directory of the repository

    Returns:
        List of (operator, version) clauses, or None if not specified or
        unparseable. A compound specifier yields one entry per comma-separated
        clause, e.g. ">=3.11,<3.14" -> [(">=", "3.11"), ("<", "3.14")].
    """
    pyproject_file = repo_root / "pyproject.toml"
    if not pyproject_file.exists():
        return None

    try:
        with pyproject_file.open("rb") as f:
            data = tomllib.load(f)
    except (tomllib.TOMLDecodeError, OSError):
        # Malformed TOML, or filesystem-level access/open errors (for example:
        # path is a directory, permission denied, or the file disappears between
        # exists() and open()), are treated as "unspecified" rather than
        # crashing the hook. Anything else (e.g. a genuine bug) is left to
        # surface.
        return None

    requires_python = data.get("project", {}).get("requires-python")
    if not requires_python:
        return None

    clauses = _parse_specifier(requires_python)
    # No clause parsed (e.g. "invalid-version"): treat as unspecified.
    return clauses or None

get_python_version_file(repo_root)

Read Python version from .python-version file.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository

required

Returns:

Type Description
str | None

Python version string or None if file doesn't exist

Source code in rhiza_hooks/check_python_version.py
def get_python_version_file(repo_root: Path) -> str | None:
    """Read Python version from .python-version file.

    Args:
        repo_root: Root directory of the repository

    Returns:
        Python version string or None if file doesn't exist
    """
    version_file = repo_root / ".python-version"
    if not version_file.exists():
        return None

    content = version_file.read_text(encoding="utf-8").strip()
    # Extract major.minor version
    match = re.match(r"(\d+\.\d+)", content)
    return match.group(1) if match else content

main(argv=None)

Main entry point for the hook.

Source code in rhiza_hooks/check_python_version.py
def main(argv: list[str] | None = None) -> int:
    """Main entry point for the hook."""
    parser = argparse.ArgumentParser(description="Check Python version consistency")
    parser.add_argument(
        "filenames",
        nargs="*",
        help="Filenames (ignored, checks repo root)",
    )
    parser.parse_args(argv)  # validate/consume pre-commit's filename args; result unused

    repo_root = find_repo_root()
    errors = check_version_consistency(repo_root)

    if errors:
        for error in errors:
            print(f"ERROR: {error}")
        return 1

    return 0

parse_version(version_str)

Parse a version string into a tuple of (major, minor).

Parameters:

Name Type Description Default
version_str str

Version string like "3.11" or "3.12"

required

Returns:

Type Description
tuple[int, int]

Tuple of (major, minor) integers

parse_version("3.11") (3, 11) parse_version("3.12") (3, 12)

Source code in rhiza_hooks/check_python_version.py
def parse_version(version_str: str) -> tuple[int, int]:
    """Parse a version string into a tuple of (major, minor).

    Args:
        version_str: Version string like "3.11" or "3.12"

    Returns:
        Tuple of (major, minor) integers

    >>> parse_version("3.11")
    (3, 11)
    >>> parse_version("3.12")
    (3, 12)
    """
    normalized = version_str.strip()
    if re.fullmatch(r"\d+\.\d+", normalized) is None:
        msg = f"Invalid version string: {version_str!r}. Expected 'major.minor'."
        raise ValueError(msg)

    parts = normalized.split(".")
    return (int(parts[0]), int(parts[1]))

version_satisfies_constraint(version, operator, constraint_version)

Check if a version satisfies a constraint.

Parameters:

Name Type Description Default
version str

The version to check (e.g., "3.12")

required
operator str

The comparison operator (e.g., ">=", "==")

required
constraint_version str

The version in the constraint (e.g., "3.11")

required

Returns:

Type Description
bool

True if version satisfies the constraint

Source code in rhiza_hooks/check_python_version.py
def version_satisfies_constraint(version: str, operator: str, constraint_version: str) -> bool:
    """Check if a version satisfies a constraint.

    Args:
        version: The version to check (e.g., "3.12")
        operator: The comparison operator (e.g., ">=", "==")
        constraint_version: The version in the constraint (e.g., "3.11")

    Returns:
        True if version satisfies the constraint
    """
    comparator = _COMPARATORS.get(operator)
    if comparator is None:
        # Unknown operator: be permissive.
        return True
    return comparator(parse_version(version), parse_version(constraint_version))