Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
strategy:
fail-fast: false
matrix:
py: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
py: ['3.10', '3.11', '3.12', '3.13', '3.14']
os: ['windows-latest', 'ubuntu-24.04', 'macos-latest']
runs-on: ${{ matrix.os }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ BaseValidator (ABC)

### Prerequisites

- Python 3.9 or newer
- Python 3.10 or newer
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- `nox` for running build sessions: `pip install nox`

### Install in development mode
Expand Down
48 changes: 24 additions & 24 deletions commit_check/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from __future__ import annotations

import copy
from typing import Any, Dict, Optional
from typing import Any

from commit_check.config_merger import get_default_config
from commit_check.engine import (
Expand All @@ -52,7 +52,7 @@
# ---------------------------------------------------------------------------


def _build_result(outcomes: list[CheckOutcome]) -> Dict[str, Any]:
def _build_result(outcomes: list[CheckOutcome]) -> dict[str, Any]:
"""Convert a list of :class:`~commit_check.engine.CheckOutcome` into the
public return-value dict."""
overall = "fail" if any(o.status == "fail" for o in outcomes) else "pass"
Expand All @@ -65,8 +65,8 @@ def _build_result(outcomes: list[CheckOutcome]) -> Dict[str, Any]:
def _run_checks(
check_names: list[str],
context: ValidationContext,
config: Dict[str, Any],
) -> Dict[str, Any]:
config: dict[str, Any],
) -> dict[str, Any]:
"""Build rules, filter to *check_names*, run the engine, return result."""
rule_builder = RuleBuilder(config)
all_rules = rule_builder.build_all_rules()
Expand All @@ -76,7 +76,7 @@ def _run_checks(
return _build_result(outcomes)


def _merge_config(user_config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
def _merge_config(user_config: dict[str, Any] | None) -> dict[str, Any]:
"""Return the effective config: user overrides merged on top of defaults."""
base = get_default_config()
if user_config:
Expand All @@ -97,8 +97,8 @@ def _merge_config(user_config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
def validate_message(
message: str,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate a commit message string.

:param message: The full commit message to validate (subject + optional body).
Expand Down Expand Up @@ -137,10 +137,10 @@ def validate_message(


def validate_branch(
branch: Optional[str] = None,
branch: str | None = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate a branch name.

:param branch: Branch name to validate. If *None*, the current git branch
Expand All @@ -167,10 +167,10 @@ def validate_branch(


def validate_push(
push_refs: Optional[str] = None,
push_refs: str | None = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate that a push is not a force push.

:param push_refs: Push ref information in the format produced by git's
Expand Down Expand Up @@ -199,11 +199,11 @@ def validate_push(


def validate_author(
name: Optional[str] = None,
email: Optional[str] = None,
name: str | None = None,
email: str | None = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate commit author name and/or email.

:param name: Author name to validate. If *None*, the value from
Expand Down Expand Up @@ -258,13 +258,13 @@ def validate_author(


def validate_all(
message: Optional[str] = None,
branch: Optional[str] = None,
author_name: Optional[str] = None,
author_email: Optional[str] = None,
message: str | None = None,
branch: str | None = None,
author_name: str | None = None,
author_email: str | None = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Run all requested validations and return a combined result.

This is the high-level entry point that mirrors the CLI ``commit-check -m -b``
Expand All @@ -288,7 +288,7 @@ def validate_all(
>>> result["status"]
'pass'
"""
all_checks: list[Dict[str, Any]] = []
all_checks: list[dict[str, Any]] = []

if message is not None:
msg_result = validate_message(message, config=config)
Expand Down
15 changes: 8 additions & 7 deletions commit_check/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""TOML config loader and schema for commit-check."""

from typing import Any, Dict, Optional
from __future__ import annotations
from typing import Any
from pathlib import Path
import urllib.request
import urllib.error
Expand All @@ -22,7 +23,7 @@
]


def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Deep merge override into base, returning a new dict."""
result = dict(base)
for key, value in override.items():
Expand All @@ -33,7 +34,7 @@ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any
return result


def _github_shorthand_to_url(value: str) -> Optional[str]:
def _github_shorthand_to_url(value: str) -> str | None:
"""Convert a ``github:`` shorthand to a raw GitHub content URL.

Supported formats (modelled after Release Drafter's convention):
Expand Down Expand Up @@ -69,7 +70,7 @@ def _github_shorthand_to_url(value: str) -> Optional[str]:
return f"https://raw.githubusercontent.com/{repo_part}/{ref}/{file_path}"


def _load_from_url(url: str) -> Dict[str, Any]:
def _load_from_url(url: str) -> dict[str, Any]:
"""Load TOML config from an HTTPS URL.

:param url: HTTPS URL pointing to a TOML config file.
Expand All @@ -88,7 +89,7 @@ def _load_from_url(url: str) -> Dict[str, Any]:
return {}


def _resolve_inherit_from(config: Dict[str, Any]) -> Dict[str, Any]:
def _resolve_inherit_from(config: dict[str, Any]) -> dict[str, Any]:
"""Resolve ``inherit_from`` directive, merging parent config with local.

The ``inherit_from`` key at the top level of a config file may be:
Expand All @@ -107,7 +108,7 @@ def _resolve_inherit_from(config: Dict[str, Any]) -> Dict[str, Any]:
if not inherit_from or not isinstance(inherit_from, str):
return config

parent: Dict[str, Any] = {}
parent: dict[str, Any] = {}
if inherit_from.startswith("github:"):
url = _github_shorthand_to_url(inherit_from)
if url:
Expand All @@ -128,7 +129,7 @@ def _resolve_inherit_from(config: Dict[str, Any]) -> Dict[str, Any]:
return config


def load_config(path_hint: str = "") -> Dict[str, Any]:
def load_config(path_hint: str = "") -> dict[str, Any]:
"""Load and validate config from TOML file.

Supports ``inherit_from`` at the top level to merge an organization-level
Expand Down
25 changes: 13 additions & 12 deletions commit_check/config_merger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from __future__ import annotations
import os
import argparse
from typing import Dict, Any, Optional, List, Callable, Tuple
from collections.abc import Callable
from typing import Any

from commit_check.config import load_config as load_toml_config
from commit_check import (
Expand Down Expand Up @@ -34,7 +35,7 @@ def parse_bool(value: Any) -> bool:
raise TypeError(f"Cannot convert {type(value).__name__} to bool")


def parse_list(value: Any) -> List[str]:
def parse_list(value: Any) -> list[str]:
"""Parse a list from comma-separated string or list."""
if isinstance(value, list):
return value
Expand All @@ -56,7 +57,7 @@ def parse_int(value: Any) -> int:
raise TypeError(f"Cannot convert {type(value).__name__} to int")


def get_default_config() -> Dict[str, Any]:
def get_default_config() -> dict[str, Any]:
"""Get the default configuration with all options."""
return {
"commit": {
Expand Down Expand Up @@ -89,7 +90,7 @@ def get_default_config() -> Dict[str, Any]:
}


def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> None:
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> None:
"""Deep merge override into base dictionary (modifies base in-place)."""
for key, value in override.items():
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
Expand All @@ -102,7 +103,7 @@ class ConfigMerger:
"""Merges configurations from multiple sources with priority: CLI > Env > TOML > Defaults."""

# Mapping of environment variable names to config keys
ENV_VAR_MAPPING: Dict[str, Tuple[str, str, Callable[[Any], Any]]] = {
ENV_VAR_MAPPING: dict[str, tuple[str, str, Callable[[Any], Any]]] = {
# Commit section
"CCHK_CONVENTIONAL_COMMITS": ("commit", "conventional_commits", parse_bool),
"CCHK_MESSAGE_PATTERN": ("commit", "message_pattern", str),
Expand Down Expand Up @@ -130,7 +131,7 @@ class ConfigMerger:
}

# Mapping of CLI argument names to config keys
CLI_ARG_MAPPING: Dict[str, Tuple[str, str]] = {
CLI_ARG_MAPPING: dict[str, tuple[str, str]] = {
# Commit section
"conventional_commits": ("commit", "conventional_commits"),
"subject_capitalized": ("commit", "subject_capitalized"),
Expand All @@ -157,9 +158,9 @@ class ConfigMerger:
}

@staticmethod
def parse_env_vars() -> Dict[str, Any]:
def parse_env_vars() -> dict[str, Any]:
"""Parse environment variables with CCHK_ prefix into config dict."""
config: Dict[str, Any] = {"commit": {}, "branch": {}, "push": {}}
config: dict[str, Any] = {"commit": {}, "branch": {}, "push": {}}

for env_var, (section, key, parser) in ConfigMerger.ENV_VAR_MAPPING.items():
value = os.environ.get(env_var)
Expand All @@ -176,9 +177,9 @@ def parse_env_vars() -> Dict[str, Any]:
return config

@staticmethod
def parse_cli_args(args: argparse.Namespace) -> Dict[str, Any]:
def parse_cli_args(args: argparse.Namespace) -> dict[str, Any]:
"""Parse CLI arguments into config dict."""
config: Dict[str, Any] = {"commit": {}, "branch": {}, "push": {}}
config: dict[str, Any] = {"commit": {}, "branch": {}, "push": {}}

for arg_name, (section, key) in ConfigMerger.CLI_ARG_MAPPING.items():
if hasattr(args, arg_name):
Expand All @@ -192,8 +193,8 @@ def parse_cli_args(args: argparse.Namespace) -> Dict[str, Any]:

@staticmethod
def from_all_sources(
cli_args: argparse.Namespace, config_path: Optional[str] = None
) -> Dict[str, Any]:
cli_args: argparse.Namespace, config_path: str | None = None
) -> dict[str, Any]:
"""Merge configs from all sources with priority: CLI > Env > TOML > Defaults.

Args:
Expand Down
26 changes: 13 additions & 13 deletions commit_check/engine.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Clean validation engine following SOLID principles."""

from typing import List, Optional, Dict, Type
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import IntEnum
Expand Down Expand Up @@ -33,9 +33,9 @@ class ValidationResult(IntEnum):
class ValidationContext:
"""Context for validation operations."""

stdin_text: Optional[str] = None
commit_file: Optional[str] = None
config: Dict = field(default_factory=dict)
stdin_text: str | None = None
commit_file: str | None = None
config: dict = field(default_factory=dict)
no_banner: bool = False
compact: bool = False
push_upstream_fallback: bool = False
Expand All @@ -56,7 +56,7 @@ class CheckOutcome:
error: str = ""
suggest: str = ""

def to_dict(self) -> Dict:
def to_dict(self) -> dict[str, str]:
"""Serialise to a plain dict (suitable for JSON encoding)."""
return {
"check": self.check,
Expand All @@ -79,7 +79,7 @@ def __init__(self, rule: ValidationRule):
self._no_banner: bool = False
self._compact: bool = False
# Populated by _print_failure() on every failure, regardless of mode.
self._last_failure: Optional[Dict[str, str]] = None
self._last_failure: dict[str, str] | None = None

@abstractmethod
def validate(self, context: ValidationContext) -> ValidationResult:
Expand Down Expand Up @@ -428,7 +428,7 @@ def validate(self, context: ValidationContext) -> ValidationResult:
self._print_failure(current_branch, f"target={target_branch}")
return ValidationResult.FAIL

def _find_target_branch(self, pattern: str) -> Optional[str]:
def _find_target_branch(self, pattern: str) -> str | None:
"""Find target branch matching the pattern."""
import subprocess
import re
Expand Down Expand Up @@ -620,12 +620,12 @@ def _check_push_line(self, line: str) -> ValidationResult:

return ValidationResult.PASS

def _remote_candidates_for_push(self, remote_ref: str) -> List[str]:
def _remote_candidates_for_push(self, remote_ref: str) -> list[str]:
"""Return remotes worth fetching for a pushed branch ref."""
if not remote_ref.startswith("refs/heads/"):
return []

remotes: List[str] = []
remotes: list[str] = []
upstream_ref = get_upstream_branch()
upstream_parts = upstream_ref.split("/", 1)
remote_branch = remote_ref.removeprefix("refs/heads/")
Expand Down Expand Up @@ -720,7 +720,7 @@ def _get_commit_message(self, context: ValidationContext) -> str:
class ValidationEngine:
"""Main validation engine that orchestrates all validations."""

VALIDATOR_MAP: Dict[str, Type[BaseValidator]] = {
VALIDATOR_MAP: dict[str, type[BaseValidator]] = {
"message": CommitMessageValidator,
"subject_capitalized": SubjectCapitalizationValidator,
"subject_imperative": SubjectImperativeValidator,
Expand All @@ -741,7 +741,7 @@ class ValidationEngine:
"no_force_push": ForcePushValidator,
}

def __init__(self, rules: List[ValidationRule]):
def __init__(self, rules: list[ValidationRule]):
self.rules = rules

def validate_all(self, context: ValidationContext) -> ValidationResult:
Expand All @@ -766,7 +766,7 @@ def validate_all(self, context: ValidationContext) -> ValidationResult:
else ValidationResult.PASS
)

def validate_all_detailed(self, context: ValidationContext) -> List[CheckOutcome]:
def validate_all_detailed(self, context: ValidationContext) -> list[CheckOutcome]:
"""Run all validations and return structured :class:`CheckOutcome` objects.

Unlike :meth:`validate_all`, this method:
Expand All @@ -781,7 +781,7 @@ def validate_all_detailed(self, context: ValidationContext) -> List[CheckOutcome
outcomes = engine.validate_all_detailed(context)
failed = [o for o in outcomes if o.status == "fail"]
"""
outcomes: List[CheckOutcome] = []
outcomes: list[CheckOutcome] = []

for rule in self.rules:
validator_class = self.VALIDATOR_MAP.get(rule.check)
Expand Down
Loading
Loading