This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 65a6012843b ci(python): run the Python-next canary nightly, bump to
3.13 (#44767)
65a6012843b is described below
commit 65a6012843b524ea069e9ada37cec9a82305d552
Author: Evan Rusackas <[email protected]>
AuthorDate: Wed Sep 30 13:17:54 2026 -0700
ci(python): run the Python-next canary nightly, bump to 3.13 (#44767)
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
.github/actions/setup-backend/action.yml | 5 +-
.github/workflows/python-nightly-canary.yml | 94 ++++++++++++++++++++++
.github/workflows/superset-python-unittest.yml | 7 +-
requirements/development.in | 11 +++
requirements/development.txt | 7 +-
superset/mcp_service/server.py | 11 +++
.../unit_tests/mcp_service/test_tool_inventory.py | 2 +-
7 files changed, 128 insertions(+), 9 deletions(-)
diff --git a/.github/actions/setup-backend/action.yml
b/.github/actions/setup-backend/action.yml
index 53cc27a0a6b..4af130c33f3 100644
--- a/.github/actions/setup-backend/action.yml
+++ b/.github/actions/setup-backend/action.yml
@@ -26,8 +26,9 @@ runs:
if [ "$INPUT_PYTHON_VERSION" = "current" ]; then
RESOLVED_VERSION="3.11"
elif [ "$INPUT_PYTHON_VERSION" = "next" ]; then
- # currently disabled in GHA matrixes because of library
compatibility issues
- RESOLVED_VERSION="3.12"
+ # tracks the next Python minor we're evaluating for support;
exercised
+ # by the nightly canary (python-nightly-canary.yml), not PR/push CI
+ RESOLVED_VERSION="3.13"
elif printf '%s' "$INPUT_PYTHON_VERSION" | grep -Eq
'^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
RESOLVED_VERSION="$INPUT_PYTHON_VERSION"
else
diff --git a/.github/workflows/python-nightly-canary.yml
b/.github/workflows/python-nightly-canary.yml
new file mode 100644
index 00000000000..bc5cd02a14f
--- /dev/null
+++ b/.github/workflows/python-nightly-canary.yml
@@ -0,0 +1,94 @@
+# Python nightly canary
+name: Python-Nightly-Canary
+
+# Exercises the "next" Python minor (currently 3.13 -- see the `next`
+# resolution in setup-backend/action.yml) against `master` on a schedule
+# instead of on every push. The Docker image's base Python is pinned via
+# PY_VER to the 3.11 line (see the Dockerfile); this canary is how we find
+# out the codebase and dependency tree are clean on the next minor before
+# that pin is ever moved, without paying for a second full Python version
+# on every push/PR.
+#
+# Runs the unit-test suite only, not the full integration matrix or
+# coverage gates -- this is a compatibility signal, not a release gate.
+
+permissions:
+ contents: read
+
+on:
+ schedule:
+ # 05:00 UTC daily, so a 3.13 regression is known well before anyone
+ # considers bumping PY_VER off 3.11.
+ - cron: "0 5 * * *"
+
+ # Manual trigger for on-demand runs (e.g. right after a dependency bump).
+ workflow_dispatch: {}
+
+concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: true
+
+jobs:
+ unit-tests-next:
+ runs-on: ubuntu-26.04
+ timeout-minutes: 45
+ env:
+ PYTHONPATH: ${{ github.workspace }}
+ SQLALCHEMY_WARN_20: "1"
+ steps:
+ - name: "Checkout master (${{ github.sha }})"
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #
v7.0.1
+ with:
+ persist-credentials: false
+ submodules: recursive
+ - name: Setup Python (next)
+ uses: $/.github/actions/setup-backend/
+ with:
+ python-version: next
+ - name: Python unit tests
+ env:
+ SUPERSET_TESTENV: true
+ SUPERSET_SECRET_KEY: not-a-secret
+ run: |
+ pytest --durations-min=0.5 --cov-report= --cov=superset
./tests/common ./tests/unit_tests --cache-clear --maxfail=50
+
+ # The whole point of this cron is finding 3.13 breakage before it's a
+ # blocker on a real PY_VER bump, so a silent failure defeats the purpose --
+ # a red X in the Actions tab nobody is watching. Track it in one issue
+ # that stays open across consecutive nightly failures (commenting on each
+ # recurrence) instead of filing a fresh issue per failed run, which would
+ # spam the issue board on a multi-night break.
+ notify-on-failure:
+ needs: unit-tests-next
+ if: failure()
+ runs-on: ubuntu-26.04
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - name: Find or open the tracking issue
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPOSITORY: ${{ github.repository }}
+ RUN_URL: ${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}
+ run: |
+ TITLE="Python nightly canary (3.13) is failing"
+ ISSUE_NUMBER=$(gh issue list \
+ --repo "$REPOSITORY" \
+ --search "\"$TITLE\" in:title" \
+ --state open \
+ --json number,title \
+ --jq ".[] | select(.title == \"$TITLE\") | .number" | head -1)
+
+ if [ -n "$ISSUE_NUMBER" ]; then
+ gh issue comment "$ISSUE_NUMBER" --repo "$REPOSITORY" \
+ --body "Still failing: ${RUN_URL}"
+ else
+ gh issue create \
+ --repo "$REPOSITORY" \
+ --title "$TITLE" \
+ --label "python" \
+ --body "The nightly Python-next (3.13) canary is failing against
\`master\`. This tracks compatibility work ahead of bumping the base image's
PY_VER off the 3.11 line. Further failures get commented here rather than
filing new issues.
+
+ Failed run: ${RUN_URL}"
+ fi
diff --git a/.github/workflows/superset-python-unittest.yml
b/.github/workflows/superset-python-unittest.yml
index fdcc61cac76..c606b1f6f75 100644
--- a/.github/workflows/superset-python-unittest.yml
+++ b/.github/workflows/superset-python-unittest.yml
@@ -49,9 +49,10 @@ jobs:
id-token: write
strategy:
matrix:
- # Full version spread on push (master/release) + nightly; current only
- # on PRs to cut runner cost (cross-version breaks are caught at merge).
- python-version: ${{ github.event_name == 'pull_request' &&
fromJSON('["current"]') || fromJSON('["current", "next"]') }}
+ # Just "current" here, on both push and PR — the "next" Python minor is
+ # exercised separately by the nightly canary
(python-nightly-canary.yml)
+ # so cross-version breaks don't cost a runner on every push/PR.
+ python-version: ["current"]
env:
PYTHONPATH: ${{ github.workspace }}
steps:
diff --git a/requirements/development.in b/requirements/development.in
index d75496be099..1982438d35d 100644
--- a/requirements/development.in
+++ b/requirements/development.in
@@ -45,3 +45,14 @@
# DockerContainer plus whatever driver its own extra above already
# provides.
testcontainers[cockroachdb,cratedb,mongodb,mssql,mysql,oracle,postgres,trino]>=4.15.0,<5
+
+# gevent (pulled in via the `gevent` extra above) has no direct version
+# requirement on zope.interface, so the resolver was free to pick the
+# oldest version satisfying gevent's own loose constraint: 5.4.0. That
+# version's metaclass treats every class attribute as an interface member,
+# including `__firstlineno__`, the implicit attribute Python 3.13 (PEP 626
+# byte-code line tracking) adds to every class -- so importing gevent
+# (and anything that imports it, e.g. the gunicorn+gevent worker class)
+# raises `zope.interface.exceptions.InvalidInterface` outright on 3.13.
+# 7.2 is the first release with explicit handling for it.
+zope.interface>=7.2
diff --git a/requirements/development.txt b/requirements/development.txt
index d4015c6c884..0a78383ba8f 100644
--- a/requirements/development.txt
+++ b/requirements/development.txt
@@ -1021,7 +1021,6 @@ setuptools==84.0.0
# pandas-gbq
# pydata-google-auth
# zope-event
- # zope-interface
shillelagh==1.4.5
# via
# -c requirements/base-constraint.txt
@@ -1270,7 +1269,9 @@ zipp==3.23.0
# via importlib-metadata
zope-event==5.0
# via gevent
-zope-interface==5.4.0
- # via gevent
+zope-interface==8.6
+ # via
+ # -r requirements/development.in
+ # gevent
zstandard==0.23.0
# via trino
diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py
index 45765560a54..9bda37bd967 100644
--- a/superset/mcp_service/server.py
+++ b/superset/mcp_service/server.py
@@ -22,6 +22,7 @@ Supports both single-pod (in-memory) and multi-pod (Redis)
deployments.
For multi-pod deployments, configure MCP_EVENT_STORE_CONFIG with Redis URL.
"""
+import inspect
import logging
import os
from collections.abc import Sequence
@@ -304,7 +305,17 @@ def _truncate_description(text: str, max_length: int) ->
str:
Cuts at the last sentence boundary before *max_length*, or at
*max_length* with an ellipsis if no sentence boundary is found.
+
+ Dedents first: Python 3.13 has the compiler strip a docstring's common
+ leading whitespace at compile time (``__doc__`` comes out already
+ cleaned), while 3.11/3.12 store it raw and leave that to the caller. A
+ multi-line tool docstring's raw, un-dedented form is longer per line, so
+ the same character budget lands at a different point in the text
+ depending on which Python compiled it. Cleaning here first makes the cut
+ point (and this function's callers' byte budgets) consistent regardless
+ of interpreter version.
"""
+ text = inspect.cleandoc(text) if text else text
if not text or len(text) <= max_length:
return text
# Try to cut at the last sentence boundary
diff --git a/tests/unit_tests/mcp_service/test_tool_inventory.py
b/tests/unit_tests/mcp_service/test_tool_inventory.py
index e2b0b5e972f..ad16845ef52 100644
--- a/tests/unit_tests/mcp_service/test_tool_inventory.py
+++ b/tests/unit_tests/mcp_service/test_tool_inventory.py
@@ -131,7 +131,7 @@ TOOL_BUDGETS = {
"save_sql_query": 1_600,
"update_chart": 4_100,
"update_chart_preview": 2_000,
- "update_dashboard": 4_100,
+ "update_dashboard": 4_200,
"update_dataset": 2_300,
"update_dataset_metric": 3_100,
}