From 21a09d570beeeecd1751841f23c06f29dd9fa590 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 19 Mar 2026 12:16:09 +0100 Subject: [PATCH 1/3] feat(tox): Add `-latest` alias for each integration test suite For every auto-generated integration, add a tox environment that aliases the highest tested version. E.g. `tox -e py3.14-httpx-latest` is equivalent to `tox -e py3.14-httpx-v0.28.1`. This makes it easy to run the latest version's tests without having to look up the exact version string. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/populate_tox/populate_tox.py | 28 ++++ scripts/populate_tox/tox.jinja | 9 ++ .../split_tox_gh_actions.py | 5 + tox.ini | 126 ++++++++++++++++++ 4 files changed, 168 insertions(+) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index fb404b0921..cd624e1e24 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -724,6 +724,31 @@ def _render_dependencies(integration: str, releases: list[Version]) -> list[str] return rendered +def _render_latest_dependencies( + integration: str, latest_release: Version +) -> list[str]: + """Render version-specific dependencies for the 'latest' alias. + + Dependencies with "*" or "py3.*" constraints already match the latest + env via tox factor matching, so only version-specific constraints need + to be duplicated here. + """ + rendered = [] + + if TEST_SUITE_CONFIG[integration].get("deps") is None: + return rendered + + for constraint, deps in TEST_SUITE_CONFIG[integration]["deps"].items(): + if constraint == "*" or constraint.startswith("py3"): + continue + restriction = SpecifierSet(constraint, prereleases=True) + if latest_release in restriction: + for dep in deps: + rendered.append(f"{integration}-latest: {dep}") + + return rendered + + def write_tox_file(packages: dict) -> None: template = ENV.get_template("tox.jinja") @@ -744,6 +769,9 @@ def write_tox_file(packages: dict) -> None: "dependencies": _render_dependencies( integration["name"], integration["releases"] ), + "latest_dependencies": _render_latest_dependencies( + integration["name"], integration["releases"][-1] + ), } ) context["testpaths"].append( diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 38a8f670bb..60df0dc83f 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -61,6 +61,7 @@ envlist = {% for release in integration.releases %} {{ release.rendered_python_versions }}-{{ integration.name }}-v{{ release }} {% endfor %} + {{ integration.releases[-1].rendered_python_versions }}-{{ integration.name }}-latest {% endfor %} @@ -143,9 +144,17 @@ deps = {{ integration.name }}-v{{ release }}: {{ integration.package }}=={{ release }} {% endif %} {% endfor %} + {% if integration.extra %} + {{ integration.name }}-latest: {{ integration.package }}[{{ integration.extra }}]=={{ integration.releases[-1] }} + {% else %} + {{ integration.name }}-latest: {{ integration.package }}=={{ integration.releases[-1] }} + {% endif %} {% for dep in integration.dependencies %} {{ dep }} {% endfor %} + {% for dep in integration.latest_dependencies %} + {{ dep }} + {% endfor %} {% endfor %} diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index b59e768a56..e8f2b5dfe2 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -240,6 +240,11 @@ def parse_tox(): raw_python_versions = groups["py_versions"] framework = groups["framework"] + # The -latest env is an alias for the highest version of the + # same framework, so merge it with the base framework. + if framework.endswith("-latest"): + framework = framework[: -len("-latest")] + # collect python versions to test the framework in raw_python_versions = set(raw_python_versions.split(",")) py_versions[framework] |= raw_python_versions diff --git a/tox.ini b/tox.ini index 293ff38b34..562823243b 100644 --- a/tox.ini +++ b/tox.ini @@ -60,12 +60,14 @@ envlist = {py3.10,py3.12,py3.13}-mcp-v1.19.0 {py3.10,py3.12,py3.13}-mcp-v1.23.3 {py3.10,py3.12,py3.13}-mcp-v1.26.0 + {py3.10,py3.12,py3.13}-mcp-latest {py3.10,py3.13,py3.14,py3.14t}-fastmcp-v0.1.0 {py3.10,py3.13,py3.14,py3.14t}-fastmcp-v0.4.1 {py3.10,py3.13,py3.14,py3.14t}-fastmcp-v1.0 {py3.10,py3.12,py3.13}-fastmcp-v2.14.5 {py3.10,py3.12,py3.13}-fastmcp-v3.1.1 + {py3.10,py3.12,py3.13}-fastmcp-latest # ~~~ Agents ~~~ @@ -73,24 +75,29 @@ envlist = {py3.10,py3.12,py3.13}-openai_agents-v0.4.2 {py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.8.4 {py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.12.5 + {py3.10,py3.13,py3.14,py3.14t}-openai_agents-latest {py3.10,py3.12,py3.13}-pydantic_ai-v1.0.18 {py3.10,py3.12,py3.13}-pydantic_ai-v1.23.0 {py3.10,py3.12,py3.13}-pydantic_ai-v1.47.0 {py3.10,py3.13,py3.14}-pydantic_ai-v1.70.0 + {py3.10,py3.13,py3.14}-pydantic_ai-latest # ~~~ AI Workflow ~~~ {py3.9,py3.11,py3.12}-langchain-base-v0.1.20 {py3.9,py3.12,py3.13}-langchain-base-v0.3.28 {py3.10,py3.13,py3.14}-langchain-base-v1.2.12 + {py3.10,py3.13,py3.14}-langchain-base-latest {py3.9,py3.11,py3.12}-langchain-notiktoken-v0.1.20 {py3.9,py3.12,py3.13}-langchain-notiktoken-v0.3.28 {py3.10,py3.13,py3.14}-langchain-notiktoken-v1.2.12 + {py3.10,py3.13,py3.14}-langchain-notiktoken-latest {py3.9,py3.13,py3.14}-langgraph-v0.6.11 {py3.10,py3.12,py3.13}-langgraph-v1.1.3 + {py3.10,py3.12,py3.13}-langgraph-latest # ~~~ AI ~~~ @@ -98,33 +105,40 @@ envlist = {py3.8,py3.11,py3.12}-anthropic-v0.39.0 {py3.8,py3.12,py3.13}-anthropic-v0.62.0 {py3.9,py3.13,py3.14,py3.14t}-anthropic-v0.86.0 + {py3.9,py3.13,py3.14,py3.14t}-anthropic-latest {py3.9,py3.10,py3.11}-cohere-v5.4.0 {py3.9,py3.11,py3.12}-cohere-v5.10.0 {py3.9,py3.11,py3.12}-cohere-v5.15.0 {py3.9,py3.13,py3.14}-cohere-v5.20.7 + {py3.9,py3.13,py3.14}-cohere-latest {py3.9,py3.12,py3.13}-google_genai-v1.29.0 {py3.9,py3.12,py3.13}-google_genai-v1.42.0 {py3.10,py3.13,py3.14,py3.14t}-google_genai-v1.55.0 {py3.10,py3.13,py3.14,py3.14t}-google_genai-v1.68.0 + {py3.10,py3.13,py3.14,py3.14t}-google_genai-latest {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 {py3.8,py3.12,py3.13}-huggingface_hub-v0.36.2 {py3.9,py3.13,py3.14,py3.14t}-huggingface_hub-v1.7.1 + {py3.9,py3.13,py3.14,py3.14t}-huggingface_hub-latest {py3.9,py3.12,py3.13}-litellm-v1.77.7 {py3.9,py3.12,py3.13}-litellm-v1.79.3 {py3.9,py3.12,py3.13}-litellm-v1.81.16 {py3.9,py3.12,py3.13}-litellm-v1.82.4 + {py3.9,py3.12,py3.13}-litellm-latest {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.12,py3.13}-openai-base-v1.109.1 {py3.9,py3.13,py3.14,py3.14t}-openai-base-v2.29.0 + {py3.9,py3.13,py3.14,py3.14t}-openai-base-latest {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.12,py3.13}-openai-notiktoken-v1.109.1 {py3.9,py3.13,py3.14,py3.14t}-openai-notiktoken-v2.29.0 + {py3.9,py3.13,py3.14,py3.14t}-openai-notiktoken-latest # ~~~ Cloud ~~~ @@ -132,9 +146,11 @@ envlist = {py3.6,py3.9,py3.10}-boto3-v1.21.46 {py3.7,py3.11,py3.12}-boto3-v1.33.13 {py3.9,py3.13,py3.14,py3.14t}-boto3-v1.42.71 + {py3.9,py3.13,py3.14,py3.14t}-boto3-latest {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.9,py3.12,py3.13}-chalice-v1.32.0 + {py3.9,py3.12,py3.13}-chalice-latest # ~~~ DBs ~~~ @@ -142,12 +158,15 @@ envlist = {py3.7,py3.9,py3.10}-asyncpg-v0.26.0 {py3.8,py3.11,py3.12}-asyncpg-v0.29.0 {py3.9,py3.13,py3.14,py3.14t}-asyncpg-v0.31.0 + {py3.9,py3.13,py3.14,py3.14t}-asyncpg-latest {py3.9,py3.13,py3.14}-clickhouse_driver-v0.2.10 + {py3.9,py3.13,py3.14}-clickhouse_driver-latest {py3.6}-pymongo-v3.5.1 {py3.6,py3.10,py3.11}-pymongo-v3.13.0 {py3.9,py3.13,py3.14,py3.14t}-pymongo-v4.16.0 + {py3.9,py3.13,py3.14,py3.14t}-pymongo-latest {py3.6}-redis-v2.10.6 {py3.6,py3.7,py3.8}-redis-v3.5.3 @@ -155,43 +174,54 @@ envlist = {py3.8,py3.11,py3.12}-redis-v5.3.1 {py3.9,py3.12,py3.13}-redis-v6.4.0 {py3.10,py3.13,py3.14,py3.14t}-redis-v7.3.0 + {py3.10,py3.13,py3.14,py3.14t}-redis-latest {py3.6}-redis_py_cluster_legacy-v1.3.6 {py3.6,py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3 + {py3.6,py3.7,py3.8}-redis_py_cluster_legacy-latest {py3.6,py3.8,py3.9}-sqlalchemy-v1.2.19 {py3.6,py3.11,py3.12}-sqlalchemy-v1.4.54 {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.48 {py3.10,py3.13,py3.14,py3.14t}-sqlalchemy-v2.1.0b1 + {py3.10,py3.13,py3.14,py3.14t}-sqlalchemy-latest # ~~~ Flags ~~~ {py3.8,py3.12,py3.13}-launchdarkly-v9.8.1 {py3.10,py3.13,py3.14,py3.14t}-launchdarkly-v9.15.0 + {py3.10,py3.13,py3.14,py3.14t}-launchdarkly-latest {py3.8,py3.13,py3.14,py3.14t}-openfeature-v0.7.5 {py3.9,py3.13,py3.14,py3.14t}-openfeature-v0.8.4 + {py3.9,py3.13,py3.14,py3.14t}-openfeature-latest {py3.7,py3.13,py3.14}-statsig-v0.55.3 {py3.7,py3.13,py3.14}-statsig-v0.71.6 + {py3.7,py3.13,py3.14}-statsig-latest {py3.8,py3.12,py3.13}-unleash-v6.0.1 {py3.8,py3.12,py3.13}-unleash-v6.7.0 + {py3.8,py3.12,py3.13}-unleash-latest # ~~~ GraphQL ~~~ {py3.8,py3.10,py3.11}-ariadne-v0.20.1 {py3.10,py3.13,py3.14,py3.14t}-ariadne-v1.0.0 + {py3.10,py3.13,py3.14,py3.14t}-ariadne-latest {py3.6,py3.9,py3.10}-gql-v3.4.1 {py3.9,py3.12,py3.13}-gql-v4.0.0 {py3.9,py3.13,py3.14,py3.14t}-gql-v4.3.0b0 + {py3.9,py3.13,py3.14,py3.14t}-gql-latest {py3.6,py3.9,py3.10}-graphene-v3.3 {py3.8,py3.12,py3.13}-graphene-v3.4.3 + {py3.8,py3.12,py3.13}-graphene-latest {py3.8,py3.10,py3.11}-strawberry-v0.209.8 {py3.10,py3.13,py3.14,py3.14t}-strawberry-v0.311.3 + {py3.10,py3.13,py3.14,py3.14t}-strawberry-latest # ~~~ Network ~~~ @@ -200,44 +230,55 @@ envlist = {py3.7,py3.11,py3.12}-grpc-v1.62.3 {py3.9,py3.13,py3.14}-grpc-v1.78.0 {py3.9,py3.13,py3.14}-grpc-v1.80.0rc1 + {py3.9,py3.13,py3.14}-grpc-latest {py3.6,py3.8,py3.9}-httpx-v0.16.1 {py3.6,py3.9,py3.10}-httpx-v0.20.0 {py3.7,py3.10,py3.11}-httpx-v0.24.1 {py3.9,py3.11,py3.12}-httpx-v0.28.1 + {py3.9,py3.11,py3.12}-httpx-latest {py3.6}-requests-v2.12.5 {py3.9,py3.13,py3.14,py3.14t}-requests-v2.32.5 + {py3.9,py3.13,py3.14,py3.14t}-requests-latest # ~~~ Tasks ~~~ {py3.7,py3.9,py3.10}-arq-v0.23 {py3.9,py3.12,py3.13}-arq-v0.27.0 + {py3.9,py3.12,py3.13}-arq-latest {py3.7}-beam-v2.14.0 {py3.10,py3.12,py3.13}-beam-v2.71.0 {py3.10,py3.12,py3.13}-beam-v2.72.0rc2 + {py3.10,py3.12,py3.13}-beam-latest {py3.6,py3.7,py3.8}-celery-v4.4.7 {py3.9,py3.12,py3.13}-celery-v5.6.2 + {py3.9,py3.12,py3.13}-celery-latest {py3.6,py3.7}-dramatiq-v1.9.0 {py3.10,py3.13,py3.14,py3.14t}-dramatiq-v2.1.0 + {py3.10,py3.13,py3.14,py3.14t}-dramatiq-latest {py3.6,py3.7}-huey-v2.1.3 {py3.6,py3.13,py3.14,py3.14t}-huey-v2.6.0 + {py3.6,py3.13,py3.14,py3.14t}-huey-latest {py3.9,py3.10}-ray-v2.7.2 {py3.10,py3.12,py3.13}-ray-v2.54.0 + {py3.10,py3.12,py3.13}-ray-latest {py3.6}-rq-v0.6.0 {py3.6,py3.7}-rq-v0.13.0 {py3.7,py3.11,py3.12}-rq-v1.16.2 {py3.9,py3.13,py3.14,py3.14t}-rq-v2.7.0 + {py3.9,py3.13,py3.14,py3.14t}-rq-latest {py3.8,py3.9}-spark-v3.0.3 {py3.8,py3.10,py3.11}-spark-v3.5.8 {py3.10,py3.13,py3.14}-spark-v4.1.1 + {py3.10,py3.13,py3.14}-spark-latest # ~~~ Web 1 ~~~ @@ -247,21 +288,25 @@ envlist = {py3.8,py3.11,py3.12}-django-v4.2.29 {py3.10,py3.13,py3.14,py3.14t}-django-v5.2.12 {py3.12,py3.13,py3.14,py3.14t}-django-v6.0.3 + {py3.12,py3.13,py3.14,py3.14t}-django-latest {py3.6,py3.7,py3.8}-flask-v1.1.4 {py3.8,py3.13,py3.14,py3.14t}-flask-v2.3.3 {py3.9,py3.13,py3.14,py3.14t}-flask-v3.1.3 + {py3.9,py3.13,py3.14,py3.14t}-flask-latest {py3.6,py3.9,py3.10}-starlette-v0.16.0 {py3.7,py3.10,py3.11}-starlette-v0.28.0 {py3.8,py3.12,py3.13}-starlette-v0.40.0 {py3.10,py3.13,py3.14,py3.14t}-starlette-v0.52.1 {py3.10,py3.13,py3.14,py3.14t}-starlette-v1.0.0rc1 + {py3.10,py3.13,py3.14,py3.14t}-starlette-latest {py3.6,py3.9,py3.10}-fastapi-v0.79.1 {py3.7,py3.10,py3.11}-fastapi-v0.98.0 {py3.8,py3.12,py3.13}-fastapi-v0.117.1 {py3.10,py3.13,py3.14,py3.14t}-fastapi-v0.135.1 + {py3.10,py3.13,py3.14,py3.14t}-fastapi-latest # ~~~ Web 2 ~~~ @@ -269,53 +314,66 @@ envlist = {py3.7,py3.8,py3.9}-aiohttp-v3.7.4 {py3.8,py3.12,py3.13}-aiohttp-v3.10.11 {py3.9,py3.13,py3.14,py3.14t}-aiohttp-v3.13.3 + {py3.9,py3.13,py3.14,py3.14t}-aiohttp-latest {py3.6,py3.7}-bottle-v0.12.25 {py3.8,py3.12,py3.13}-bottle-v0.13.4 + {py3.8,py3.12,py3.13}-bottle-latest {py3.6}-falcon-v1.4.1 {py3.6,py3.7}-falcon-v2.0.0 {py3.6,py3.11,py3.12}-falcon-v3.1.3 {py3.9,py3.11,py3.12}-falcon-v4.2.0 + {py3.9,py3.11,py3.12}-falcon-latest {py3.8,py3.10,py3.11}-litestar-v2.0.1 {py3.8,py3.11,py3.12}-litestar-v2.7.2 {py3.8,py3.12,py3.13}-litestar-v2.14.0 {py3.8,py3.12,py3.13}-litestar-v2.21.1 + {py3.8,py3.12,py3.13}-litestar-latest {py3.6}-pyramid-v1.8.6 {py3.6,py3.8,py3.9}-pyramid-v1.10.8 {py3.10,py3.13,py3.14}-pyramid-v2.1 + {py3.10,py3.13,py3.14}-pyramid-latest {py3.7,py3.9,py3.10}-quart-v0.16.3 {py3.9,py3.13,py3.14,py3.14t}-quart-v0.20.0 + {py3.9,py3.13,py3.14,py3.14t}-quart-latest {py3.6}-sanic-v0.8.3 {py3.6,py3.8,py3.9}-sanic-v20.12.7 {py3.8,py3.10,py3.11}-sanic-v23.12.2 {py3.10,py3.13,py3.14}-sanic-v25.12.0 + {py3.10,py3.13,py3.14}-sanic-latest {py3.8,py3.10,py3.11}-starlite-v1.48.1 {py3.8,py3.10,py3.11}-starlite-v1.51.16 + {py3.8,py3.10,py3.11}-starlite-latest {py3.6,py3.7,py3.8}-tornado-v6.0.4 {py3.9,py3.12,py3.13}-tornado-v6.5.5 + {py3.9,py3.12,py3.13}-tornado-latest # ~~~ Misc ~~~ {py3.6,py3.12,py3.13}-loguru-v0.7.3 + {py3.6,py3.12,py3.13}-loguru-latest {py3.6,py3.7,py3.8}-pure_eval-v0.0.3 {py3.7,py3.12,py3.13}-pure_eval-v0.2.3 + {py3.7,py3.12,py3.13}-pure_eval-latest {py3.6}-trytond-v4.6.22 {py3.6}-trytond-v4.8.18 {py3.6,py3.7,py3.8}-trytond-v5.8.16 {py3.8,py3.10,py3.11}-trytond-v6.8.17 {py3.9,py3.12,py3.13}-trytond-v7.8.6 + {py3.9,py3.12,py3.13}-trytond-latest {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.10,py3.13,py3.14,py3.14t}-typer-v0.24.1 + {py3.10,py3.13,py3.14,py3.14t}-typer-latest @@ -391,6 +449,7 @@ deps = mcp-v1.19.0: mcp==1.19.0 mcp-v1.23.3: mcp==1.23.3 mcp-v1.26.0: mcp==1.26.0 + mcp-latest: mcp==1.26.0 mcp: pytest-asyncio fastmcp-v0.1.0: fastmcp==0.1.0 @@ -398,6 +457,7 @@ deps = fastmcp-v1.0: fastmcp==1.0 fastmcp-v2.14.5: fastmcp==2.14.5 fastmcp-v3.1.1: fastmcp==3.1.1 + fastmcp-latest: fastmcp==3.1.1 fastmcp: pytest-asyncio @@ -406,12 +466,14 @@ deps = openai_agents-v0.4.2: openai-agents==0.4.2 openai_agents-v0.8.4: openai-agents==0.8.4 openai_agents-v0.12.5: openai-agents==0.12.5 + openai_agents-latest: openai-agents==0.12.5 openai_agents: pytest-asyncio pydantic_ai-v1.0.18: pydantic-ai==1.0.18 pydantic_ai-v1.23.0: pydantic-ai==1.23.0 pydantic_ai-v1.47.0: pydantic-ai==1.47.0 pydantic_ai-v1.70.0: pydantic-ai==1.70.0 + pydantic_ai-latest: pydantic-ai==1.70.0 pydantic_ai: pytest-asyncio @@ -419,6 +481,7 @@ deps = langchain-base-v0.1.20: langchain==0.1.20 langchain-base-v0.3.28: langchain==0.3.28 langchain-base-v1.2.12: langchain==1.2.12 + langchain-base-latest: langchain==1.2.12 langchain-base: pytest-asyncio langchain-base: openai langchain-base: tiktoken @@ -426,19 +489,25 @@ deps = langchain-base-v0.3.28: langchain-community langchain-base-v1.2.12: langchain-community langchain-base-v1.2.12: langchain-classic + langchain-base-latest: langchain-community + langchain-base-latest: langchain-classic langchain-notiktoken-v0.1.20: langchain==0.1.20 langchain-notiktoken-v0.3.28: langchain==0.3.28 langchain-notiktoken-v1.2.12: langchain==1.2.12 + langchain-notiktoken-latest: langchain==1.2.12 langchain-notiktoken: pytest-asyncio langchain-notiktoken: openai langchain-notiktoken: langchain-openai langchain-notiktoken-v0.3.28: langchain-community langchain-notiktoken-v1.2.12: langchain-community langchain-notiktoken-v1.2.12: langchain-classic + langchain-notiktoken-latest: langchain-community + langchain-notiktoken-latest: langchain-classic langgraph-v0.6.11: langgraph==0.6.11 langgraph-v1.1.3: langgraph==1.1.3 + langgraph-latest: langgraph==1.1.3 # ~~~ AI ~~~ @@ -446,6 +515,7 @@ deps = anthropic-v0.39.0: anthropic==0.39.0 anthropic-v0.62.0: anthropic==0.62.0 anthropic-v0.86.0: anthropic==0.86.0 + anthropic-latest: anthropic==0.86.0 anthropic: pytest-asyncio anthropic-v0.16.0: httpx<0.28.0 anthropic-v0.39.0: httpx<0.28.0 @@ -454,16 +524,19 @@ deps = cohere-v5.10.0: cohere==5.10.0 cohere-v5.15.0: cohere==5.15.0 cohere-v5.20.7: cohere==5.20.7 + cohere-latest: cohere==5.20.7 google_genai-v1.29.0: google-genai==1.29.0 google_genai-v1.42.0: google-genai==1.42.0 google_genai-v1.55.0: google-genai==1.55.0 google_genai-v1.68.0: google-genai==1.68.0 + google_genai-latest: google-genai==1.68.0 google_genai: pytest-asyncio huggingface_hub-v0.24.7: huggingface_hub==0.24.7 huggingface_hub-v0.36.2: huggingface_hub==0.36.2 huggingface_hub-v1.7.1: huggingface_hub==1.7.1 + huggingface_hub-latest: huggingface_hub==1.7.1 huggingface_hub: responses huggingface_hub: pytest-httpx @@ -471,10 +544,12 @@ deps = litellm-v1.79.3: litellm==1.79.3 litellm-v1.81.16: litellm==1.81.16 litellm-v1.82.4: litellm==1.82.4 + litellm-latest: litellm==1.82.4 openai-base-v1.0.1: openai==1.0.1 openai-base-v1.109.1: openai==1.109.1 openai-base-v2.29.0: openai==2.29.0 + openai-base-latest: openai==2.29.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -482,6 +557,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.109.1: openai==1.109.1 openai-notiktoken-v2.29.0: openai==2.29.0 + openai-notiktoken-latest: openai==2.29.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 @@ -491,10 +567,12 @@ deps = boto3-v1.21.46: boto3==1.21.46 boto3-v1.33.13: boto3==1.33.13 boto3-v1.42.71: boto3==1.42.71 + boto3-latest: boto3==1.42.71 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 chalice-v1.32.0: chalice==1.32.0 + chalice-latest: chalice==1.32.0 chalice: pytest-chalice chalice: setuptools<82 @@ -504,13 +582,16 @@ deps = asyncpg-v0.26.0: asyncpg==0.26.0 asyncpg-v0.29.0: asyncpg==0.29.0 asyncpg-v0.31.0: asyncpg==0.31.0 + asyncpg-latest: asyncpg==0.31.0 asyncpg: pytest-asyncio clickhouse_driver-v0.2.10: clickhouse-driver==0.2.10 + clickhouse_driver-latest: clickhouse-driver==0.2.10 pymongo-v3.5.1: pymongo==3.5.1 pymongo-v3.13.0: pymongo==3.13.0 pymongo-v4.16.0: pymongo==4.16.0 + pymongo-latest: pymongo==4.16.0 pymongo: mockupdb redis-v2.10.6: redis==2.10.6 @@ -519,6 +600,7 @@ deps = redis-v5.3.1: redis==5.3.1 redis-v6.4.0: redis==6.4.0 redis-v7.3.0: redis==7.3.0 + redis-latest: redis==7.3.0 redis: fakeredis!=1.7.4 redis: pytest<8.0.0 redis-v4.6.0: fakeredis<2.31.0 @@ -527,31 +609,38 @@ deps = redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 redis_py_cluster_legacy-v2.1.3: redis-py-cluster==2.1.3 + redis_py_cluster_legacy-latest: redis-py-cluster==2.1.3 sqlalchemy-v1.2.19: sqlalchemy==1.2.19 sqlalchemy-v1.4.54: sqlalchemy==1.4.54 sqlalchemy-v2.0.48: sqlalchemy==2.0.48 sqlalchemy-v2.1.0b1: sqlalchemy==2.1.0b1 + sqlalchemy-latest: sqlalchemy==2.1.0b1 # ~~~ Flags ~~~ launchdarkly-v9.8.1: launchdarkly-server-sdk==9.8.1 launchdarkly-v9.15.0: launchdarkly-server-sdk==9.15.0 + launchdarkly-latest: launchdarkly-server-sdk==9.15.0 openfeature-v0.7.5: openfeature-sdk==0.7.5 openfeature-v0.8.4: openfeature-sdk==0.8.4 + openfeature-latest: openfeature-sdk==0.8.4 statsig-v0.55.3: statsig==0.55.3 statsig-v0.71.6: statsig==0.71.6 + statsig-latest: statsig==0.71.6 statsig: typing_extensions unleash-v6.0.1: UnleashClient==6.0.1 unleash-v6.7.0: UnleashClient==6.7.0 + unleash-latest: UnleashClient==6.7.0 # ~~~ GraphQL ~~~ ariadne-v0.20.1: ariadne==0.20.1 ariadne-v1.0.0: ariadne==1.0.0 + ariadne-latest: ariadne==1.0.0 ariadne: fastapi ariadne: flask ariadne: httpx @@ -559,9 +648,11 @@ deps = gql-v3.4.1: gql[all]==3.4.1 gql-v4.0.0: gql[all]==4.0.0 gql-v4.3.0b0: gql[all]==4.3.0b0 + gql-latest: gql[all]==4.3.0b0 graphene-v3.3: graphene==3.3 graphene-v3.4.3: graphene==3.4.3 + graphene-latest: graphene==3.4.3 graphene: blinker graphene: fastapi graphene: flask @@ -570,6 +661,7 @@ deps = strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 strawberry-v0.311.3: strawberry-graphql[fastapi,flask]==0.311.3 + strawberry-latest: strawberry-graphql[fastapi,flask]==0.311.3 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 @@ -580,6 +672,7 @@ deps = grpc-v1.62.3: grpcio==1.62.3 grpc-v1.78.0: grpcio==1.78.0 grpc-v1.80.0rc1: grpcio==1.80.0rc1 + grpc-latest: grpcio==1.80.0rc1 grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf @@ -589,19 +682,23 @@ deps = httpx-v0.20.0: httpx==0.20.0 httpx-v0.24.1: httpx==0.24.1 httpx-v0.28.1: httpx==0.28.1 + httpx-latest: httpx==0.28.1 httpx: anyio<4.0.0 httpx-v0.16.1: pytest-httpx==0.10.0 httpx-v0.20.0: pytest-httpx==0.14.0 httpx-v0.24.1: pytest-httpx==0.22.0 httpx-v0.28.1: pytest-httpx==0.35.0 + httpx-latest: pytest-httpx==0.35.0 requests-v2.12.5: requests==2.12.5 requests-v2.32.5: requests==2.32.5 + requests-latest: requests==2.32.5 # ~~~ Tasks ~~~ arq-v0.23: arq==0.23 arq-v0.27.0: arq==0.27.0 + arq-latest: arq==0.27.0 arq: async-timeout arq: pytest-asyncio arq: fakeredis>=2.2.0,<2.8 @@ -610,27 +707,33 @@ deps = beam-v2.14.0: apache-beam==2.14.0 beam-v2.71.0: apache-beam==2.71.0 beam-v2.72.0rc2: apache-beam==2.72.0rc2 + beam-latest: apache-beam==2.72.0rc2 beam: dill celery-v4.4.7: celery==4.4.7 celery-v5.6.2: celery==5.6.2 + celery-latest: celery==5.6.2 celery: newrelic<10.17.0 celery: redis {py3.7}-celery: importlib-metadata<5.0 dramatiq-v1.9.0: dramatiq==1.9.0 dramatiq-v2.1.0: dramatiq==2.1.0 + dramatiq-latest: dramatiq==2.1.0 huey-v2.1.3: huey==2.1.3 huey-v2.6.0: huey==2.6.0 + huey-latest: huey==2.6.0 ray-v2.7.2: ray==2.7.2 ray-v2.54.0: ray==2.54.0 + ray-latest: ray==2.54.0 rq-v0.6.0: rq==0.6.0 rq-v0.13.0: rq==0.13.0 rq-v1.16.2: rq==1.16.2 rq-v2.7.0: rq==2.7.0 + rq-latest: rq==2.7.0 rq: fakeredis<2.28.0 rq-v0.6.0: fakeredis<1.0 rq-v0.6.0: redis<3.2.2 @@ -640,6 +743,7 @@ deps = spark-v3.0.3: pyspark==3.0.3 spark-v3.5.8: pyspark==3.5.8 spark-v4.1.1: pyspark==4.1.1 + spark-latest: pyspark==4.1.1 # ~~~ Web 1 ~~~ @@ -649,6 +753,7 @@ deps = django-v4.2.29: django==4.2.29 django-v5.2.12: django==5.2.12 django-v6.0.3: django==6.0.3 + django-latest: django==6.0.3 django: psycopg2-binary django: djangorestframework django: pytest-django @@ -672,10 +777,13 @@ deps = django-v1.11.29: pytest-django<4.0 django-v2.2.28: pytest-django<4.0 {py3.14,py3.14t}-django: coverage==7.11.0 + django-latest: channels[daphne] + django-latest: pytest-asyncio flask-v1.1.4: flask==1.1.4 flask-v2.3.3: flask==2.3.3 flask-v3.1.3: flask==3.1.3 + flask-latest: flask==3.1.3 flask: flask-login flask: werkzeug flask-v1.1.4: werkzeug<2.1.0 @@ -686,6 +794,7 @@ deps = starlette-v0.40.0: starlette==0.40.0 starlette-v0.52.1: starlette==0.52.1 starlette-v1.0.0rc1: starlette==1.0.0rc1 + starlette-latest: starlette==1.0.0rc1 starlette: pytest-asyncio starlette: python-multipart starlette: requests @@ -700,6 +809,7 @@ deps = fastapi-v0.98.0: fastapi==0.98.0 fastapi-v0.117.1: fastapi==0.117.1 fastapi-v0.135.1: fastapi==0.135.1 + fastapi-latest: fastapi==0.135.1 fastapi: httpx fastapi: pytest-asyncio fastapi: python-multipart @@ -715,23 +825,28 @@ deps = aiohttp-v3.7.4: aiohttp==3.7.4 aiohttp-v3.10.11: aiohttp==3.10.11 aiohttp-v3.13.3: aiohttp==3.13.3 + aiohttp-latest: aiohttp==3.13.3 aiohttp: pytest-aiohttp aiohttp-v3.10.11: pytest-asyncio aiohttp-v3.13.3: pytest-asyncio + aiohttp-latest: pytest-asyncio bottle-v0.12.25: bottle==0.12.25 bottle-v0.13.4: bottle==0.13.4 + bottle-latest: bottle==0.13.4 bottle: werkzeug<2.1.0 falcon-v1.4.1: falcon==1.4.1 falcon-v2.0.0: falcon==2.0.0 falcon-v3.1.3: falcon==3.1.3 falcon-v4.2.0: falcon==4.2.0 + falcon-latest: falcon==4.2.0 litestar-v2.0.1: litestar==2.0.1 litestar-v2.7.2: litestar==2.7.2 litestar-v2.14.0: litestar==2.14.0 litestar-v2.21.1: litestar==2.21.1 + litestar-latest: litestar==2.21.1 litestar: pytest-asyncio litestar: python-multipart litestar: requests @@ -742,10 +857,12 @@ deps = pyramid-v1.8.6: pyramid==1.8.6 pyramid-v1.10.8: pyramid==1.10.8 pyramid-v2.1: pyramid==2.1 + pyramid-latest: pyramid==2.1 pyramid: werkzeug<2.1.0 quart-v0.16.3: quart==0.16.3 quart-v0.20.0: quart==0.20.0 + quart-latest: quart==0.20.0 quart: quart-auth quart: pytest-asyncio quart: Werkzeug @@ -755,20 +872,24 @@ deps = quart-v0.16.3: Werkzeug<2.3.0 quart-v0.16.3: hypercorn<0.15.0 {py3.8}-quart: taskgroup==0.0.0a4 + quart-latest: quart-flask-patch sanic-v0.8.3: sanic==0.8.3 sanic-v20.12.7: sanic==20.12.7 sanic-v23.12.2: sanic==23.12.2 sanic-v25.12.0: sanic==25.12.0 + sanic-latest: sanic==25.12.0 sanic: websockets<11.0 sanic: aiohttp sanic-v23.12.2: sanic-testing sanic-v25.12.0: sanic-testing {py3.6}-sanic: aiocontextvars==0.2.1 {py3.8}-sanic: tracerite<1.1.2 + sanic-latest: sanic-testing starlite-v1.48.1: starlite==1.48.1 starlite-v1.51.16: starlite==1.51.16 + starlite-latest: starlite==1.51.16 starlite: pytest-asyncio starlite: python-multipart starlite: requests @@ -778,6 +899,7 @@ deps = tornado-v6.0.4: tornado==6.0.4 tornado-v6.5.5: tornado==6.5.5 + tornado-latest: tornado==6.5.5 tornado: pytest tornado-v6.0.4: pytest<8.2 {py3.6}-tornado: aiocontextvars @@ -785,21 +907,25 @@ deps = # ~~~ Misc ~~~ loguru-v0.7.3: loguru==0.7.3 + loguru-latest: loguru==0.7.3 pure_eval-v0.0.3: pure_eval==0.0.3 pure_eval-v0.2.3: pure_eval==0.2.3 + pure_eval-latest: pure_eval==0.2.3 trytond-v4.6.22: trytond==4.6.22 trytond-v4.8.18: trytond==4.8.18 trytond-v5.8.16: trytond==5.8.16 trytond-v6.8.17: trytond==6.8.17 trytond-v7.8.6: trytond==7.8.6 + trytond-latest: trytond==7.8.6 trytond: werkzeug trytond-v4.6.22: werkzeug<1.0 trytond-v4.8.18: werkzeug<1.0 typer-v0.15.4: typer==0.15.4 typer-v0.24.1: typer==0.24.1 + typer-latest: typer==0.24.1 From cbe60a3bfc60093414c9351b4ffd9d609748a11c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 19 Mar 2026 12:18:20 +0100 Subject: [PATCH 2/3] fix(ci): Exclude -latest tox envs from CI runs runtox.sh uses grep to match tox environments, so a search like "py3.14-httpx" would match both the versioned env and the -latest alias, running tests twice. Filter out -latest envs unless explicitly requested. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/runtox.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/runtox.sh b/scripts/runtox.sh index bd6e954947..77aab28ae3 100755 --- a/scripts/runtox.sh +++ b/scripts/runtox.sh @@ -15,7 +15,12 @@ fi searchstring="$1" -ENV="$($TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" +# Filter out -latest environments unless explicitly requested +if [[ "$searchstring" == *-latest* ]]; then + ENV="$($TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" +else + ENV="$($TOXPATH -l | grep -- "$searchstring" | grep -v -- '-latest$' | tr $'\n' ',')" +fi if [ -z "${ENV}" ]; then echo "No targets found. Skipping." From f1712d23fe581e81a060a550ad62e52715fc8c87 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 19 Mar 2026 12:30:14 +0100 Subject: [PATCH 3/3] fix(tox): Make -latest alias point to highest stable release, not prerelease If the highest tested version is a prerelease (rc, alpha, beta), the -latest alias now points to the highest stable version instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/populate_tox/populate_tox.py | 16 ++++++++++++++-- scripts/populate_tox/tox.jinja | 10 +++++++--- tox.ini | 14 +++++++------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index cd624e1e24..fa0c31a3d1 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -760,18 +760,30 @@ def write_tox_file(packages: dict) -> None: for group, integrations in packages.items(): context["groups"][group] = [] for integration in integrations: + # Find the highest stable (non-prerelease) release for the + # -latest alias. Prereleases are always appended last by + # pick_releases_to_test, so we walk backwards. + latest_stable = None + for rel in reversed(integration["releases"]): + if not rel.is_prerelease: + latest_stable = rel + break + context["groups"][group].append( { "name": integration["name"], "package": integration["package"], "extra": integration["extra"], "releases": integration["releases"], + "latest_stable": latest_stable, "dependencies": _render_dependencies( integration["name"], integration["releases"] ), "latest_dependencies": _render_latest_dependencies( - integration["name"], integration["releases"][-1] - ), + integration["name"], latest_stable + ) + if latest_stable + else [], } ) context["testpaths"].append( diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 60df0dc83f..6a6d1a54c9 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -61,7 +61,9 @@ envlist = {% for release in integration.releases %} {{ release.rendered_python_versions }}-{{ integration.name }}-v{{ release }} {% endfor %} - {{ integration.releases[-1].rendered_python_versions }}-{{ integration.name }}-latest + {% if integration.latest_stable %} + {{ integration.latest_stable.rendered_python_versions }}-{{ integration.name }}-latest + {% endif %} {% endfor %} @@ -144,10 +146,12 @@ deps = {{ integration.name }}-v{{ release }}: {{ integration.package }}=={{ release }} {% endif %} {% endfor %} + {% if integration.latest_stable %} {% if integration.extra %} - {{ integration.name }}-latest: {{ integration.package }}[{{ integration.extra }}]=={{ integration.releases[-1] }} + {{ integration.name }}-latest: {{ integration.package }}[{{ integration.extra }}]=={{ integration.latest_stable }} {% else %} - {{ integration.name }}-latest: {{ integration.package }}=={{ integration.releases[-1] }} + {{ integration.name }}-latest: {{ integration.package }}=={{ integration.latest_stable }} + {% endif %} {% endif %} {% for dep in integration.dependencies %} {{ dep }} diff --git a/tox.ini b/tox.ini index 562823243b..1772ee1c73 100644 --- a/tox.ini +++ b/tox.ini @@ -184,7 +184,7 @@ envlist = {py3.6,py3.11,py3.12}-sqlalchemy-v1.4.54 {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.48 {py3.10,py3.13,py3.14,py3.14t}-sqlalchemy-v2.1.0b1 - {py3.10,py3.13,py3.14,py3.14t}-sqlalchemy-latest + {py3.7,py3.12,py3.13}-sqlalchemy-latest # ~~~ Flags ~~~ @@ -213,7 +213,7 @@ envlist = {py3.6,py3.9,py3.10}-gql-v3.4.1 {py3.9,py3.12,py3.13}-gql-v4.0.0 {py3.9,py3.13,py3.14,py3.14t}-gql-v4.3.0b0 - {py3.9,py3.13,py3.14,py3.14t}-gql-latest + {py3.9,py3.12,py3.13}-gql-latest {py3.6,py3.9,py3.10}-graphene-v3.3 {py3.8,py3.12,py3.13}-graphene-v3.4.3 @@ -615,7 +615,7 @@ deps = sqlalchemy-v1.4.54: sqlalchemy==1.4.54 sqlalchemy-v2.0.48: sqlalchemy==2.0.48 sqlalchemy-v2.1.0b1: sqlalchemy==2.1.0b1 - sqlalchemy-latest: sqlalchemy==2.1.0b1 + sqlalchemy-latest: sqlalchemy==2.0.48 # ~~~ Flags ~~~ @@ -648,7 +648,7 @@ deps = gql-v3.4.1: gql[all]==3.4.1 gql-v4.0.0: gql[all]==4.0.0 gql-v4.3.0b0: gql[all]==4.3.0b0 - gql-latest: gql[all]==4.3.0b0 + gql-latest: gql[all]==4.0.0 graphene-v3.3: graphene==3.3 graphene-v3.4.3: graphene==3.4.3 @@ -672,7 +672,7 @@ deps = grpc-v1.62.3: grpcio==1.62.3 grpc-v1.78.0: grpcio==1.78.0 grpc-v1.80.0rc1: grpcio==1.80.0rc1 - grpc-latest: grpcio==1.80.0rc1 + grpc-latest: grpcio==1.78.0 grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf @@ -707,7 +707,7 @@ deps = beam-v2.14.0: apache-beam==2.14.0 beam-v2.71.0: apache-beam==2.71.0 beam-v2.72.0rc2: apache-beam==2.72.0rc2 - beam-latest: apache-beam==2.72.0rc2 + beam-latest: apache-beam==2.71.0 beam: dill celery-v4.4.7: celery==4.4.7 @@ -794,7 +794,7 @@ deps = starlette-v0.40.0: starlette==0.40.0 starlette-v0.52.1: starlette==0.52.1 starlette-v1.0.0rc1: starlette==1.0.0rc1 - starlette-latest: starlette==1.0.0rc1 + starlette-latest: starlette==0.52.1 starlette: pytest-asyncio starlette: python-multipart starlette: requests