-
Notifications
You must be signed in to change notification settings - Fork 0
Run Python coverage tooling in a venv #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 13 commits
2b71fb7
009141b
c6669c4
fb502bc
a93f3b7
cb49e90
6a8c09b
2ab45d6
600e710
23b373e
ba7e729
006b165
1a302b8
89cfd3a
3827e03
7c9e72f
c759a4e
f4fbdce
2b60470
3d8f177
a9f3b44
145b90b
533fa20
ee24905
3c20bbd
3588178
ab88554
e7b1ce7
aaf1276
13da80f
d712151
d002a85
394def3
d617af4
ee1226c
a8aa693
92d0b34
dc432bb
bcbc0f1
e26e9de
8fe680e
217dc00
6086cec
dad0cf6
6865952
3db43ba
77ca00c
c811865
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,12 +9,14 @@ | |
|
|
||
| import collections.abc as cabc # noqa: TC003 - used at runtime | ||
| import contextlib | ||
| import shutil | ||
| import typing as typ | ||
| from pathlib import Path | ||
|
|
||
| import typer | ||
| from cmd_utils_loader import run_cmd | ||
| from coverage_parsers import get_line_coverage_percent_from_cobertura | ||
| from plumbum import local | ||
| from plumbum.cmd import uv | ||
| from plumbum.commands.processes import ProcessExecutionError | ||
| from shared_utils import read_previous_coverage | ||
|
|
@@ -27,6 +29,14 @@ | |
| FMT_OPT = typer.Option(..., envvar="DETECTED_FMT") | ||
| GITHUB_OUTPUT_OPT = typer.Option(..., envvar="GITHUB_OUTPUT") | ||
| BASELINE_OPT = typer.Option(None, envvar="BASELINE_PYTHON_FILE") | ||
| COVERAGE_VENV = Path(".venv-coverage") | ||
| TOOLING_PACKAGES: tuple[str, ...] = ("slipcover", "pytest", "coverage") | ||
| # _COVERAGE_PYTHON_CMD is a module-level lazy singleton. GitHub Actions | ||
| # runners execute action steps sequentially in a single thread, so no | ||
| # synchronisation is required. The variable is None until the first call | ||
| # to _coverage_python_cmd(), after which it is reused for the lifetime of | ||
| # the process. | ||
| _COVERAGE_PYTHON_CMD: BoundCommand | None = None | ||
|
|
||
| SLIPCOVER_ARGS: tuple[str, ...] = ( | ||
| "-m", | ||
|
|
@@ -40,18 +50,93 @@ | |
| ) | ||
|
|
||
|
|
||
| def _uv_python_cmd() -> BoundCommand: | ||
| """Return ``uv run`` configured with the required coverage tools.""" | ||
| return uv[ | ||
| "run", | ||
| "--with", | ||
| "slipcover", | ||
| "--with", | ||
| "pytest", | ||
| "--with", | ||
| "coverage", | ||
| "python", | ||
| ] | ||
| def _coverage_python_path() -> Path: | ||
| """Return the Python executable path inside the coverage venv.""" | ||
| candidates = ( | ||
| COVERAGE_VENV / "bin" / "python", | ||
| COVERAGE_VENV / "Scripts" / "python.exe", | ||
| COVERAGE_VENV / "Scripts" / "python", | ||
| ) | ||
| for candidate in candidates: | ||
| if candidate.is_file(): | ||
| return candidate | ||
| paths = ", ".join(str(candidate) for candidate in candidates) | ||
| msg = f"Coverage venv Python executable not found; checked: {paths}" | ||
| raise RuntimeError(msg) | ||
|
|
||
|
|
||
| def create_venv() -> str: | ||
| """Create a throwaway venv for coverage tooling. | ||
|
|
||
| If the venv directory already exists but its Python executable cannot be | ||
| located (broken-cache state), the directory is removed and the venv is | ||
| recreated before returning the interpreter path. | ||
|
|
||
| Returns | ||
| ------- | ||
| str | ||
| Absolute path to the Python executable inside the created venv. | ||
| """ | ||
| if not COVERAGE_VENV.exists(): | ||
| typer.echo(f"Creating coverage venv at {COVERAGE_VENV}") | ||
| run_cmd(uv["venv", str(COVERAGE_VENV)]) | ||
| else: | ||
| typer.echo(f"Reusing existing coverage venv at {COVERAGE_VENV}") | ||
| try: | ||
| python = str(_coverage_python_path()) | ||
| except RuntimeError: | ||
| typer.echo( | ||
| f"Coverage venv at {COVERAGE_VENV} is missing its Python " | ||
| "executable; recreating.", | ||
| err=True, | ||
| ) | ||
| shutil.rmtree(COVERAGE_VENV) | ||
| run_cmd(uv["venv", str(COVERAGE_VENV)]) | ||
| python = str(_coverage_python_path()) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| return python | ||
|
|
||
|
Comment on lines
+101
to
+106
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return a resolved interpreter path to satisfy the absolute-path contract. Align implementation with the documented return contract. Line 101 and Line 160 currently propagate a relative interpreter path, while the docstrings state an absolute path. Resolve once before returning and cache the resolved value. 🔧 Proposed fix def _recreate_coverage_venv() -> Path:
@@
- return python
+ return python.resolve()
@@
def _ensure_coverage_venv() -> str:
@@
python = _find_coverage_python()
if python is None:
python = _recreate_coverage_venv()
+ else:
+ python = python.resolve()
@@
- return str(python)
+ return str(python)Also applies to: 117-121, 160-160 🤖 Prompt for AI Agents |
||
|
|
||
| def install_coverage_tools(python: str) -> None: | ||
| """Install coverage tooling into the throwaway venv. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| python : str | ||
| Path to the Python executable inside the target venv, as returned by | ||
| create_venv(). | ||
|
|
||
| Raises | ||
| ------ | ||
| plumbum.commands.processes.ProcessExecutionError | ||
| Propagated from run_cmd() when uv pip install fails. | ||
| """ | ||
| typer.echo(f"Installing coverage tooling {TOOLING_PACKAGES} into {COVERAGE_VENV}") | ||
| run_cmd(uv["pip", "install", "--python", python, *TOOLING_PACKAGES]) | ||
|
leynos marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def _coverage_python_cmd() -> BoundCommand: | ||
| """Set up the coverage venv on first call and return the cached command. | ||
|
|
||
| Side effects on first call | ||
| -------------------------- | ||
| * Creates .venv-coverage via create_venv() (recreates on broken cache). | ||
| * Installs slipcover, pytest, and coverage into the venv. | ||
| * Caches the resulting BoundCommand in _COVERAGE_PYTHON_CMD. | ||
|
|
||
| Returns | ||
| ------- | ||
| BoundCommand | ||
| A plumbum command bound to the venv's Python executable. | ||
| """ | ||
| global _COVERAGE_PYTHON_CMD | ||
| if _COVERAGE_PYTHON_CMD is not None: | ||
| typer.echo("Reusing cached coverage Python command.") | ||
| return _COVERAGE_PYTHON_CMD | ||
| typer.echo("Setting up coverage Python environment (first use).") | ||
| python = create_venv() | ||
| install_coverage_tools(python) | ||
| _COVERAGE_PYTHON_CMD = local[python] | ||
| return _COVERAGE_PYTHON_CMD | ||
|
|
||
|
|
||
| def _coverage_args(fmt: str, out: Path) -> list[str]: | ||
|
|
@@ -66,16 +151,17 @@ def _coverage_args(fmt: str, out: Path) -> list[str]: | |
|
|
||
| def coverage_cmd_for_fmt(fmt: str, out: Path) -> BoundCommand: | ||
| """Return the slipcover command for the requested format.""" | ||
| python_cmd = _uv_python_cmd() | ||
| python_cmd = _coverage_python_cmd() | ||
| return python_cmd[_coverage_args(fmt, out)] | ||
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def tmp_coveragepy_xml(out: Path) -> cabc.Generator[Path]: | ||
| """Generate a cobertura XML from coverage.py and clean up afterwards.""" | ||
| xml_tmp = out.with_suffix(".xml") | ||
| python_cmd = _coverage_python_cmd() | ||
| try: | ||
| cmd = _uv_python_cmd()["-m", "coverage", "xml", "-o", str(xml_tmp)] | ||
| cmd = python_cmd["-m", "coverage", "xml", "-o", str(xml_tmp)] | ||
| run_cmd(cmd) | ||
| except ProcessExecutionError as exc: | ||
| typer.echo( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.