diff --git a/superset/db_engine_specs/redshift.py b/superset/db_engine_specs/redshift.py index de6a2a5b514..c4c266ab6e8 100644 --- a/superset/db_engine_specs/redshift.py +++ b/superset/db_engine_specs/redshift.py @@ -46,9 +46,25 @@ from superset.utils import json # Setuptools 80.x (pinned in requirements/base.txt) raises this as a plain # UserWarning, not DeprecationWarning -- don't add category=DeprecationWarning # here, it would silently stop matching. +# +# Scoped to the sqlalchemy_redshift module (via stacklevel=2 in setuptools' +# own warn() call, the warning is attributed to whatever imports +# pkg_resources, i.e. sqlalchemy_redshift/__init__.py) so this doesn't also +# swallow the same deprecation warning from unrelated dependencies. +# +# The same filter is also registered unconditionally in +# SupersetAppInitializer.configure_logging() (superset/initialization/ +# __init__.py), before it dispatches to the (deployment-replaceable) +# LOGGING_CONFIGURATOR. That's now the primary suppression point for the +# web app and celery workers; this one remains as a fallback for standalone +# scripts that import this module without going through create_app() +# (filterwarnings() calls are idempotent, so registering it twice is +# harmless). warnings.filterwarnings( "ignore", message=r"pkg_resources is deprecated as an API", + category=UserWarning, + module=r"sqlalchemy_redshift(?:\..*)?", ) logger = logging.getLogger() diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 9bd94bd54ef..851dc04a775 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -20,6 +20,7 @@ import contextlib import logging import os import sys +import warnings from typing import Any, Callable, TYPE_CHECKING import wtforms_json @@ -1392,6 +1393,20 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods ) def configure_logging(self) -> None: + # sqlalchemy-redshift's own __init__ still imports pkg_resources (see + # superset/db_engine_specs/redshift.py for the full rationale). This + # filter used to live in DefaultLoggingConfigurator.configure_logging(), + # but LOGGING_CONFIGURATOR is a deployment-replaceable hook -- any + # custom configurator skipped it entirely and still hit the warning. + # Registering it here, before LOGGING_CONFIGURATOR runs, guarantees + # it's installed regardless of which configurator is configured. + warnings.filterwarnings( + "ignore", + message=r"pkg_resources is deprecated as an API", + category=UserWarning, + module=r"sqlalchemy_redshift(?:\..*)?", + ) + self.config["LOGGING_CONFIGURATOR"].configure_logging( self.config, self.superset_app.debug ) diff --git a/superset/mcp_service/__init__.py b/superset/mcp_service/__init__.py index 75949faa7c9..419ed13fe49 100644 --- a/superset/mcp_service/__init__.py +++ b/superset/mcp_service/__init__.py @@ -47,6 +47,18 @@ warnings.filterwarnings( message=r"authlib\.jose module is deprecated", ) +# sqlalchemy-redshift's own __init__ still imports pkg_resources (see the +# equivalent filter and rationale in superset/db_engine_specs/redshift.py). +# MCP tools that touch a Redshift-backed database trigger this import via a +# separate process from the main web/worker app, so it needs its own filter +# here rather than relying on db_engine_specs/redshift.py having been loaded. +warnings.filterwarnings( + "ignore", + message=r"pkg_resources is deprecated as an API", + category=UserWarning, + module=r"sqlalchemy_redshift(?:\..*)?", +) + __version__ = "1.0.0" # Tools are auto-registered when imported by the MCP service diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py index 37101cc0011..630141d712a 100644 --- a/superset/mcp_service/server.py +++ b/superset/mcp_service/server.py @@ -63,6 +63,8 @@ def _suppress_third_party_warnings() -> None: - marshmallow ``RemovedInMarshmallow4Warning`` (triggered during database engine schema instantiation) - google.api_core ``FutureWarning`` (Python version support notices) + - sqlalchemy-redshift ``pkg_resources`` UserWarning (see + superset/db_engine_specs/redshift.py for details) """ import warnings @@ -83,6 +85,19 @@ def _suppress_third_party_warnings() -> None: "ignore", message=r"authlib\.jose module is deprecated", ) + # Same treatment for the pkg_resources warning suppressed at package + # init time. Confirmed non-redundant: warnings.filters can be reset + # between the package import and this call (e.g. pytest's warnings + # plugin resets it around every test -- test_suppress_third_party_warnings + # below fails without this line, proving the reset scenario is real, + # not hypothetical), so re-registering here is load-bearing, not + # belt-and-suspenders. + warnings.filterwarnings( + "ignore", + message=r"pkg_resources is deprecated as an API", + category=UserWarning, + module=r"sqlalchemy_redshift(?:\..*)?", + ) def _downgrade_to_warning(record: logging.LogRecord) -> None: diff --git a/tests/unit_tests/initialization_test.py b/tests/unit_tests/initialization_test.py index 860063905a0..e68560906ce 100644 --- a/tests/unit_tests/initialization_test.py +++ b/tests/unit_tests/initialization_test.py @@ -226,6 +226,51 @@ class TestSupersetAppInitializer: assert "secretpass" not in output assert "postgresql://user:***@localhost:5432/db" in output + @patch("superset.initialization.logger") + def test_configure_logging_installs_pkg_resources_filter_before_configurator( + self, mock_logger + ) -> None: + """The pkg_resources warning filter must be installed before + LOGGING_CONFIGURATOR.configure_logging() dispatches, so a deployment's + custom configurator (which may skip DefaultLoggingConfigurator's own + filter) still benefits from it.""" + import re + import warnings + + def has_pkg_resources_filter() -> bool: + return any( + f[0] == "ignore" + and isinstance(f[1], re.Pattern) + and f[1].pattern == r"pkg_resources is deprecated as an API" + and f[2] is UserWarning + and isinstance(f[3], re.Pattern) + and f[3].pattern == r"sqlalchemy_redshift(?:\..*)?" + for f in warnings.filters + ) + + seen_during_dispatch = [] + + class RecordingConfigurator: + def configure_logging(self, app_config, debug_mode): + seen_during_dispatch.append(has_pkg_resources_filter()) + + mock_app = MagicMock() + mock_app.config = {"LOGGING_CONFIGURATOR": RecordingConfigurator()} + mock_app.debug = False + app_initializer = SupersetAppInitializer(mock_app) + + with warnings.catch_warnings(): + # Isolate from filters registered by other tests/import side effects. + warnings.resetwarnings() + assert not has_pkg_resources_filter() + + app_initializer.configure_logging() + + assert seen_during_dispatch == [True], ( + "pkg_resources filter must already be installed by the time " + "LOGGING_CONFIGURATOR.configure_logging() runs" + ) + def test_check_and_warn_database_connection_invalid_uri(self) -> None: """Test that invalid URIs are handled safely without crashing.""" mock_app = MagicMock() diff --git a/tests/unit_tests/mcp_service/test_mcp_server.py b/tests/unit_tests/mcp_service/test_mcp_server.py index 9b2de2c74ca..4ab76afecaa 100644 --- a/tests/unit_tests/mcp_service/test_mcp_server.py +++ b/tests/unit_tests/mcp_service/test_mcp_server.py @@ -151,6 +151,24 @@ def test_suppress_third_party_warnings(): ] assert len(google_filters) >= 1, "Expected google FutureWarning filter" + # Verify pkg_resources UserWarning filter is installed, scoped to + # sqlalchemy_redshift (sqlalchemy-redshift triggers this via a late + # import on Redshift-backed connections; see + # superset/db_engine_specs/redshift.py for the full rationale). Scoping + # by category+module keeps this from also swallowing the same + # deprecation message from unrelated dependencies. + pkg_resources_filters = [ + f + for f in warnings.filters + if f[0] == "ignore" + and f[2] is UserWarning + and isinstance(f[1], re.Pattern) + and f[1].pattern == r"pkg_resources is deprecated as an API" + and isinstance(f[3], re.Pattern) + and f[3].pattern == r"sqlalchemy_redshift(?:\..*)?" + ] + assert len(pkg_resources_filters) >= 1, "Expected pkg_resources warning filter" + def test_create_event_store_returns_none_when_redis_store_fails(): """EventStore returns None when Redis store creation fails."""