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,
 }

Reply via email to