From 55bda9a2d672a229630c4fa26ded2c4f259a839b Mon Sep 17 00:00:00 2001 From: Starfolk Date: Mon, 11 May 2026 17:59:21 +0000 Subject: [PATCH 1/2] chore(deps): bump pytest to 9.0.3 and fold pytest-matrix latest Fixes Dependabot alert #78 (GHSA pytest tmpdir handling). The base `test` dependency group pinned `pytest==9.0.2` while `[tool.braintrust.matrix.pytest-matrix].latest` already pinned `9.0.3`; the two were born divergent in #300 and could drift again because the matrix table doesn't participate in `uv lock`. Make `[dependency-groups].test` the single source of truth for the pytest pin and have the noxfile derive `test_pytest_plugin(latest)` from it via a small `_BASE_GROUP_FALLBACKS` set. `test_pytest_plugin(latest)` still runs; the matrix table only carries deliberate older overrides (`8.4.2`). Co-Authored-By: Claude Opus 4.7 --- py/noxfile.py | 11 ++++++++++- py/pyproject.toml | 5 +++-- py/uv.lock | 36 ++++++++++++++++++------------------ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/py/noxfile.py b/py/noxfile.py index 0f1663f4..e36eda30 100644 --- a/py/noxfile.py +++ b/py/noxfile.py @@ -74,13 +74,22 @@ def _install_group_locked(session: nox.Session, *group_names: str) -> None: os.unlink(req_file) +# Matrices in this set derive their "latest" pin from a base dependency group +# in ``[dependency-groups]`` (via ``uv.lock``) rather than from a duplicate +# string in ``[tool.braintrust.matrix]``. ``_install_matrix_dep`` naturally +# no-ops for "latest" on these prefixes because no matrix entry exists; the +# base test deps already installed the pinned version. +_BASE_GROUP_FALLBACKS = {"pytest-matrix"} + + def _get_matrix_versions(prefix: str) -> tuple[str, ...]: """Read the version matrix for *prefix* from ``[tool.braintrust.matrix]``. Returns a tuple ordered with LATEST first, then descending version order. """ matrix_entry = _MATRIX.get(prefix, {}) - latest = [LATEST] if "latest" in matrix_entry else [] + has_latest = "latest" in matrix_entry or prefix in _BASE_GROUP_FALLBACKS + latest = [LATEST] if has_latest else [] rest = sorted([v for v in matrix_entry if v != "latest"], key=Version, reverse=True) return tuple(latest + rest) diff --git a/py/pyproject.toml b/py/pyproject.toml index 79b8846f..c5a0bcdf 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -98,7 +98,7 @@ braintrust = ["py.typed"] # -- Base test deps (all sessions include this) -------------------------------- test = [ - "pytest==9.0.2", + "pytest==9.0.3", "pytest-asyncio==1.3.0", "pytest-vcr==1.0.2", ] @@ -388,7 +388,8 @@ latest = "temporalio==1.27.0" "1.19.0" = "temporalio==1.19.0" [tool.braintrust.matrix.pytest-matrix] -latest = "pytest==9.0.3" +# "latest" is derived from [dependency-groups].test in py/noxfile.py +# (see _BASE_GROUP_FALLBACKS) — keep older overrides only. "8.4.2" = "pytest==8.4.2" [tool.braintrust.matrix.braintrust-core] diff --git a/py/uv.lock b/py/uv.lock index 495ff8aa..7e6b2172 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -933,7 +933,7 @@ dev = [ { name = "pre-commit", specifier = "==4.5.1" }, { name = "pylint", specifier = "==4.0.5" }, { name = "pyperf", specifier = "==2.10.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, { name = "setuptools", specifier = ">=82.0.1" }, @@ -973,32 +973,32 @@ lint = [ { name = "temporalio" }, ] test = [ - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-agentscope = [ { name = "openai", specifier = "==2.31.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-agno = [ { name = "fastapi", specifier = "==0.135.3" }, { name = "openai", specifier = "==2.31.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-cli = [ { name = "httpx", specifier = "==0.28.1" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-crewai = [ { name = "litellm", specifier = "==1.83.14" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] @@ -1006,7 +1006,7 @@ test-langchain = [ { name = "langchain-anthropic", specifier = "==1.4.0" }, { name = "langchain-openai", specifier = "==1.1.13" }, { name = "langgraph", specifier = "==1.1.6" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] @@ -1014,49 +1014,49 @@ test-litellm = [ { name = "fastapi", specifier = "==0.135.3" }, { name = "openai", specifier = "==1.99.9" }, { name = "orjson", specifier = "==3.11.8" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-llamaindex = [ - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-openai-agents = [ { name = "openai", specifier = "==2.31.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-openai-ddtrace = [ { name = "ddtrace", specifier = "==4.1.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-openai-http2 = [ { name = "h2", specifier = "==4.3.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-pydantic-ai-logfire = [ { name = "logfire", specifier = "==4.32.1" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-strands = [ { name = "openai", specifier = "==2.32.0" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] test-types = [ { name = "mypy", specifier = "==1.20.0" }, { name = "pyright", specifier = "==1.1.408" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, ] @@ -7737,7 +7737,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-agentscope') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-agno') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-crewai') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-pydantic-ai-logfire') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-agno') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-crewai') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-crewai') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-langchain' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-langchain' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-langchain' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-litellm' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-litellm' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-openai-agents' and extra == 'group-10-braintrust-test-strands')" }, @@ -7749,9 +7749,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-agentscope') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-agno') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-crewai') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-pydantic-ai-logfire') or (extra == 'group-10-braintrust-lint' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-agno') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-crewai') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-agentscope' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-crewai') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-agno' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-langchain') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-crewai' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-langchain' and extra == 'group-10-braintrust-test-litellm') or (extra == 'group-10-braintrust-test-langchain' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-langchain' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-litellm' and extra == 'group-10-braintrust-test-openai-agents') or (extra == 'group-10-braintrust-test-litellm' and extra == 'group-10-braintrust-test-strands') or (extra == 'group-10-braintrust-test-openai-agents' and extra == 'group-10-braintrust-test-strands')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] From a4c0ee23490f8d24fe763b95203e23e21c91b420 Mon Sep 17 00:00:00 2001 From: Starfolk Date: Mon, 11 May 2026 18:08:09 +0000 Subject: [PATCH 2/2] chore(deps): make pytest-matrix.latest the canonical pytest pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up on the prior commit. Restore `latest = "pytest==9.0.3"` in [tool.braintrust.matrix.pytest-matrix] as the single source of truth and keep [dependency-groups].test in sync mechanically: - Add py/scripts/sync-pytest-pin.py — reads matrix latest, rewrites the dep-group pin (or fails with --check) so the lockfile-anchored pin matches what test_pytest_plugin(latest) installs. - Wire it into pre-commit as a --check gate on py/pyproject.toml. - Add `make sync-pytest-pin` mirroring make check-stale-cassettes. - Revert the noxfile _BASE_GROUP_FALLBACKS introduced earlier; the matrix table now has a `latest` entry, so the existing _install_matrix_dep path works without special-casing. TOML/PEP 735 have no variable substitution and uv resolves dep-groups statically, so a tiny rewrite step is the simplest way to keep one visible canonical pin while preserving uv.lock reproducibility. Co-Authored-By: Claude Opus 4.7 --- .pre-commit-config.yaml | 6 +++ py/Makefile | 6 ++- py/noxfile.py | 11 +---- py/pyproject.toml | 5 ++- py/scripts/sync-pytest-pin.py | 84 +++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 13 deletions(-) create mode 100755 py/scripts/sync-pytest-pin.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71eb170d..29acea72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,12 @@ repos: language: python pass_filenames: false files: (pyproject\.toml|cassettes/) + - id: sync-pytest-pin + name: sync pytest pin (matrix -> dep group) + entry: python py/scripts/sync-pytest-pin.py --check + language: python + pass_filenames: false + files: ^py/pyproject\.toml$ - repo: https://github.com/codespell-project/codespell rev: v2.2.5 hooks: diff --git a/py/Makefile b/py/Makefile index 72e2ef42..86c004f6 100644 --- a/py/Makefile +++ b/py/Makefile @@ -1,6 +1,6 @@ PYTHON ?= python -.PHONY: lint pylint test test-wheel _template-version clean fixup build verify-build verify help install-build-deps install-dev test-core check-stale-cassettes _check-git-clean bench bench-compare +.PHONY: lint pylint test test-wheel _template-version clean fixup build verify-build verify help install-build-deps install-dev test-core check-stale-cassettes sync-pytest-pin _check-git-clean bench bench-compare clean: rm -rf build dist @@ -33,6 +33,9 @@ test-core: check-stale-cassettes: $(PYTHON) scripts/check-stale-cassettes.py +sync-pytest-pin: + $(PYTHON) scripts/sync-pytest-pin.py + bench: $(PYTHON) -m benchmarks $(BENCH_ARGS) @@ -75,6 +78,7 @@ help: @echo " bench-compare - Compare two benchmark results (BENCH_BASE=... BENCH_NEW=...)" @echo " build - Build Python package" @echo " check-stale-cassettes - Detect orphaned cassette version directories" + @echo " sync-pytest-pin - Sync [dependency-groups].test pytest pin from matrix" @echo " clean - Remove build artifacts" @echo " help - Show this help message" @echo " install-build-deps - Install build dependencies for CI" diff --git a/py/noxfile.py b/py/noxfile.py index e36eda30..0f1663f4 100644 --- a/py/noxfile.py +++ b/py/noxfile.py @@ -74,22 +74,13 @@ def _install_group_locked(session: nox.Session, *group_names: str) -> None: os.unlink(req_file) -# Matrices in this set derive their "latest" pin from a base dependency group -# in ``[dependency-groups]`` (via ``uv.lock``) rather than from a duplicate -# string in ``[tool.braintrust.matrix]``. ``_install_matrix_dep`` naturally -# no-ops for "latest" on these prefixes because no matrix entry exists; the -# base test deps already installed the pinned version. -_BASE_GROUP_FALLBACKS = {"pytest-matrix"} - - def _get_matrix_versions(prefix: str) -> tuple[str, ...]: """Read the version matrix for *prefix* from ``[tool.braintrust.matrix]``. Returns a tuple ordered with LATEST first, then descending version order. """ matrix_entry = _MATRIX.get(prefix, {}) - has_latest = "latest" in matrix_entry or prefix in _BASE_GROUP_FALLBACKS - latest = [LATEST] if has_latest else [] + latest = [LATEST] if "latest" in matrix_entry else [] rest = sorted([v for v in matrix_entry if v != "latest"], key=Version, reverse=True) return tuple(latest + rest) diff --git a/py/pyproject.toml b/py/pyproject.toml index c5a0bcdf..4abb7d30 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -388,8 +388,9 @@ latest = "temporalio==1.27.0" "1.19.0" = "temporalio==1.19.0" [tool.braintrust.matrix.pytest-matrix] -# "latest" is derived from [dependency-groups].test in py/noxfile.py -# (see _BASE_GROUP_FALLBACKS) — keep older overrides only. +# Canonical pytest pin. The matching entry in [dependency-groups].test is +# kept in sync by py/scripts/sync-pytest-pin.py (enforced by pre-commit). +latest = "pytest==9.0.3" "8.4.2" = "pytest==8.4.2" [tool.braintrust.matrix.braintrust-core] diff --git a/py/scripts/sync-pytest-pin.py b/py/scripts/sync-pytest-pin.py new file mode 100755 index 00000000..ecd9774e --- /dev/null +++ b/py/scripts/sync-pytest-pin.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Sync [dependency-groups].test pytest pin from the matrix table. + +[tool.braintrust.matrix.pytest-matrix].latest is the canonical pytest pin. +The base [dependency-groups].test entry has to match it (so uv.lock anchors +the same version that test_pytest_plugin(latest) exercises), but TOML has +no variable substitution, so we rewrite it mechanically. + +Run without arguments to rewrite the dep-group pin in place. Run with +``--check`` (used by pre-commit) to fail without modifying anything. +""" + +import argparse +import pathlib +import re +import sys + +import tomllib + + +PYPROJECT = pathlib.Path(__file__).resolve().parent.parent / "pyproject.toml" + + +def _compute_new_text(text: str) -> tuple[str, str]: + data = tomllib.loads(text) + canonical = data["tool"]["braintrust"]["matrix"]["pytest-matrix"]["latest"] + if not isinstance(canonical, str) or not canonical.startswith("pytest=="): + raise SystemExit( + f"sync-pytest-pin: [tool.braintrust.matrix.pytest-matrix].latest " + f"must be a 'pytest==X.Y.Z' string, got: {canonical!r}" + ) + + # Match a pytest pin used as a list element (leading indent + quote). This + # uniquely identifies the [dependency-groups].test entry and does not + # collide with `latest = "pytest==..."` or `"8.4.2" = "pytest==..."` in + # the matrix table (those start with a key, not whitespace+quote). + pattern = re.compile(r'(?m)^(?P[ \t]+)"pytest==[^"]+"(?P,?)\s*$') + matches = list(pattern.finditer(text)) + if not matches: + raise SystemExit("sync-pytest-pin: no pytest list-element pin found in pyproject.toml") + if len(matches) > 1: + raise SystemExit( + "sync-pytest-pin: multiple pytest list-element pins found; refusing " + "to guess. Update py/scripts/sync-pytest-pin.py." + ) + + m = matches[0] + new_line = f'{m.group("indent")}"{canonical}"{m.group("tail")}' + new_text = text[: m.start()] + new_line + text[m.end() :] + return new_text, canonical + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="exit non-zero if the dep-group pin is out of sync; do not modify", + ) + args = parser.parse_args() + + text = PYPROJECT.read_text() + new_text, canonical = _compute_new_text(text) + + if new_text == text: + return 0 + + if args.check: + print( + f"[dependency-groups].test pytest pin is out of sync with " + f"[tool.braintrust.matrix.pytest-matrix].latest ({canonical}).\n" + f"Run: python py/scripts/sync-pytest-pin.py && (cd py && uv lock)", + file=sys.stderr, + ) + return 1 + + PYPROJECT.write_text(new_text) + print(f"sync-pytest-pin: updated [dependency-groups].test -> {canonical}") + print("note: run `cd py && uv lock` to refresh uv.lock", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main())