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/pyproject.toml b/py/pyproject.toml index 79b8846f..4abb7d30 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,6 +388,8 @@ latest = "temporalio==1.27.0" "1.19.0" = "temporalio==1.19.0" [tool.braintrust.matrix.pytest-matrix] +# 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" 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()) 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]]