From 30bb6247fe9af5fd1fcf21f1e75ddca948fdee10 Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 23 Sep 2025 12:28:09 +0100 Subject: [PATCH 1/3] Improve release validation jitter handling --- .../generate-coverage/scripts/run_rust.py | 7 +- .github/actions/release-to-pypi-uv/README.md | 5 +- .github/actions/release-to-pypi-uv/action.yml | 11 +- .../scripts/check_github_release.py | 21 +- .../scripts/confirm_release.py | 6 +- .../scripts/determine_release.py | 15 +- .../scripts/publish_release.py | 15 + .../scripts/validate_toml_versions.py | 14 + .../tests/test_action_python_version.py | 11 + .../tests/test_check_github_release.py | 3 +- .../tests/test_determine_release.py | 46 ++- .../tests/test_publish_release.py | 20 +- .../tests/test_validate_toml_versions.py | 69 +++- .../tests/test_write_summary.py | 1 + .../rust-build-release/tests/test_smoke.py | 6 +- .../tests/test_target_install.py | 51 ++- .gitignore | 4 +- Makefile | 3 +- docs/cmd-mox-users-guide.md | 4 +- docs/python-native-command-mocking-design.md | 6 +- pyproject.toml | 2 +- uv.lock | 324 ------------------ 22 files changed, 271 insertions(+), 373 deletions(-) delete mode 100644 uv.lock diff --git a/.github/actions/generate-coverage/scripts/run_rust.py b/.github/actions/generate-coverage/scripts/run_rust.py index 6cd3ebc5..6b65ed6b 100644 --- a/.github/actions/generate-coverage/scripts/run_rust.py +++ b/.github/actions/generate-coverage/scripts/run_rust.py @@ -168,7 +168,12 @@ def _run_cargo(args: list[str]) -> str: missing_streams.append("stderr") missing = ", ".join(missing_streams) message = f"cargo output streams not captured: missing {missing}" - raise RuntimeError(message) + with contextlib.suppress(Exception): + proc.kill() + with contextlib.suppress(Exception): + proc.wait(timeout=5) + typer.echo(f"::error::{message}", err=True) + raise typer.Exit(1) stdout_lines: list[str] = [] if os.name == "nt": diff --git a/.github/actions/release-to-pypi-uv/README.md b/.github/actions/release-to-pypi-uv/README.md index db5c075e..66c7ff22 100644 --- a/.github/actions/release-to-pypi-uv/README.md +++ b/.github/actions/release-to-pypi-uv/README.md @@ -14,11 +14,14 @@ Build and publish Python distributions via | uv-index | Optional uv index name to publish to (e.g. `testpypi`). Must exist in `tool.uv.index`. | no | _(empty)_ | | toml-glob | Glob used to discover `pyproject.toml` files for version validation. | no | `**/pyproject.toml` | | fail-on-dynamic-version | Fail when a project declares a dynamic PEP 621 version instead of a literal string. | no | `false` | +| fail-on-empty | Fail when no `pyproject.toml` files match the discovery glob. | no | `false` | | python-version | Python version to install and use for all uv commands. | no | `3.13` | The composite action installs the interpreter requested through `python-version` before invoking any uv commands, ensuring builds run against the expected -runtime. +runtime. Set `fail-on-empty: true` when your repository must always contain at +least one `pyproject.toml`; this will turn the default warning into a failing +error to catch misconfigured globs. ## Outputs diff --git a/.github/actions/release-to-pypi-uv/action.yml b/.github/actions/release-to-pypi-uv/action.yml index 84d4f3bc..e1a73e67 100644 --- a/.github/actions/release-to-pypi-uv/action.yml +++ b/.github/actions/release-to-pypi-uv/action.yml @@ -30,6 +30,10 @@ inputs: description: Fail if any project declares a dynamic version instead of a literal string. required: false default: 'false' + fail-on-empty: + description: Fail when no pyproject.toml files match the configured glob. + required: false + default: 'false' python-version: description: Python version to install and use with uv commands. required: false @@ -53,6 +57,9 @@ runs: **/pyproject.toml **/uv.lock cache-suffix: action-${{ github.action_ref || github.sha }} + - name: Install Python + run: uv python install "${{ inputs.python-version }}" + shell: bash - name: Determine tag and version id: resolve run: uv run --script "${{ github.action_path }}/scripts/determine_release.py" @@ -81,9 +88,7 @@ runs: RESOLVED_VERSION: ${{ steps.resolve.outputs.version }} INPUT_TOML_GLOB: ${{ inputs.toml-glob }} INPUT_FAIL_ON_DYNAMIC_VERSION: ${{ inputs.fail-on-dynamic-version }} - - name: Install Python - run: uv python install "${{ inputs.python-version }}" - shell: bash + INPUT_FAIL_ON_EMPTY: ${{ inputs.fail-on-empty }} - name: Build distributions run: uv build shell: bash diff --git a/.github/actions/release-to-pypi-uv/scripts/check_github_release.py b/.github/actions/release-to-pypi-uv/scripts/check_github_release.py index f0b29b5c..5202ba39 100644 --- a/.github/actions/release-to-pypi-uv/scripts/check_github_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/check_github_release.py @@ -7,7 +7,9 @@ from __future__ import annotations +import contextlib import json +import random import time import urllib.error import urllib.parse @@ -20,6 +22,15 @@ REPO_OPTION = typer.Option(..., envvar="GITHUB_REPOSITORY") +_JITTER = random.SystemRandom() + + +def _sleep_with_jitter(delay: float) -> None: + sleep_base = max(delay, 0.0) + jitter = sleep_base * _JITTER.uniform(0.0, 0.1) + time.sleep(sleep_base + jitter) + + class GithubReleaseError(RuntimeError): """Raised when the GitHub release is not ready for publishing.""" @@ -76,13 +87,19 @@ def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]: f"{exc.code}: {failure_reason}" ) raise GithubReleaseError(message) from exc - time.sleep(delay) + retry_after = None + if hasattr(exc, "headers") and exc.headers is not None: + retry_after = exc.headers.get("Retry-After") + if retry_after: + with contextlib.suppress(Exception): + delay = float(retry_after) + _sleep_with_jitter(delay) delay *= backoff_factor except urllib.error.URLError as exc: # pragma: no cover - network failure path if attempt == max_attempts: message = f"Failed to reach GitHub API: {exc.reason}" raise GithubReleaseError(message) from exc - time.sleep(delay) + _sleep_with_jitter(delay) delay *= backoff_factor else: # pragma: no cover - loop exhausted without break message = "GitHub API request failed after retries." diff --git a/.github/actions/release-to-pypi-uv/scripts/confirm_release.py b/.github/actions/release-to-pypi-uv/scripts/confirm_release.py index ae1ce08e..6c022a57 100644 --- a/.github/actions/release-to-pypi-uv/scripts/confirm_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/confirm_release.py @@ -28,9 +28,13 @@ def main(expected: str = EXPECTED_OPTION, confirm: str = CONFIRM_OPTION) -> None typer.Exit Raised when the supplied confirmation does not match ``expected``. """ + # Normalise whitespace in both inputs before comparison. + expected = expected.strip() + confirm = confirm.strip() if confirm != expected: typer.echo( - f"::error::Confirmation failed. Set the 'confirm' input to: {expected}", + "::error::Confirmation failed. " + "Set the 'confirm' input to the expected phrase.", err=True, ) raise typer.Exit(1) diff --git a/.github/actions/release-to-pypi-uv/scripts/determine_release.py b/.github/actions/release-to-pypi-uv/scripts/determine_release.py index 3e1276c9..4a529c97 100644 --- a/.github/actions/release-to-pypi-uv/scripts/determine_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/determine_release.py @@ -19,8 +19,8 @@ def _emit_outputs(dest: Path, tag: str, version: str) -> None: with dest.open("a", encoding="utf-8") as fh: - fh.write(f"tag={tag}\n") - fh.write(f"version={version}\n") + for key, value in (("tag", tag), ("version", version)): + fh.write(f"{key}<<__EOF__\n{value}\n__EOF__\n") def main( @@ -45,10 +45,12 @@ def main( ref_name = os.getenv("GITHUB_REF_NAME", "") resolved_tag: str | None = None - if ref_type == "tag" and ref_name: + candidate_tag = (tag or "").strip() + ref_name = ref_name.strip() + if candidate_tag: + resolved_tag = candidate_tag + elif ref_type == "tag" and ref_name: resolved_tag = ref_name - elif tag: - resolved_tag = tag if not resolved_tag: typer.echo( @@ -57,7 +59,8 @@ def main( ) raise typer.Exit(1) - if not re.fullmatch(r"v\d+\.\d+\.\d+", resolved_tag): + semver_pattern = r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?" + if not re.fullmatch(semver_pattern, resolved_tag): typer.echo( "::error::Tag must be a valid semantic version (e.g. v1.2.3), " f"got '{resolved_tag}'.", diff --git a/.github/actions/release-to-pypi-uv/scripts/publish_release.py b/.github/actions/release-to-pypi-uv/scripts/publish_release.py index e5616cba..17ee216d 100644 --- a/.github/actions/release-to-pypi-uv/scripts/publish_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/publish_release.py @@ -9,12 +9,26 @@ import contextlib import os +import shutil import sys from pathlib import Path import typer +def _ensure_python_runtime() -> None: + """Fail fast when Python 3.13+ or uv provisioning is unavailable.""" + if sys.version_info >= (3, 13): + return + if shutil.which("uv") is not None: + return + typer.echo( + "::error::Python >= 3.13 or uv must be available before publishing.", + err=True, + ) + raise typer.Exit(1) + + def _extend_sys_path() -> None: candidates: list[Path] = [] action_path_env = os.getenv("GITHUB_ACTION_PATH") @@ -38,6 +52,7 @@ def _extend_sys_path() -> None: sys.path.insert(0, path_str) +_ensure_python_runtime() _extend_sys_path() from cmd_utils import run_cmd # noqa: E402 diff --git a/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py b/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py index ff5543bd..a677f2fe 100644 --- a/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py @@ -18,6 +18,10 @@ "false", envvar="INPUT_FAIL_ON_DYNAMIC_VERSION", ) +FAIL_ON_EMPTY_OPTION = typer.Option( + "false", + envvar="INPUT_FAIL_ON_EMPTY", +) # Common transient directories created by tooling (virtualenvs, caches, # pytest artefacts such as ``.pytest_cache``/``.cache`` and coverage reports @@ -86,6 +90,7 @@ def main( version: str = VERSION_OPTION, pattern: str = PATTERN_OPTION, fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION, + fail_on_empty: str = FAIL_ON_EMPTY_OPTION, ) -> None: """Confirm that project versions in TOML files match the release version. @@ -98,6 +103,9 @@ def main( fail_on_dynamic : str String flag that controls whether dynamic versions should raise an error. + fail_on_empty : str + String flag that controls whether missing matches should raise an + error instead of logging a warning. Raises ------ @@ -106,6 +114,12 @@ def main( """ files = list(_iter_files(pattern)) if not files: + if _parse_bool(fail_on_empty): + typer.echo( + f"::error::No TOML files matched pattern {pattern}", + err=True, + ) + raise typer.Exit(1) typer.echo(f"::warning::No TOML files matched pattern {pattern}") return diff --git a/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py b/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py index de3d79e4..d8e2185c 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py +++ b/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py @@ -35,3 +35,14 @@ def test_install_step_uses_python_version_input() -> None: steps = data["runs"]["steps"] install_step = next(step for step in steps if step["name"] == "Install Python") assert 'uv python install "${{ inputs.python-version }}"' in install_step["run"] + + +def test_validate_step_passes_fail_on_empty_flag() -> None: + """Ensure the validation step forwards the fail-on-empty input.""" + data = _load_action() + steps = data["runs"]["steps"] + validate_step = next( + step for step in steps if step["name"] == "Validate TOML files" + ) + env = validate_step["env"] + assert env["INPUT_FAIL_ON_EMPTY"] == "${{ inputs.fail-on-empty }}" diff --git a/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py b/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py index ead65c14..1102adf5 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py +++ b/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py @@ -206,8 +206,9 @@ def failing_urlopen(request: typ.Any, timeout: float = 30) -> typ.Any: # noqa: monkeypatch.setattr(module.urllib.request, "urlopen", failing_urlopen) monkeypatch.setattr(module.time, "sleep", lambda _: None) - with pytest.raises(module.typer.Exit): + with pytest.raises(module.typer.Exit) as exc_info: module.main(tag="v1.0.0", token=fake_token, repo="owner/repo") captured = capsys.readouterr() + assert exc_info.value.exit_code == 1 assert "temporary" in captured.err or "fetch" in captured.err diff --git a/.github/actions/release-to-pypi-uv/tests/test_determine_release.py b/.github/actions/release-to-pypi-uv/tests/test_determine_release.py index dd93180a..cd845c9f 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_determine_release.py +++ b/.github/actions/release-to-pypi-uv/tests/test_determine_release.py @@ -45,10 +45,18 @@ def read_outputs(tmp_path: Path) -> dict[str, str]: output_file = tmp_path / "out.txt" if not output_file.exists(): return out - for line in output_file.read_text(encoding="utf-8").splitlines(): - if "=" in line: - key, value = line.split("=", 1) - out[key] = value + lines = output_file.read_text(encoding="utf-8").splitlines() + iterator = iter(lines) + for line in iterator: + if "<<__EOF__" not in line: + continue + key, _ = line.split("<<", 1) + value_lines: list[str] = [] + for value_line in iterator: + if value_line == "__EOF__": + break + value_lines.append(value_line) + out[key] = "\n".join(value_lines) return out @@ -81,6 +89,36 @@ def test_resolves_tag_from_input(tmp_path: Path) -> None: assert outputs["version"] == "2.0.0" +def test_input_tag_overrides_ref(tmp_path: Path) -> None: + """Prefer the workflow input tag when both sources are present.""" + env = base_env(tmp_path) + env["GITHUB_REF_TYPE"] = "tag" + env["GITHUB_REF_NAME"] = "v0.9.9" + env["INPUT_TAG"] = "v2.3.4" + + script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py" + result = run_script(script, env=env) + + assert result.returncode == 0, result.stderr + outputs = read_outputs(tmp_path) + assert outputs["tag"] == "v2.3.4" + assert outputs["version"] == "2.3.4" + + +def test_accepts_prerelease_and_build_tags(tmp_path: Path) -> None: + """Allow SemVer pre-release and build metadata components.""" + env = base_env(tmp_path) + env["INPUT_TAG"] = "v1.2.3-rc.1+build.5" + + script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py" + result = run_script(script, env=env) + + assert result.returncode == 0, result.stderr + outputs = read_outputs(tmp_path) + assert outputs["tag"] == "v1.2.3-rc.1+build.5" + assert outputs["version"] == "1.2.3-rc.1+build.5" + + def test_rejects_invalid_tag(tmp_path: Path) -> None: """Reject release tags that do not follow the expected SemVer format.""" env = base_env(tmp_path) diff --git a/.github/actions/release-to-pypi-uv/tests/test_publish_release.py b/.github/actions/release-to-pypi-uv/tests/test_publish_release.py index 024504cc..9bbe873e 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_publish_release.py +++ b/.github/actions/release-to-pypi-uv/tests/test_publish_release.py @@ -2,6 +2,7 @@ from __future__ import annotations +import types import typing as typ if typ.TYPE_CHECKING: # pragma: no cover - imported for annotations only @@ -57,6 +58,23 @@ def fake_run_cmd(args: list[str], **_: object) -> None: assert expected_message in captured.out +def test_ensure_python_runtime_errors_without_uv( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + publish_module: ModuleType, +) -> None: + """Guard the fail-fast check when Python < 3.13 and uv is unavailable.""" + stub_sys = types.SimpleNamespace(version_info=(3, 12, 0)) + monkeypatch.setattr(publish_module, "sys", stub_sys) + monkeypatch.setattr(publish_module.shutil, "which", lambda name: None) + + with pytest.raises(publish_module.typer.Exit): + publish_module._ensure_python_runtime() + + err = capsys.readouterr().err + assert "Python >= 3.13" in err + + def test_publish_run_cmd_error( monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType ) -> None: @@ -71,7 +89,7 @@ def fake_run_cmd(_: list[str], **__: object) -> None: monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd) - with pytest.raises(DummyError): + with pytest.raises(DummyError, match="uv publish failed"): publish_module.main(index="") diff --git a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py index 85156cda..ec58aa12 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py @@ -33,13 +33,14 @@ def project_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: def _write_pyproject(base: Path, content: str) -> None: """Create a ``pyproject.toml`` file populated with the provided content.""" base.mkdir(parents=True, exist_ok=True) - (base / "pyproject.toml").write_text(content.strip()) + (base / "pyproject.toml").write_text(content.strip(), encoding="utf-8") def _invoke_main(module: ModuleType, **kwargs: object) -> None: """Invoke ``module.main`` with defaults tailored for the tests.""" kwargs.setdefault("pattern", "**/pyproject.toml") kwargs.setdefault("fail_on_dynamic", "false") + kwargs.setdefault("fail_on_empty", "false") module.main(**kwargs) @@ -56,7 +57,7 @@ def test_passes_when_versions_match( """, ) - _invoke_main(module, version="1.0.0") + _invoke_main(module, version="1.0.0", fail_on_dynamic=None) captured = capsys.readouterr() assert ( @@ -217,7 +218,7 @@ def test_skips_files_in_ignored_directory( capsys: pytest.CaptureFixture[str], skip_part: str, ) -> None: - """Warn and exit when only a single ignored directory matches the pattern.""" + """Warn and exit when matches appear solely under ignored directories.""" assert skip_part in module.SKIP_PARTS _write_pyproject( project_root / skip_part / "pkg", @@ -225,6 +226,14 @@ def test_skips_files_in_ignored_directory( [project] name = "ignored" version = "9.9.9" +""", + ) + _write_pyproject( + project_root / "nested" / skip_part / "pkg", + """ +[project] +name = "nested-ignored" +version = "9.9.9" """, ) @@ -236,9 +245,61 @@ def test_skips_files_in_ignored_directory( assert "::warning::No TOML files matched pattern" in captured.out +def test_iter_files_skips_virtualenv_and_mypy_cache( + project_root: Path, + module: ModuleType, + capsys: pytest.CaptureFixture[str], +) -> None: + """Ignore matches located under virtualenv and mypy cache directories.""" + _write_pyproject( + project_root / ".venv" / "pkg", + """ +[project] +name = "ignored-venv" +version = "0.1.0" +""", + ) + _write_pyproject( + project_root / "src" / ".mypy_cache" / "pkg", + """ +[project] +name = "ignored-mypy" +version = "0.2.0" +""", + ) + + discovered = list(module._iter_files("**/pyproject.toml")) + assert not discovered + + _invoke_main(module, version="1.0.0") + captured = capsys.readouterr() + assert "::warning::No TOML files matched pattern" in captured.out + + +def test_fail_on_empty_errors_when_enabled( + project_root: Path, + module: ModuleType, + capsys: pytest.CaptureFixture[str], +) -> None: + """Raise an error instead of a warning when ``fail_on_empty`` is truthy.""" + with pytest.raises(module.typer.Exit): + _invoke_main(module, version="1.0.0", fail_on_empty="true") + + captured = capsys.readouterr() + assert "::error::No TOML files matched pattern" in captured.err + + def test_skip_parts_cover_transient_tooling_dirs(module: ModuleType) -> None: """Ensure tooling artefact directories remain excluded from discovery.""" - expected = {".pytest_cache", ".cache", "htmlcov"} + expected = { + ".venv", + "venv", + ".direnv", + ".mypy_cache", + ".pytest_cache", + ".cache", + "htmlcov", + } assert expected <= module.SKIP_PARTS diff --git a/.github/actions/release-to-pypi-uv/tests/test_write_summary.py b/.github/actions/release-to-pypi-uv/tests/test_write_summary.py index a78f9418..f0932cc6 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_write_summary.py +++ b/.github/actions/release-to-pypi-uv/tests/test_write_summary.py @@ -36,6 +36,7 @@ def test_write_summary_appends_markdown( assert "## Release summary" in content assert "- Released tag: v1.2.3" in content assert "- Publish index: pypi (default)" in content + assert "- Environment: pypi" in content def test_write_summary_handles_existing_content( diff --git a/.github/actions/rust-build-release/tests/test_smoke.py b/.github/actions/rust-build-release/tests/test_smoke.py index bdd3e2f9..a669b5f6 100644 --- a/.github/actions/rust-build-release/tests/test_smoke.py +++ b/.github/actions/rust-build-release/tests/test_smoke.py @@ -62,10 +62,8 @@ def _param_for_target(target: str) -> object: marks: list[pytest.MarkDecorator] = [] if target != HOST_TARGET and target.endswith("-unknown-linux-gnu"): marks.append(LINUX_ONLY) - if target.endswith("-pc-windows-gnu") or target.endswith("-pc-windows-msvc"): - marks.append(WINDOWS_ONLY) - if target.endswith("-pc-windows-gnu") or target.endswith("-pc-windows-msvc"): - marks.append(WINDOWS_KNOWN_FAILURE) + if target.endswith(("-pc-windows-gnu", "-pc-windows-msvc")): + marks.extend((WINDOWS_ONLY, WINDOWS_KNOWN_FAILURE)) if marks: return pytest.param(target, marks=tuple(marks)) return pytest.param(target) diff --git a/.github/actions/rust-build-release/tests/test_target_install.py b/.github/actions/rust-build-release/tests/test_target_install.py index 46cbf59d..1338ce29 100644 --- a/.github/actions/rust-build-release/tests/test_target_install.py +++ b/.github/actions/rust-build-release/tests/test_target_install.py @@ -370,6 +370,29 @@ def raise_timeout(name: str) -> bool: assert expected in err +def test_probe_runtime_warns_on_timeout_without_duration( + main_module: ModuleType, + module_harness: HarnessFactory, + capsys: pytest.CaptureFixture[str], +) -> None: + """Timeout warnings omit duration when the exception lacks a timeout.""" + harness = module_harness(main_module) + + def raise_timeout(name: str) -> bool: + _ = name + raise subprocess.TimeoutExpired(cmd="docker info", timeout=None) + + harness.patch_attr("runtime_available", raise_timeout) + + assert main_module._probe_runtime("docker") is False + + err = capsys.readouterr().err + expected = ( + "::warning::docker runtime probe timed out; treating runtime as unavailable" + ) + assert expected in err + + def test_probe_runtime_propagates_unexpected_error( main_module: ModuleType, module_harness: HarnessFactory, @@ -398,7 +421,7 @@ def test_runtime_available_handles_timeout( module_harness: HarnessFactory, capsys: pytest.CaptureFixture[str], ) -> None: - """Treat runtime probe timeouts as unavailable instead of crashing.""" + """Treat runtime probe timeouts as unavailable while still completing the build.""" harness = module_harness(main_module) default_toolchain = main_module.DEFAULT_TOOLCHAIN @@ -416,14 +439,17 @@ def fake_run_validated( harness.patch_attr("run_validated", fake_run_validated) - def run_cmd_side_effect(cmd: list[str]) -> None: + commands: list[list[str]] = [] + + def record_run_cmd(cmd: list[str]) -> None: + commands.append(cmd) if cmd[:3] == ["/usr/bin/rustup", "target", "add"]: - raise subprocess.CalledProcessError(1, cmd) + return if cmd and cmd[0] == "cargo": return pytest.fail(f"unexpected run_cmd call: {cmd}") - harness.patch_run_cmd(run_cmd_side_effect) + harness.patch_run_cmd(record_run_cmd) harness.patch_attr("configure_windows_linkers", lambda *_: None) def timeout_runtime(_name: str, *, cwd: object | None = None) -> bool: @@ -433,10 +459,9 @@ def timeout_runtime(_name: str, *, cwd: object | None = None) -> bool: harness.patch_attr("runtime_available", timeout_runtime) harness.patch_attr("ensure_cross", lambda *_: (None, None)) - with pytest.raises(main_module.typer.Exit): - main_module.main("thumbv7em-none-eabihf", default_toolchain) + main_module.main("thumbv7em-none-eabihf", default_toolchain) - err = capsys.readouterr().err + out, err = capsys.readouterr() expected_docker = ( "::warning::docker runtime probe timed out after 10s; " "treating runtime as unavailable" @@ -445,15 +470,17 @@ def timeout_runtime(_name: str, *, cwd: object | None = None) -> bool: "::warning::podman runtime probe timed out after 10s; " "treating runtime as unavailable" ) - expected_toolchain = ( - f"::error:: toolchain '{default_toolchain}-x86_64-unknown-linux-gnu' " - "does not support target 'thumbv7em-none-eabihf'" - ) assert expected_docker in err assert expected_podman in err - assert expected_toolchain in err + assert "cross missing; using cargo" in out _assert_no_timeout_trace(err) + assert len(commands) >= 2 + assert commands[0][:3] == ["/usr/bin/rustup", "target", "add"] + assert commands[1][0] == "cargo" + assert commands[1][1].startswith("+") + assert commands[1][-1] == "thumbv7em-none-eabihf" + def test_configure_windows_linkers_prefers_toolchain_gcc( main_module: ModuleType, diff --git a/.gitignore b/.gitignore index 9eebcdb9..646ad25f 100644 --- a/.gitignore +++ b/.gitignore @@ -153,8 +153,8 @@ venv/ target/ # uv cache and lockfile -uv.lock -.uv/ +/uv.lock +/.uv/ # Crush AI agent .crush/ diff --git a/Makefile b/Makefile index 206e0766..4b0b1700 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ clean: ## Remove transient artefacts BUILD_JOBS ?= MDLINT ?= markdownlint NIXIE ?= nixie +RUFF_FIX_RULES ?= D202,I001 test: .venv ## Run tests uv run --with typer --with packaging --with plumbum --with pyyaml pytest -v @@ -39,7 +40,7 @@ typecheck: .venv ## Run static type checking with Ty fmt: ## Apply formatting to Python files uvx ruff format - uvx ruff check --select D202,I001 --fix + uvx ruff check --select $(RUFF_FIX_RULES) --fix check-fmt: ## Check Python formatting without modifying files uvx ruff format --check diff --git a/docs/cmd-mox-users-guide.md b/docs/cmd-mox-users-guide.md index 88d1ad55..b64297cc 100644 --- a/docs/cmd-mox-users-guide.md +++ b/docs/cmd-mox-users-guide.md @@ -40,7 +40,7 @@ interactions matched what was recorded. The three phases are defined in the design document: 1. **Record** – describe each expected command call, including its arguments - and behavior. + and behaviour. 2. **Replay** – run the code under test while CmdMox intercepts command executions. 3. **Verify** – ensure every expectation was met and nothing unexpected @@ -73,7 +73,7 @@ cmd_mox.spy("curl") - **Spies** record every call for later inspection and can behave like stubs. Each call returns a `CommandDouble` that offers a fluent DSL to configure -behavior. +behaviour. ## Defining expectations diff --git a/docs/python-native-command-mocking-design.md b/docs/python-native-command-mocking-design.md index 3976ae43..f1ed9034 100644 --- a/docs/python-native-command-mocking-design.md +++ b/docs/python-native-command-mocking-design.md @@ -27,7 +27,7 @@ CmdMox consists of three cooperating subsystems: exposed through attributes such as `environment.shim_dir`. 3. **IPC Server** – Handles requests from shims, dispatching them to the recorded doubles. The server enforces strict sequencing to maintain - deterministic behavior. + deterministic behaviour. The pytest plugin creates a controller per test function. When used as a context manager (`with CmdMox() as mox:`) the same controller lifecycle is available for @@ -52,7 +52,7 @@ exception visible to the test runner. ## Command Doubles and Responses -`CommandDouble` instances configure behavior with a fluent DSL: +`CommandDouble` instances configure behaviour with a fluent DSL: - `with_args(*args)` asserts exact argument sequences. - `with_matching_args(*matchers)` allows per-position comparator functions such @@ -104,7 +104,7 @@ named-pipe transports for full parity. - Each shim invocation is validated against its matching strategy; mismatches are surfaced immediately with descriptive error messages. - Journal eviction and verification are both deterministic so repeated runs yield - identical behavior given the same expectations and inputs. + identical behaviour given the same expectations and inputs. CmdMox is designed to remain implementation-agnostic at the call site, allowing maintainers to evolve the underlying IPC layer or shim mechanism without diff --git a/pyproject.toml b/pyproject.toml index ec4af737..45fa9a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dev = [ "pyyaml>=6.0,<7.0", "ty>=0.0.1a20", "uuid6>=2025.0.1", - "cmd-mox@git+https://github.com/leynos/cmd-mox.git", + "cmd-mox@git+https://github.com/leynos/cmd-mox.git@baaaf89862837b8a1565fab2c18c34d498e08601", ] [tool.ruff] diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 519811e9..00000000 --- a/uv.lock +++ /dev/null @@ -1,324 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.12" - -[[package]] -name = "click" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, -] - -[[package]] -name = "cmd-mox" -version = "0.1.0" -source = { git = "https://github.com/leynos/cmd-mox.git#5bf23d0ae6055397956a3d4440063fa6a77b10d8" } - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, -] - -[[package]] -name = "lxml-stubs" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "plumbum" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083, upload-time = "2024-10-05T05:59:27.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970, upload-time = "2024-10-05T05:59:25.102Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "rich" -version = "14.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, -] - -[[package]] -name = "shared-actions" -version = "1.2.2" -source = { virtual = "." } -dependencies = [ - { name = "lxml" }, - { name = "plumbum" }, - { name = "typer" }, -] - -[package.dev-dependencies] -dev = [ - { name = "cmd-mox" }, - { name = "lxml-stubs" }, - { name = "pytest" }, - { name = "pyyaml" }, - { name = "ty" }, - { name = "uuid6" }, -] - -[package.metadata] -requires-dist = [ - { name = "lxml", specifier = ">=5.2,<6.0" }, - { name = "plumbum", specifier = ">=1.8,<2.0" }, - { name = "typer", specifier = ">=0.9,<1.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "cmd-mox", git = "https://github.com/leynos/cmd-mox.git" }, - { name = "lxml-stubs", specifier = ">=0.5.1" }, - { name = "pytest", specifier = ">=8.0,<9.0" }, - { name = "pyyaml", specifier = ">=6.0,<7.0" }, - { name = "ty", specifier = ">=0.0.1a20" }, - { name = "uuid6", specifier = ">=2025.0.1" }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[package]] -name = "ty" -version = "0.0.1a20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/82/a5e3b4bc5280ec49c4b0b43d0ff727d58c7df128752c9c6f97ad0b5f575f/ty-0.0.1a20.tar.gz", hash = "sha256:933b65a152f277aa0e23ba9027e5df2c2cc09e18293e87f2a918658634db5f15", size = 4194773, upload-time = "2025-09-03T12:35:46.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/c8/f7d39392043d5c04936f6cad90e50eb661965ed092ca4bfc01db917d7b8a/ty-0.0.1a20-py3-none-linux_armv6l.whl", hash = "sha256:f73a7aca1f0d38af4d6999b375eb00553f3bfcba102ae976756cc142e14f3450", size = 8443599, upload-time = "2025-09-03T12:35:04.289Z" }, - { url = "https://files.pythonhosted.org/packages/1e/57/5aec78f9b8a677b7439ccded7d66c3361e61247e0f6b14e659b00dd01008/ty-0.0.1a20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cad12c857ea4b97bf61e02f6796e13061ccca5e41f054cbd657862d80aa43bae", size = 8618102, upload-time = "2025-09-03T12:35:07.448Z" }, - { url = "https://files.pythonhosted.org/packages/15/20/50c9107d93cdb55676473d9dc4e2339af6af606660c9428d3b86a1b2a476/ty-0.0.1a20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f153b65c7fcb6b8b59547ddb6353761b3e8d8bb6f0edd15e3e3ac14405949f7a", size = 8192167, upload-time = "2025-09-03T12:35:09.706Z" }, - { url = "https://files.pythonhosted.org/packages/85/28/018b2f330109cee19e81c5ca9df3dc29f06c5778440eb9af05d4550c4302/ty-0.0.1a20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c4336987a6a781d4392a9fd7b3a39edb7e4f3dd4f860e03f46c932b52aefa2", size = 8349256, upload-time = "2025-09-03T12:35:11.76Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c9/2f8797a05587158f52b142278796ffd72c893bc5ad41840fce5aeb65c6f2/ty-0.0.1a20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ff75cd4c744d09914e8c9db8d99e02f82c9379ad56b0a3fc4c5c9c923cfa84e", size = 8271214, upload-time = "2025-09-03T12:35:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/30/d4/2cac5e5eb9ee51941358cb3139aadadb59520cfaec94e4fcd2b166969748/ty-0.0.1a20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e26437772be7f7808868701f2bf9e14e706a6ec4c7d02dbd377ff94d7ba60c11", size = 9264939, upload-time = "2025-09-03T12:35:16.896Z" }, - { url = "https://files.pythonhosted.org/packages/93/96/a6f2b54e484b2c6a5488f217882237dbdf10f0fdbdb6cd31333d57afe494/ty-0.0.1a20-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:83a7ee12465841619b5eb3ca962ffc7d576bb1c1ac812638681aee241acbfbbe", size = 9743137, upload-time = "2025-09-03T12:35:19.799Z" }, - { url = "https://files.pythonhosted.org/packages/6e/67/95b40dcbec3d222f3af5fe5dd1ce066d42f8a25a2f70d5724490457048e7/ty-0.0.1a20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:726d0738be4459ac7ffae312ba96c5f486d6cbc082723f322555d7cba9397871", size = 9368153, upload-time = "2025-09-03T12:35:22.569Z" }, - { url = "https://files.pythonhosted.org/packages/2c/24/689fa4c4270b9ef9a53dc2b1d6ffade259ba2c4127e451f0629e130ea46a/ty-0.0.1a20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b481f26513f38543df514189fb16744690bcba8d23afee95a01927d93b46e36", size = 9099637, upload-time = "2025-09-03T12:35:24.94Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5b/913011cbf3ea4030097fb3c4ce751856114c9e1a5e1075561a4c5242af9b/ty-0.0.1a20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7abbe3c02218c12228b1d7c5f98c57240029cc3bcb15b6997b707c19be3908c1", size = 8952000, upload-time = "2025-09-03T12:35:27.288Z" }, - { url = "https://files.pythonhosted.org/packages/df/f9/f5ba2ae455b20c5bb003f9940ef8142a8c4ed9e27de16e8f7472013609db/ty-0.0.1a20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fff51c75ee3f7cc6d7722f2f15789ef8ffe6fd2af70e7269ac785763c906688e", size = 8217938, upload-time = "2025-09-03T12:35:29.54Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/17002cf9032f0981cdb8c898d02422c095c30eefd69ca62a8b705d15bd0f/ty-0.0.1a20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b4124ab75e0e6f09fe7bc9df4a77ee43c5e0ef7e61b0c149d7c089d971437cbd", size = 8292369, upload-time = "2025-09-03T12:35:31.748Z" }, - { url = "https://files.pythonhosted.org/packages/28/d6/0879b1fb66afe1d01d45c7658f3849aa641ac4ea10679404094f3b40053e/ty-0.0.1a20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8a138fa4f74e6ed34e9fd14652d132409700c7ff57682c2fed656109ebfba42f", size = 8811973, upload-time = "2025-09-03T12:35:33.997Z" }, - { url = "https://files.pythonhosted.org/packages/60/1e/70bf0348cfe8ba5f7532983f53c508c293ddf5fa9f942ed79a3c4d576df3/ty-0.0.1a20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8eff8871d6b88d150e2a67beba2c57048f20c090c219f38ed02eebaada04c124", size = 9010990, upload-time = "2025-09-03T12:35:36.766Z" }, - { url = "https://files.pythonhosted.org/packages/b7/ca/03d85c7650359247b1ca3f38a0d869a608ef540450151920e7014ed58292/ty-0.0.1a20-py3-none-win32.whl", hash = "sha256:3c2ace3a22fab4bd79f84c74e3dab26e798bfba7006bea4008d6321c1bd6efc6", size = 8100746, upload-time = "2025-09-03T12:35:40.007Z" }, - { url = "https://files.pythonhosted.org/packages/94/53/7a1937b8c7a66d0c8ed7493de49ed454a850396fe137d2ae12ed247e0b2f/ty-0.0.1a20-py3-none-win_amd64.whl", hash = "sha256:f41e77ff118da3385915e13c3f366b3a2f823461de54abd2e0ca72b170ba0f19", size = 8748861, upload-time = "2025-09-03T12:35:42.175Z" }, - { url = "https://files.pythonhosted.org/packages/27/36/5a3a70c5d497d3332f9e63cabc9c6f13484783b832fecc393f4f1c0c4aa8/ty-0.0.1a20-py3-none-win_arm64.whl", hash = "sha256:d8ac1c5a14cda5fad1a8b53959d9a5d979fe16ce1cc2785ea8676fed143ac85f", size = 8269906, upload-time = "2025-09-03T12:35:45.045Z" }, -] - -[[package]] -name = "typer" -version = "0.17.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "uuid6" -version = "2025.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/b7/4c0f736ca824b3a25b15e8213d1bcfc15f8ac2ae48d1b445b310892dc4da/uuid6-2025.0.1.tar.gz", hash = "sha256:cd0af94fa428675a44e32c5319ec5a3485225ba2179eefcf4c3f205ae30a81bd", size = 13932, upload-time = "2025-07-04T18:30:35.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/b2/93faaab7962e2aa8d6e174afb6f76be2ca0ce89fde14d3af835acebcaa59/uuid6-2025.0.1-py3-none-any.whl", hash = "sha256:80530ce4d02a93cdf82e7122ca0da3ebbbc269790ec1cb902481fa3e9cc9ff99", size = 6979, upload-time = "2025-07-04T18:30:34.001Z" }, -] From 8cd9cb70231d366bc6b1bf12f05d56c27fdf7ec6 Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 23 Sep 2025 15:07:20 +0100 Subject: [PATCH 2/3] Adjust runtime fallback host triples per platform --- .../actions/rust-build-release/src/runtime.py | 33 ++++++++++++++++++- .../rust-build-release/tests/test_runtime.py | 23 +++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.github/actions/rust-build-release/src/runtime.py b/.github/actions/rust-build-release/src/runtime.py index 801874ac..557b9fd3 100644 --- a/.github/actions/rust-build-release/src/runtime.py +++ b/.github/actions/rust-build-release/src/runtime.py @@ -4,8 +4,10 @@ import json import os +import platform import shutil import subprocess +import sys import typing as typ import typer @@ -15,7 +17,36 @@ from pathlib import Path CROSS_CONTAINER_ERROR_CODES = {125, 126, 127} -DEFAULT_HOST_TARGET = "x86_64-unknown-linux-gnu" +_ARCH_TO_WINDOWS_DEFAULT = { + "amd64": "x86_64-pc-windows-msvc", + "x86_64": "x86_64-pc-windows-msvc", + "arm64": "aarch64-pc-windows-msvc", + "aarch64": "aarch64-pc-windows-msvc", +} + +_ARCH_TO_DARWIN_DEFAULT = { + "x86_64": "x86_64-apple-darwin", + "amd64": "x86_64-apple-darwin", + "arm64": "aarch64-apple-darwin", + "aarch64": "aarch64-apple-darwin", +} + + +def _platform_default_host_target() -> str: + """Return a platform-specific fallback host triple.""" + machine = ( + platform.machine().lower() + or os.environ.get("PROCESSOR_ARCHITECTURE", "").lower() + ) + if sys_platform := sys.platform: + if sys_platform == "win32": + return _ARCH_TO_WINDOWS_DEFAULT.get(machine, "x86_64-pc-windows-msvc") + if sys_platform == "darwin": + return _ARCH_TO_DARWIN_DEFAULT.get(machine, "x86_64-apple-darwin") + return "x86_64-unknown-linux-gnu" + + +DEFAULT_HOST_TARGET = _platform_default_host_target() PROBE_TIMEOUT = int(os.environ.get("RUNTIME_PROBE_TIMEOUT", "10")) diff --git a/.github/actions/rust-build-release/tests/test_runtime.py b/.github/actions/rust-build-release/tests/test_runtime.py index 85c91c68..bb379dec 100644 --- a/.github/actions/rust-build-release/tests/test_runtime.py +++ b/.github/actions/rust-build-release/tests/test_runtime.py @@ -5,6 +5,7 @@ import json import subprocess import typing as typ +from types import SimpleNamespace if typ.TYPE_CHECKING: from types import ModuleType @@ -212,6 +213,28 @@ def test_detect_host_target_returns_default_on_timeout( ) +def test_platform_default_host_target_windows( + runtime_module: ModuleType, module_harness: HarnessFactory +) -> None: + """Windows fallbacks prefer the MSVC triple for common architectures.""" + harness = module_harness(runtime_module) + harness.patch_attr("platform", SimpleNamespace(machine=lambda: "AMD64")) + harness.monkeypatch.setattr(runtime_module.sys, "platform", "win32") + + assert runtime_module._platform_default_host_target() == "x86_64-pc-windows-msvc" + + +def test_platform_default_host_target_darwin_arm( + runtime_module: ModuleType, module_harness: HarnessFactory +) -> None: + """Ensure macOS ARM platforms fall back to the aarch64 Apple triple.""" + harness = module_harness(runtime_module) + harness.patch_attr("platform", SimpleNamespace(machine=lambda: "arm64")) + harness.monkeypatch.setattr(runtime_module.sys, "platform", "darwin") + + assert runtime_module._platform_default_host_target() == "aarch64-apple-darwin" + + def test_detect_host_target_passes_timeout_to_run_validated( runtime_module: ModuleType, module_harness: HarnessFactory ) -> None: From 4a5253cd878e8b86ac3946f4f16903957ccbc410 Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 23 Sep 2025 17:00:19 +0100 Subject: [PATCH 3/3] Make release validation deterministic and configurable --- .github/actions/release-to-pypi-uv/README.md | 10 ++++- .github/actions/release-to-pypi-uv/action.yml | 5 +++ .../scripts/check_github_release.py | 24 ++++++++++-- .../scripts/determine_release.py | 2 +- .../scripts/validate_toml_versions.py | 29 ++++++++++++-- .../tests/test_action_python_version.py | 11 ++++++ .../tests/test_check_github_release.py | 17 ++++++++ .../tests/test_determine_release.py | 19 +++++---- .../tests/test_validate_toml_versions.py | 39 +++++++++++++++++++ 9 files changed, 138 insertions(+), 18 deletions(-) diff --git a/.github/actions/release-to-pypi-uv/README.md b/.github/actions/release-to-pypi-uv/README.md index 66c7ff22..767b4fc5 100644 --- a/.github/actions/release-to-pypi-uv/README.md +++ b/.github/actions/release-to-pypi-uv/README.md @@ -13,6 +13,7 @@ Build and publish Python distributions via | environment-name | GitHub environment to reference in the release summary. | no | `pypi` | | uv-index | Optional uv index name to publish to (e.g. `testpypi`). Must exist in `tool.uv.index`. | no | _(empty)_ | | toml-glob | Glob used to discover `pyproject.toml` files for version validation. | no | `**/pyproject.toml` | +| skip-directories | Comma- or newline-separated directory names to skip during discovery. | no | _(empty)_ | | fail-on-dynamic-version | Fail when a project declares a dynamic PEP 621 version instead of a literal string. | no | `false` | | fail-on-empty | Fail when no `pyproject.toml` files match the discovery glob. | no | `false` | | python-version | Python version to install and use for all uv commands. | no | `3.13` | @@ -20,8 +21,13 @@ Build and publish Python distributions via The composite action installs the interpreter requested through `python-version` before invoking any uv commands, ensuring builds run against the expected runtime. Set `fail-on-empty: true` when your repository must always contain at -least one `pyproject.toml`; this will turn the default warning into a failing -error to catch misconfigured globs. +least one `pyproject.toml`. This turns the default warning into a failing error +so misconfigured globs surface early. + +Directories named `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.cache`, +`htmlcov`, and `node_modules` are skipped during TOML discovery. Provide a +comma- or newline-separated list via `skip-directories` when your repository +uses additional transient paths that should be excluded. ## Outputs diff --git a/.github/actions/release-to-pypi-uv/action.yml b/.github/actions/release-to-pypi-uv/action.yml index e1a73e67..1fbe9afb 100644 --- a/.github/actions/release-to-pypi-uv/action.yml +++ b/.github/actions/release-to-pypi-uv/action.yml @@ -26,6 +26,10 @@ inputs: description: Glob used to discover pyproject.toml files for version validation. required: false default: "**/pyproject.toml" + skip-directories: + description: Comma- or newline-separated directory names to skip during TOML discovery. + required: false + default: '' fail-on-dynamic-version: description: Fail if any project declares a dynamic version instead of a literal string. required: false @@ -89,6 +93,7 @@ runs: INPUT_TOML_GLOB: ${{ inputs.toml-glob }} INPUT_FAIL_ON_DYNAMIC_VERSION: ${{ inputs.fail-on-dynamic-version }} INPUT_FAIL_ON_EMPTY: ${{ inputs.fail-on-empty }} + INPUT_SKIP_DIRECTORIES: ${{ inputs.skip-directories }} - name: Build distributions run: uv build shell: bash diff --git a/.github/actions/release-to-pypi-uv/scripts/check_github_release.py b/.github/actions/release-to-pypi-uv/scripts/check_github_release.py index 5202ba39..74d625cf 100644 --- a/.github/actions/release-to-pypi-uv/scripts/check_github_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/check_github_release.py @@ -11,6 +11,7 @@ import json import random import time +import typing as typ import urllib.error import urllib.parse import urllib.request @@ -22,13 +23,30 @@ REPO_OPTION = typer.Option(..., envvar="GITHUB_REPOSITORY") +class _UniformGenerator(typ.Protocol): + """Protocol describing RNG objects that provide ``uniform``.""" + + def uniform(self, a: float, b: float) -> float: + """Return a random floating point number N such that ``a <= N <= b``.""" + + +SleepFn = typ.Callable[[float], None] + _JITTER = random.SystemRandom() -def _sleep_with_jitter(delay: float) -> None: +def _sleep_with_jitter( + delay: float, + *, + jitter: _UniformGenerator | None = None, + sleep: SleepFn | None = None, +) -> None: + """Sleep for ``delay`` seconds with a deterministic jitter hook for tests.""" sleep_base = max(delay, 0.0) - jitter = sleep_base * _JITTER.uniform(0.0, 0.1) - time.sleep(sleep_base + jitter) + jitter_source = _JITTER if jitter is None else jitter + sleep_fn = time.sleep if sleep is None else sleep + jitter_amount = sleep_base * jitter_source.uniform(0.0, 0.1) + sleep_fn(sleep_base + jitter_amount) class GithubReleaseError(RuntimeError): diff --git a/.github/actions/release-to-pypi-uv/scripts/determine_release.py b/.github/actions/release-to-pypi-uv/scripts/determine_release.py index 4a529c97..2df6a0fb 100644 --- a/.github/actions/release-to-pypi-uv/scripts/determine_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/determine_release.py @@ -20,7 +20,7 @@ def _emit_outputs(dest: Path, tag: str, version: str) -> None: with dest.open("a", encoding="utf-8") as fh: for key, value in (("tag", tag), ("version", version)): - fh.write(f"{key}<<__EOF__\n{value}\n__EOF__\n") + fh.write(f"{key}={value}\n") def main( diff --git a/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py b/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py index a677f2fe..146164db 100644 --- a/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py @@ -22,12 +22,16 @@ "false", envvar="INPUT_FAIL_ON_EMPTY", ) +SKIP_DIRECTORIES_OPTION = typer.Option( + "", + envvar="INPUT_SKIP_DIRECTORIES", +) # Common transient directories created by tooling (virtualenvs, caches, # pytest artefacts such as ``.pytest_cache``/``.cache`` and coverage reports # under ``htmlcov``) that should be ignored when searching for # ``pyproject.toml`` files to validate. -SKIP_PARTS = { +DEFAULT_SKIP_PARTS = { ".git", ".venv", "venv", @@ -41,22 +45,34 @@ "htmlcov", } +SKIP_PARTS = frozenset(DEFAULT_SKIP_PARTS) + TRUTHY_STRINGS = {"true", "1", "yes", "y", "on"} -def _iter_files(pattern: str) -> typ.Iterable[Path]: +def _iter_files( + pattern: str, *, skip_parts: typ.Collection[str] | None = None +) -> typ.Iterable[Path]: root = Path() + skip = set(SKIP_PARTS if skip_parts is None else skip_parts) for path in sorted( root.glob(pattern), key=lambda candidate: tuple(candidate.parts) ): if not path.is_file(): continue parts = set(path.parts) - if parts & SKIP_PARTS: + if parts & skip: continue yield path +def _parse_skip_directories(raw: str | None) -> set[str]: + if not raw: + return set() + normalized = raw.replace(",", "\n") + return {part.strip() for part in normalized.splitlines() if part.strip()} + + def _parse_bool(value: str | None) -> bool: if value is None: return False @@ -91,6 +107,7 @@ def main( pattern: str = PATTERN_OPTION, fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION, fail_on_empty: str = FAIL_ON_EMPTY_OPTION, + skip_directories: str = SKIP_DIRECTORIES_OPTION, ) -> None: """Confirm that project versions in TOML files match the release version. @@ -106,13 +123,17 @@ def main( fail_on_empty : str String flag that controls whether missing matches should raise an error instead of logging a warning. + skip_directories : str + Comma- or newline-separated list of directory name components to ignore + when matching ``pyproject.toml`` files. Raises ------ typer.Exit Raised when TOML files cannot be read or contain mismatched versions. """ - files = list(_iter_files(pattern)) + skip_parts = set(SKIP_PARTS) | _parse_skip_directories(skip_directories) + files = list(_iter_files(pattern, skip_parts=skip_parts)) if not files: if _parse_bool(fail_on_empty): typer.echo( diff --git a/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py b/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py index d8e2185c..7461f7f5 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py +++ b/.github/actions/release-to-pypi-uv/tests/test_action_python_version.py @@ -46,3 +46,14 @@ def test_validate_step_passes_fail_on_empty_flag() -> None: ) env = validate_step["env"] assert env["INPUT_FAIL_ON_EMPTY"] == "${{ inputs.fail-on-empty }}" + + +def test_validate_step_passes_skip_directories_input() -> None: + """Ensure the validation step forwards the skip-directories input.""" + data = _load_action() + steps = data["runs"]["steps"] + validate_step = next( + step for step in steps if step["name"] == "Validate TOML files" + ) + env = validate_step["env"] + assert env["INPUT_SKIP_DIRECTORIES"] == "${{ inputs.skip-directories }}" diff --git a/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py b/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py index 1102adf5..9472211a 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py +++ b/.github/actions/release-to-pypi-uv/tests/test_check_github_release.py @@ -52,6 +52,23 @@ def fixture_fake_token() -> str: return f"test-token-{uuid.uuid4().hex}" +def test_sleep_with_jitter_allows_custom_rng(module: ModuleType) -> None: + """Allow tests to provide deterministic jitter and sleep functions.""" + calls: list[float] = [] + + class FixedRandom: + """Stub RNG that always returns a fixed jitter fraction.""" + + def uniform(self, a: float, b: float) -> float: + assert a == 0.0 + assert b == 0.1 + return 0.05 + + module._sleep_with_jitter(4.0, jitter=FixedRandom(), sleep=calls.append) + + assert calls == [4.2] + + def test_success( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], diff --git a/.github/actions/release-to-pypi-uv/tests/test_determine_release.py b/.github/actions/release-to-pypi-uv/tests/test_determine_release.py index cd845c9f..e51dbc06 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_determine_release.py +++ b/.github/actions/release-to-pypi-uv/tests/test_determine_release.py @@ -48,15 +48,18 @@ def read_outputs(tmp_path: Path) -> dict[str, str]: lines = output_file.read_text(encoding="utf-8").splitlines() iterator = iter(lines) for line in iterator: - if "<<__EOF__" not in line: + if line.endswith("<<__EOF__"): + key, _ = line.split("<<", 1) + value_lines: list[str] = [] + for value_line in iterator: + if value_line == "__EOF__": + break + value_lines.append(value_line) + out[key] = "\n".join(value_lines) continue - key, _ = line.split("<<", 1) - value_lines: list[str] = [] - for value_line in iterator: - if value_line == "__EOF__": - break - value_lines.append(value_line) - out[key] = "\n".join(value_lines) + if "=" in line: + key, value = line.split("=", 1) + out[key] = value return out diff --git a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py index ec58aa12..f57f6b76 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py @@ -41,6 +41,7 @@ def _invoke_main(module: ModuleType, **kwargs: object) -> None: kwargs.setdefault("pattern", "**/pyproject.toml") kwargs.setdefault("fail_on_dynamic", "false") kwargs.setdefault("fail_on_empty", "false") + kwargs.setdefault("skip_directories", "") module.main(**kwargs) @@ -276,6 +277,44 @@ def test_iter_files_skips_virtualenv_and_mypy_cache( assert "::warning::No TOML files matched pattern" in captured.out +def test_custom_skip_directories_filter_matches( + project_root: Path, + module: ModuleType, + capsys: pytest.CaptureFixture[str], +) -> None: + """Allow repositories to skip additional transient directory names.""" + _write_pyproject( + project_root / "cache_dir" / "pkg", + """ +[project] +name = "ignored-cache" +version = "0.3.0" +""", + ) + _write_pyproject( + project_root / "alt-dir" / "pkg", + """ +[project] +name = "ignored-alt" +version = "0.4.0" +""", + ) + + discovered = list(module._iter_files("**/pyproject.toml")) + assert discovered + assert "cache_dir" not in module.SKIP_PARTS + + _invoke_main( + module, + version="1.0.0", + skip_directories="cache_dir\nalt-dir", + ) + + captured = capsys.readouterr() + assert "::warning::No TOML files matched pattern" in captured.out + assert "cache_dir" not in module.SKIP_PARTS + + def test_fail_on_empty_errors_when_enabled( project_root: Path, module: ModuleType,