"""Load pyproject.toml coverage omit patterns and match product source paths."""
from __future__ import annotations
import sys
from functools import lru_cache
from pathlib import Path
from typing import Final
from scripts.helpers._config import ConfigError
from scripts.helpers._paths import REPO_ROOT
from scripts.helpers.common.test_map_loader import is_product_source
_PYPROJECT_REL: Final = Path("pyproject.toml")
@lru_cache(maxsize=1)
def load_coverage_omit_patterns() -> tuple[str, ...]:
"""Return ``[tool.coverage.run].omit`` patterns from repo pyproject.toml."""
pyproject_path = REPO_ROOT / _PYPROJECT_REL
if not pyproject_path.is_file():
raise ConfigError(f"{_PYPROJECT_REL.as_posix()}: file not found")
try:
raw = pyproject_path.read_bytes()
except OSError as exc:
raise ConfigError(f"{_PYPROJECT_REL.as_posix()}: cannot read file: {exc}") from exc
if sys.version_info >= (3, 11):
import tomllib
try:
data = tomllib.loads(raw.decode("utf-8"))
except tomllib.TOMLDecodeError as exc:
raise ConfigError(f"{_PYPROJECT_REL.as_posix()}: invalid TOML: {exc}") from exc
else:
try:
import tomli
except ImportError as exc:
raise ConfigError(
"tomli required to parse pyproject.toml on Python < 3.11. Run: uv sync --frozen --group ci"
) from exc
try:
data = tomli.loads(raw.decode("utf-8"))
except tomli.TOMLDecodeError as exc:
raise ConfigError(f"{_PYPROJECT_REL.as_posix()}: invalid TOML: {exc}") from exc
coverage_run = data.get("tool", {}).get("coverage", {}).get("run")
if not isinstance(coverage_run, dict):
raise ConfigError(f"{_PYPROJECT_REL.as_posix()}: [tool.coverage.run] section missing")
omit = coverage_run.get("omit")
if omit is None:
return ()
if not isinstance(omit, list):
raise ConfigError(f"{_PYPROJECT_REL.as_posix()}: [tool.coverage.run].omit must be a list")
patterns: list[str] = []
for index, pattern in enumerate(omit):
if not isinstance(pattern, str) or not pattern.strip():
raise ConfigError(
f"{_PYPROJECT_REL.as_posix()}: [tool.coverage.run].omit[{index}] must be a non-empty string"
)
patterns.append(pattern)
return tuple(patterns)
@lru_cache(maxsize=1)
def _coverage_omit_matcher() -> object | None:
patterns = load_coverage_omit_patterns()
if not patterns:
return None
from coverage.files import GlobMatcher, prep_patterns
return GlobMatcher(prep_patterns(list(patterns)), "omit")
def is_coverage_omitted_source(path: str, roots: tuple[str, ...]) -> bool:
"""Return True when *path* is a product source excluded by coverage omit patterns."""
if not is_product_source(path, roots):
return False
matcher = _coverage_omit_matcher()
if matcher is None:
return False
return bool(matcher.match(path))