.github/scripts/check_extras_sync.py PYTHON 119 lines View on github.com → Search inside
1"""Check that optional extras stay in sync with required dependencies.23When a package appears in both [project.dependencies] and4[project.optional-dependencies], we ensure their version constraints match.5This prevents silent version drift (e.g. bumping a required dep but6forgetting the corresponding extra).7"""89import sys10import tomllib11from pathlib import Path12from re import compile as re_compile1314# Matches the package name at the start of a PEP 508 dependency string.15# Stops at the first non-name character; downstream code is responsible for16# stripping extras (`[...]`) and env markers (`; ...`) from the remainder.17_NAME_RE = re_compile(r"^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)")181920def _normalize(name: str) -> str:21    """Normalize a package name for equality comparison.2223    Lowercases and maps `-` and `.` to `_`. Looser than PEP 50324    (which uses `-` and collapses runs), but sufficient for matching the25    same package across two PEP 508 strings.2627    Returns:28        Lowercased, underscore-normalized package name.29    """30    return name.lower().replace("-", "_").replace(".", "_")313233def _parse_dep(dep: str) -> tuple[str, str]:34    """Return `(normalized_name, version_spec)` from a PEP 508 string.3536    Strips extras (`pkg[async]`), environment markers (`; python_version ...`),37    URL specifiers (`pkg @ git+...`), and whitespace so the returned38    `version_spec` is directly comparable between a required and optional dep.3940    Returns:41        Tuple of normalized package name and bare version specifier.4243    Raises:44        ValueError: If the dependency string cannot be parsed.45    """46    match = _NAME_RE.match(dep)47    if not match:48        msg = f"Cannot parse dependency: {dep!r}"49        raise ValueError(msg)50    name = match.group(1)51    rest = dep[match.end() :].strip()5253    if rest.startswith("["):54        close = rest.find("]")55        if close == -1:56            msg = f"Unclosed extras bracket in dependency: {dep!r}"57            raise ValueError(msg)58        rest = rest[close + 1 :].strip()5960    if ";" in rest:61        rest = rest.split(";", 1)[0].strip()6263    # URL specifiers have no comparable version; treat as unconstrained.64    if rest.startswith("@"):65        rest = ""6667    rest = " ".join(rest.split())68    return _normalize(name), rest697071def main(pyproject_path: Path) -> int:72    """Check extras sync and return `0` on pass, `1` on mismatch or parse error."""73    with pyproject_path.open("rb") as f:74        data = tomllib.load(f)7576    required: dict[str, str] = {}77    for dep in data.get("project", {}).get("dependencies", []):78        try:79            name, spec = _parse_dep(dep)80        except ValueError as e:81            print(f"::error file={pyproject_path}::{e}")82            return 183        required[name] = spec8485    optional = data.get("project", {}).get("optional-dependencies", {})86    if not optional:87        return 08889    mismatches: list[str] = []90    for group, deps in optional.items():91        for dep in deps:92            try:93                name, spec = _parse_dep(dep)94            except ValueError as e:95                print(f"::error file={pyproject_path}::{e}")96                return 197            if name in required and spec != required[name]:98                mismatches.append(99                    f"  [{group}] {name}: extra has '{spec}' "100                    f"but required dep has '{required[name]}'"101                )102103    if mismatches:104        print(f"Extra / required dependency version mismatch in {pyproject_path}:")105        print("\n".join(mismatches))106        print(107            "\nUpdate the optional extras in [project.optional-dependencies] "108            "to match [project.dependencies]."109        )110        return 1111112    print(f"All extras in {pyproject_path} are in sync with required dependencies.")113    return 0114115116if __name__ == "__main__":117    path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("pyproject.toml")118    raise SystemExit(main(path))

Code quality findings 6

Use logging module for better control and configurability
print-statement
print(f"::error file={pyproject_path}::{e}")
Use logging module for better control and configurability
print-statement
print(f"::error file={pyproject_path}::{e}")
Use logging module for better control and configurability
print-statement
print(f"Extra / required dependency version mismatch in {pyproject_path}:")
Use logging module for better control and configurability
print-statement
print("\n".join(mismatches))
Use logging module for better control and configurability
print-statement
print(
Use logging module for better control and configurability
print-statement
print(f"All extras in {pyproject_path} are in sync with required dependencies.")

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.