Use logging module for better control and configurability
print(f"::error file={pyproject_path}::{e}")
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))
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.