From f220397bb398677f70c71873e6617fafb0fe5954 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 6 Oct 2025 18:16:30 -0400 Subject: [PATCH 01/17] My Local Setup (Revert this commit later) --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d58ed84488f6..280b87c05332 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,7 +67,7 @@ services: container_name: superset_cache restart: unless-stopped ports: - - "127.0.0.1:6379:6379" + - "6379:6379" volumes: - redis:/data @@ -81,7 +81,7 @@ services: container_name: superset_db restart: unless-stopped ports: - - "127.0.0.1:5432:5432" + - "5433:5432" volumes: - db_home:/var/lib/postgresql/data - ./docker/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d From 663922c216513225f200201261533e05012e6c6a Mon Sep 17 00:00:00 2001 From: FLAME Date: Sat, 11 Oct 2025 12:13:11 -0400 Subject: [PATCH 02/17] Upgrade to Marshmallow >= 4 with Superset able to be booted up and able to be logged in --- docker/pythonpath_dev/superset_config.py | 4 +++- pyproject.toml | 3 +-- requirements/base.in | 3 ++- requirements/base.txt | 4 ++-- requirements/development.txt | 4 ++-- superset/advanced_data_type/schemas.py | 4 ++-- superset/charts/schemas.py | 10 +++++---- superset/config.py | 6 +++--- superset/db_engine_specs/databend.py | 21 ++++++++++++------ superset/initialization/__init__.py | 27 ++++++++++++++++-------- superset/reports/schemas.py | 4 ++-- superset/themes/schemas.py | 12 ++++++++--- 12 files changed, 64 insertions(+), 38 deletions(-) diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py index d88d9899c27a..6ac050a36603 100644 --- a/docker/pythonpath_dev/superset_config.py +++ b/docker/pythonpath_dev/superset_config.py @@ -105,7 +105,9 @@ class CeleryConfig: CELERY_CONFIG = CeleryConfig -FEATURE_FLAGS = {"ALERT_REPORTS": True} +FEATURE_FLAGS = { + "ALERT_REPORTS": False +} # Temporarily disabled for marshmallow 4.x compatibility ALERT_REPORTS_NOTIFICATION_DRY_RUN = True WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501 # The base URL for the email report hyperlinks. diff --git a/pyproject.toml b/pyproject.toml index 0f1aa6faaf1b..734aaa5ebd31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,8 +67,7 @@ dependencies = [ "jsonpath-ng>=1.6.1, <2", "Mako>=1.2.2", "markdown>=3.0", - # marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162 - "marshmallow>=3.0, <4", + "marshmallow>=4", "marshmallow-union>=0.1", "msgpack>=1.0.0, <1.1", "nh3>=0.2.11, <0.3", diff --git a/requirements/base.in b/requirements/base.in index d110fa893142..7ef3a2c5f097 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -32,7 +32,8 @@ apispec>=6.0.0,<6.7.0 # causing CI to fail. 1.4.0 is the last version that works. # https://marshmallow-sqlalchemy.readthedocs.io/en/latest/changelog.html#id3 # Opened this issue https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/665 -marshmallow-sqlalchemy>=1.3.0,<1.4.1 +# Update: Upgrading to 1.4.2+ for marshmallow 4.x compatibility +marshmallow-sqlalchemy>=1.4.2 # needed for python 3.12 support openapi-schema-validator>=0.6.3 diff --git a/requirements/base.txt b/requirements/base.txt index 3ff7c38950de..2c97ee37edce 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -217,13 +217,13 @@ markupsafe==3.0.2 # mako # werkzeug # wtforms -marshmallow==3.26.1 +marshmallow==4.0.0 # via # apache-superset (pyproject.toml) # flask-appbuilder # marshmallow-sqlalchemy # marshmallow-union -marshmallow-sqlalchemy==1.4.0 +marshmallow-sqlalchemy==1.4.2 # via # -r requirements/base.in # flask-appbuilder diff --git a/requirements/development.txt b/requirements/development.txt index 317ef4e119a8..3ad1b0615e12 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -443,14 +443,14 @@ markupsafe==3.0.2 # mako # werkzeug # wtforms -marshmallow==3.26.1 +marshmallow==4.0.0 # via # -c requirements/base-constraint.txt # apache-superset # flask-appbuilder # marshmallow-sqlalchemy # marshmallow-union -marshmallow-sqlalchemy==1.4.0 +marshmallow-sqlalchemy==1.4.2 # via # -c requirements/base-constraint.txt # flask-appbuilder diff --git a/superset/advanced_data_type/schemas.py b/superset/advanced_data_type/schemas.py index d94fd19873b5..47918e5416f0 100644 --- a/superset/advanced_data_type/schemas.py +++ b/superset/advanced_data_type/schemas.py @@ -23,10 +23,10 @@ advanced_data_type_convert_schema = { "type": "object", "properties": { - "type": {"type": "string", "default": "port"}, + "type": {"type": "string", "dump_default": "port"}, "values": { "type": "array", - "items": {"default": "http"}, + "items": {"dump_default": "http"}, "minItems": 1, }, }, diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index 2e3dec7fd856..4ac6a538b270 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -459,7 +459,7 @@ class ChartDataAggregateOptionsSchema(ChartDataPostProcessingOperationOptionsSch allow_none=False, metadata={"description": "Columns by which to group by"}, ), - minLength=1, + validate=Length(min=1), required=True, ), ) @@ -657,7 +657,9 @@ class ChartDataProphetOptionsSchema(ChartDataPostProcessingOperationOptionsSchem "the future", "example": 7, }, - min=0, + validate=[ + Range(min=0, error=_("`periods` must be greater than or equal to 0")) + ], required=True, ) confidence_interval = fields.Float( @@ -791,7 +793,7 @@ class ChartDataPivotOptionsSchema(ChartDataPostProcessingOperationOptionsSchema) fields.List( fields.String(allow_none=False), metadata={"description": "Columns to group by on the table index (=rows)"}, - minLength=1, + validate=Length(min=1), required=True, ), ) @@ -1643,7 +1645,7 @@ class DashboardSchema(Schema): class ChartGetResponseSchema(Schema): - id = fields.Int(description=id_description) + id = fields.Int(metadata={"description": id_description}) url = fields.String() cache_timeout = fields.String() certified_by = fields.String() diff --git a/superset/config.py b/superset/config.py index 662e79576e10..482d1b66c30e 100644 --- a/superset/config.py +++ b/superset/config.py @@ -86,7 +86,7 @@ # https://github.com/apache/superset/blob/master/superset/utils/log.py EVENT_LOGGER = DBEventLogger() -SUPERSET_LOG_VIEW = True +SUPERSET_LOG_VIEW = False # Temporarily disabled for marshmallow 4.x compatibility # This config is used to enable/disable the folowing security menu items: # List Users, List Roles, List Groups @@ -524,7 +524,7 @@ class D3TimeFormat(TypedDict, total=False): # It is dependent on ENABLE_DASHBOARD_SCREENSHOT_ENDPOINT being enabled. "ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT": False, "TAGGING_SYSTEM": False, - "SQLLAB_BACKEND_PERSISTENCE": True, + "SQLLAB_BACKEND_PERSISTENCE": False, # Temporarily disabled for marshmallow 4.x compatibility "LISTVIEWS_DEFAULT_CARD_VIEW": False, # When True, this escapes HTML (rather than rendering it) in Markdown components "ESCAPE_MARKDOWN_HTML": False, @@ -1386,7 +1386,7 @@ def allowed_schemas_for_csv_upload( # pylint: disable=unused-argument SILENCE_FAB = True FAB_ADD_SECURITY_VIEWS = True -FAB_ADD_SECURITY_API = True +FAB_ADD_SECURITY_API = False # Temporarily disabled for marshmallow 4.x compatibility FAB_ADD_SECURITY_PERMISSION_VIEW = False FAB_ADD_SECURITY_VIEW_MENU_VIEW = False FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW = False diff --git a/superset/db_engine_specs/databend.py b/superset/db_engine_specs/databend.py index 9789512450b6..e248e914634d 100644 --- a/superset/db_engine_specs/databend.py +++ b/superset/db_engine_specs/databend.py @@ -190,20 +190,27 @@ def get_function_names(cls, database: Database) -> list[str]: class DatabendParametersSchema(Schema): - username = fields.String(allow_none=True, description=__("Username")) - password = fields.String(allow_none=True, description=__("Password")) - host = fields.String(required=True, description=__("Hostname or IP address")) + username = fields.String(allow_none=True, metadata={"description": __("Username")}) + password = fields.String(allow_none=True, metadata={"description": __("Password")}) + host = fields.String( + required=True, metadata={"description": __("Hostname or IP address")} + ) port = fields.Integer( allow_none=True, - description=__("Database port"), + metadata={"description": __("Database port")}, validate=Range(min=0, max=65535), ) - database = fields.String(allow_none=True, description=__("Database name")) + database = fields.String( + allow_none=True, metadata={"description": __("Database name")} + ) encryption = fields.Boolean( - default=True, description=__("Use an encrypted connection to the database") + dump_default=True, + metadata={"description": __("Use an encrypted connection to the database")}, ) query = fields.Dict( - keys=fields.Str(), values=fields.Raw(), description=__("Additional parameters") + keys=fields.Str(), + values=fields.Raw(), + metadata={"description": __("Additional parameters")}, ) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 133456f35a6d..85f062a2893b 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -178,9 +178,6 @@ def init_views(self) -> None: from superset.extensions.view import ExtensionsView from superset.importexport.api import ImportExportRestApi from superset.queries.api import QueryRestApi - from superset.queries.saved_queries.api import SavedQueryRestApi - from superset.reports.api import ReportScheduleRestApi - from superset.reports.logs.api import ReportExecutionLogRestApi from superset.row_level_security.api import RLSRestApi from superset.security.api import ( RoleRestAPI, @@ -208,7 +205,6 @@ def init_views(self) -> None: from superset.views.error_handling import set_app_error_handlers from superset.views.explore import ExplorePermalinkView, ExploreView from superset.views.groups import GroupsListView - from superset.views.log.api import LogRestApi from superset.views.logs import ActionLogView from superset.views.roles import RolesListView from superset.views.sql_lab.views import ( @@ -265,14 +261,22 @@ def init_views(self) -> None: appbuilder.add_api(ExplorePermalinkRestApi) appbuilder.add_api(ImportExportRestApi) appbuilder.add_api(QueryRestApi) - appbuilder.add_api(ReportScheduleRestApi) - appbuilder.add_api(ReportExecutionLogRestApi) appbuilder.add_api(RLSRestApi) - appbuilder.add_api(SavedQueryRestApi) appbuilder.add_api(TagRestApi) appbuilder.add_api(SqlLabRestApi) appbuilder.add_api(SqlLabPermalinkRestApi) - appbuilder.add_api(LogRestApi) + + if feature_flag_manager.is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"): + from superset.queries.saved_queries.api import SavedQueryRestApi + + appbuilder.add_api(SavedQueryRestApi) + + if feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): + from superset.reports.api import ReportScheduleRestApi + from superset.reports.logs.api import ReportExecutionLogRestApi + + appbuilder.add_api(ReportScheduleRestApi) + appbuilder.add_api(ReportExecutionLogRestApi) if feature_flag_manager.is_feature_enabled("ENABLE_EXTENSIONS"): from superset.extensions.api import ExtensionsRestApi @@ -472,7 +476,6 @@ def init_views(self) -> None: category="Manage", menu_cond=lambda: feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"), ) - appbuilder.add_api(LogRestApi) appbuilder.add_api(UserRegistrationsRestAPI) appbuilder.add_view( ActionLogView, @@ -486,6 +489,12 @@ def init_views(self) -> None: and self.config["SUPERSET_LOG_VIEW"] ), ) + + if self.config["FAB_ADD_SECURITY_VIEWS"] and self.config["SUPERSET_LOG_VIEW"]: + from superset.views.log.api import LogRestApi + + appbuilder.add_api(LogRestApi) + appbuilder.add_api(SecurityRestApi) # # Conditionally setup email views diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py index cfccc579bc04..305ccd9a04e3 100644 --- a/superset/reports/schemas.py +++ b/superset/reports/schemas.py @@ -240,7 +240,7 @@ class ReportSchedulePostSchema(Schema): }, allow_none=True, required=False, - default=None, + load_default=None, ) @validates("custom_width") @@ -378,7 +378,7 @@ class ReportSchedulePutSchema(Schema): }, allow_none=True, required=False, - default=None, + load_default=None, ) @validates("custom_width") diff --git a/superset/themes/schemas.py b/superset/themes/schemas.py index 6594e30c1990..8ce5551eee04 100644 --- a/superset/themes/schemas.py +++ b/superset/themes/schemas.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from contextvars import ContextVar from typing import Any from marshmallow import fields, Schema, validates, ValidationError @@ -21,6 +22,11 @@ from superset.themes.utils import is_valid_theme, sanitize_theme_tokens from superset.utils import json +# Context variable for storing sanitized JSON data during validation +sanitized_json_context: ContextVar[str | None] = ContextVar( + "sanitized_json_data", default=None +) + class ImportV1ThemeSchema(Schema): theme_name = fields.String(required=True) @@ -56,7 +62,7 @@ def validate_json_data(self, value: dict[str, Any]) -> None: value.clear() value.update(sanitized_config) else: - self.context["sanitized_json_data"] = json.dumps(sanitized_config) + sanitized_json_context.set(json.dumps(sanitized_config)) class ThemePostSchema(Schema): @@ -87,7 +93,7 @@ def validate_and_sanitize_json_data(self, value: str) -> None: # Note: This modifies the input data to ensure sanitized content is stored if sanitized_config != theme_config: # Re-serialize the sanitized config - self.context["sanitized_json_data"] = json.dumps(sanitized_config) + sanitized_json_context.set(json.dumps(sanitized_config)) class ThemePutSchema(Schema): @@ -118,7 +124,7 @@ def validate_and_sanitize_json_data(self, value: str) -> None: # Note: This modifies the input data to ensure sanitized content is stored if sanitized_config != theme_config: # Re-serialize the sanitized config - self.context["sanitized_json_data"] = json.dumps(sanitized_config) + sanitized_json_context.set(json.dumps(sanitized_config)) openapi_spec_methods_override = { From 7b4f545a328a97116dd8ce05f0ff9165192d2180 Mon Sep 17 00:00:00 2001 From: FLAME Date: Sat, 11 Oct 2025 12:26:30 -0400 Subject: [PATCH 03/17] Revert "My Local Setup (Revert this commit later)" This reverts commit f220397bb398677f70c71873e6617fafb0fe5954. --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 280b87c05332..d58ed84488f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,7 +67,7 @@ services: container_name: superset_cache restart: unless-stopped ports: - - "6379:6379" + - "127.0.0.1:6379:6379" volumes: - redis:/data @@ -81,7 +81,7 @@ services: container_name: superset_db restart: unless-stopped ports: - - "5433:5432" + - "127.0.0.1:5432:5432" volumes: - db_home:/var/lib/postgresql/data - ./docker/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d From c841166d8036bd35906f8335dbb74456b021aac0 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 27 Oct 2025 00:03:10 -0400 Subject: [PATCH 04/17] Reset feature flags back to their original status --- docker/pythonpath_dev/superset_config.py | 4 +--- superset/config.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py index 6ac050a36603..d88d9899c27a 100644 --- a/docker/pythonpath_dev/superset_config.py +++ b/docker/pythonpath_dev/superset_config.py @@ -105,9 +105,7 @@ class CeleryConfig: CELERY_CONFIG = CeleryConfig -FEATURE_FLAGS = { - "ALERT_REPORTS": False -} # Temporarily disabled for marshmallow 4.x compatibility +FEATURE_FLAGS = {"ALERT_REPORTS": True} ALERT_REPORTS_NOTIFICATION_DRY_RUN = True WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501 # The base URL for the email report hyperlinks. diff --git a/superset/config.py b/superset/config.py index 482d1b66c30e..662e79576e10 100644 --- a/superset/config.py +++ b/superset/config.py @@ -86,7 +86,7 @@ # https://github.com/apache/superset/blob/master/superset/utils/log.py EVENT_LOGGER = DBEventLogger() -SUPERSET_LOG_VIEW = False # Temporarily disabled for marshmallow 4.x compatibility +SUPERSET_LOG_VIEW = True # This config is used to enable/disable the folowing security menu items: # List Users, List Roles, List Groups @@ -524,7 +524,7 @@ class D3TimeFormat(TypedDict, total=False): # It is dependent on ENABLE_DASHBOARD_SCREENSHOT_ENDPOINT being enabled. "ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT": False, "TAGGING_SYSTEM": False, - "SQLLAB_BACKEND_PERSISTENCE": False, # Temporarily disabled for marshmallow 4.x compatibility + "SQLLAB_BACKEND_PERSISTENCE": True, "LISTVIEWS_DEFAULT_CARD_VIEW": False, # When True, this escapes HTML (rather than rendering it) in Markdown components "ESCAPE_MARKDOWN_HTML": False, @@ -1386,7 +1386,7 @@ def allowed_schemas_for_csv_upload( # pylint: disable=unused-argument SILENCE_FAB = True FAB_ADD_SECURITY_VIEWS = True -FAB_ADD_SECURITY_API = False # Temporarily disabled for marshmallow 4.x compatibility +FAB_ADD_SECURITY_API = True FAB_ADD_SECURITY_PERMISSION_VIEW = False FAB_ADD_SECURITY_VIEW_MENU_VIEW = False FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW = False From bc755decb107437cb7e3c0219cdce710f21570a6 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 27 Oct 2025 00:26:11 -0400 Subject: [PATCH 05/17] Apply the compatibility patch to address incompatibilities between Flask-AppBuilder 5.0.0 and marshmallow 4.x --- docker/pythonpath_dev/superset_config.py | 8 +++ superset/marshmallow_fix.py | 68 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 superset/marshmallow_fix.py diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py index d88d9899c27a..0b59968a5c78 100644 --- a/docker/pythonpath_dev/superset_config.py +++ b/docker/pythonpath_dev/superset_config.py @@ -105,6 +105,14 @@ class CeleryConfig: CELERY_CONFIG = CeleryConfig +# Apply marshmallow 4.x compatibility fix for Flask-AppBuilder +try: + from superset.marshmallow_fix import patch_marshmallow_for_flask_appbuilder + + patch_marshmallow_for_flask_appbuilder() +except ImportError: + pass # If the fix module doesn't exist, continue without it + FEATURE_FLAGS = {"ALERT_REPORTS": True} ALERT_REPORTS_NOTIFICATION_DRY_RUN = True WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501 diff --git a/superset/marshmallow_fix.py b/superset/marshmallow_fix.py new file mode 100644 index 000000000000..99a6da3716c5 --- /dev/null +++ b/superset/marshmallow_fix.py @@ -0,0 +1,68 @@ +""" +Marshmallow 4.x Compatibility Fix for Flask-AppBuilder 5.0.0 + +This module provides a targeted fix for incompatibilities between +Flask-AppBuilder 5.0.0 and marshmallow 4.x, specifically handling +missing auto-generated fields during schema initialization. +""" + +from marshmallow import fields + + +def patch_marshmallow_for_flask_appbuilder(): + """ + Patches marshmallow Schema._init_fields to handle Flask-AppBuilder 5.0.0 + compatibility with marshmallow 4.x. + + Flask-AppBuilder 5.0.0 automatically generates schema fields that reference + SQL relationship fields that may not exist in marshmallow 4.x's stricter + field validation. This patch dynamically adds missing fields as Raw fields + to prevent KeyError exceptions during schema initialization. + """ + import marshmallow + + # Store the original method + original_init_fields = marshmallow.Schema._init_fields + + def patched_init_fields(self): + """Patched version that handles missing declared fields.""" + max_retries = 10 # Prevent infinite loops in case of unexpected errors + retries = 0 + + while retries < max_retries: + try: + return original_init_fields(self) + except KeyError as e: + # Extract the missing field name from the KeyError + missing_field = str(e).strip("'\"") + + # Initialize declared_fields if it doesn't exist + if not hasattr(self, "declared_fields"): + self.declared_fields = {} + + # Only add if it doesn't already exist + if missing_field not in self.declared_fields: + # Use Raw field as a safe fallback for unknown auto-generated fields + self.declared_fields[missing_field] = fields.Raw( + allow_none=True, + dump_only=True, # Prevent validation issues during serialization + ) + + print( + f"Marshmallow compatibility: Added missing field " + f"'{missing_field}' as Raw field" + ) + + retries += 1 + # Continue the loop to retry initialization + except Exception: + # For any other type of error, just propagate it + raise + + # If we've exhausted retries, something is seriously wrong + raise RuntimeError( + f"Marshmallow field initialization failed after {max_retries} retries" + ) + + # Apply the patch + marshmallow.Schema._init_fields = patched_init_fields From bf9fc91611df35d523bd3b4a24898c84586b4d18 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 27 Oct 2025 00:41:51 -0400 Subject: [PATCH 06/17] Revert initialization\__init__.py to its initial state --- superset/initialization/__init__.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 85f062a2893b..3973508122a9 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -178,6 +178,9 @@ def init_views(self) -> None: from superset.extensions.view import ExtensionsView from superset.importexport.api import ImportExportRestApi from superset.queries.api import QueryRestApi + from superset.queries.saved_queries.api import SavedQueryRestApi + from superset.reports.api import ReportScheduleRestApi + from superset.reports.logs.api import ReportExecutionLogRestApi from superset.row_level_security.api import RLSRestApi from superset.security.api import ( RoleRestAPI, @@ -205,6 +208,7 @@ def init_views(self) -> None: from superset.views.error_handling import set_app_error_handlers from superset.views.explore import ExplorePermalinkView, ExploreView from superset.views.groups import GroupsListView + from superset.views.log.api import LogRestApi from superset.views.logs import ActionLogView from superset.views.roles import RolesListView from superset.views.sql_lab.views import ( @@ -261,22 +265,14 @@ def init_views(self) -> None: appbuilder.add_api(ExplorePermalinkRestApi) appbuilder.add_api(ImportExportRestApi) appbuilder.add_api(QueryRestApi) + appbuilder.add_api(ReportScheduleRestApi) + appbuilder.add_api(ReportExecutionLogRestApi) appbuilder.add_api(RLSRestApi) + appbuilder.add_api(SavedQueryRestApi) appbuilder.add_api(TagRestApi) appbuilder.add_api(SqlLabRestApi) appbuilder.add_api(SqlLabPermalinkRestApi) - - if feature_flag_manager.is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"): - from superset.queries.saved_queries.api import SavedQueryRestApi - - appbuilder.add_api(SavedQueryRestApi) - - if feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): - from superset.reports.api import ReportScheduleRestApi - from superset.reports.logs.api import ReportExecutionLogRestApi - - appbuilder.add_api(ReportScheduleRestApi) - appbuilder.add_api(ReportExecutionLogRestApi) + appbuilder.add_api(LogRestApi) if feature_flag_manager.is_feature_enabled("ENABLE_EXTENSIONS"): from superset.extensions.api import ExtensionsRestApi @@ -476,6 +472,7 @@ def init_views(self) -> None: category="Manage", menu_cond=lambda: feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"), ) + appbuilder.add_api(LogRestApi) appbuilder.add_api(UserRegistrationsRestAPI) appbuilder.add_view( ActionLogView, @@ -489,12 +486,6 @@ def init_views(self) -> None: and self.config["SUPERSET_LOG_VIEW"] ), ) - - if self.config["FAB_ADD_SECURITY_VIEWS"] and self.config["SUPERSET_LOG_VIEW"]: - from superset.views.log.api import LogRestApi - - appbuilder.add_api(LogRestApi) - appbuilder.add_api(SecurityRestApi) # # Conditionally setup email views @@ -953,4 +944,4 @@ def patch_flask_locale(self, locale: str) -> FlaskResponse: if redirect_to := request.headers.get("Referer"): return redirect(get_safe_redirect(redirect_to)) - return redirect(self.get_redirect()) + return redirect(self.get_redirect()) \ No newline at end of file From dd9186a714e1e746fcfd78707a4ca7df7d18a760 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 27 Oct 2025 00:44:38 -0400 Subject: [PATCH 07/17] Fix whitespace in superset\initialization\__init__.py --- superset/initialization/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 3973508122a9..133456f35a6d 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -944,4 +944,4 @@ def patch_flask_locale(self, locale: str) -> FlaskResponse: if redirect_to := request.headers.get("Referer"): return redirect(get_safe_redirect(redirect_to)) - return redirect(self.get_redirect()) \ No newline at end of file + return redirect(self.get_redirect()) From 2a4339a77e15d43bc58d323e2a79dbc943bca1ec Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 27 Oct 2025 22:00:44 -0400 Subject: [PATCH 08/17] Add unit tests --- tests/unit_tests/test_marshmallow_fix.py | 197 +++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 tests/unit_tests/test_marshmallow_fix.py diff --git a/tests/unit_tests/test_marshmallow_fix.py b/tests/unit_tests/test_marshmallow_fix.py new file mode 100644 index 000000000000..27505852b1a3 --- /dev/null +++ b/tests/unit_tests/test_marshmallow_fix.py @@ -0,0 +1,197 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Unit tests for marshmallow 4.x compatibility fix. + +This module tests the marshmallow_fix.py module that provides compatibility +between Flask-AppBuilder 5.0.0 and marshmallow 4.x by handling missing +auto-generated fields during schema initialization. +""" + +from unittest.mock import patch + +import pytest +from marshmallow import Schema, fields + +from superset.marshmallow_fix import patch_marshmallow_for_flask_appbuilder + + +class TestMarshmallowFix: + """Test cases for the marshmallow 4.x compatibility fix.""" + + def test_patch_marshmallow_for_flask_appbuilder_applies_patch(self): + """Test that the patch function correctly replaces Schema._init_fields.""" + # Store original method + original_method = Schema._init_fields + + # Apply patch + patch_marshmallow_for_flask_appbuilder() + + # Verify the method was replaced + assert Schema._init_fields != original_method + assert callable(Schema._init_fields) + + # Restore original for other tests + Schema._init_fields = original_method + + def test_patch_functionality_with_real_schema_creation(self): + """Test that the patch works with actual schema creation scenarios.""" + # Store original method + original_method = Schema._init_fields + + try: + # Apply the patch + patch_marshmallow_for_flask_appbuilder() + + # Create a simple schema - this should work without errors + class TestSchema(Schema): + name = fields.Str() + age = fields.Int() + + # Schema creation should succeed + schema = TestSchema() + assert "name" in schema.declared_fields + assert "age" in schema.declared_fields + assert isinstance(schema.declared_fields["name"], fields.Str) + assert isinstance(schema.declared_fields["age"], fields.Int) + + finally: + # Restore original method + Schema._init_fields = original_method + + def test_patch_handles_schema_with_no_fields(self): + """Test that the patch works with schemas that have no declared fields.""" + # Store original method + original_method = Schema._init_fields + + try: + # Apply the patch + patch_marshmallow_for_flask_appbuilder() + + # Create an empty schema + class EmptySchema(Schema): + pass + + # Schema creation should succeed + schema = EmptySchema() + # Should have at least a declared_fields attribute + assert hasattr(schema, "declared_fields") + + finally: + # Restore original method + Schema._init_fields = original_method + + def test_raw_field_creation_and_configuration(self): + """Test that Raw fields can be created with the expected configuration.""" + # Test creating a Raw field with our configuration + raw_field = fields.Raw(allow_none=True, dump_only=True) + + assert isinstance(raw_field, fields.Raw) + assert raw_field.allow_none is True + assert raw_field.dump_only is True + + @patch("builtins.print") + def test_print_function_can_be_mocked(self, mock_print): + """Test that print function can be mocked (for testing log output).""" + test_message = ( + "Marshmallow compatibility: Added missing field 'test' as Raw field" + ) + print(test_message) + mock_print.assert_called_once_with(test_message) + + def test_keyerror_exception_handling(self): + """Test that KeyError exceptions can be caught and handled.""" + try: + raise KeyError("test_field") + except KeyError as e: + # Verify we can extract the field name + field_name = str(e).strip("'\"") + assert field_name == "test_field" + + def test_schema_declared_fields_manipulation(self): + """Test that we can manipulate schema declared_fields.""" + + class TestSchema(Schema): + existing_field = fields.Str() + + schema = TestSchema() + + # Verify initial state + assert "existing_field" in schema.declared_fields + assert isinstance(schema.declared_fields["existing_field"], fields.Str) + + # Test adding a new field + schema.declared_fields["new_field"] = fields.Raw( + allow_none=True, dump_only=True + ) + + # Verify the new field was added + assert "new_field" in schema.declared_fields + assert isinstance(schema.declared_fields["new_field"], fields.Raw) + assert schema.declared_fields["new_field"].allow_none is True + assert schema.declared_fields["new_field"].dump_only is True + + def test_flask_appbuilder_field_names_list(self): + """Test that we have the correct list of Flask-AppBuilder field names.""" + # Common Flask-AppBuilder auto-generated field names that our fix handles + expected_fab_fields = [ + "permission_id", + "view_menu_id", + "db_id", + "chart_id", + "dashboard_id", + "user_id", + ] + + # Verify these are strings (field names) + for field_name in expected_fab_fields: + assert isinstance(field_name, str) + assert len(field_name) > 0 + assert "_id" in field_name + + def test_patch_function_is_callable(self): + """Test that the patch function can be called without errors.""" + # This should not raise any exceptions + patch_marshmallow_for_flask_appbuilder() + + # Calling it multiple times should also be safe + patch_marshmallow_for_flask_appbuilder() + patch_marshmallow_for_flask_appbuilder() + + def test_marshmallow_schema_basic_functionality(self): + """Test basic marshmallow schema functionality still works.""" + + class UserSchema(Schema): + name = fields.Str(required=True) + email = fields.Email() + age = fields.Int(validate=lambda x: x > 0) + + schema = UserSchema() + + # Test serialization + data = {"name": "John Doe", "email": "john@example.com", "age": 30} + result = schema.load(data) + assert result["name"] == "John Doe" + assert result["email"] == "john@example.com" + assert result["age"] == 30 + + from marshmallow import ValidationError + + # Test validation - missing required field should raise error + with pytest.raises(ValidationError): + schema.load({"email": "john@example.com", "age": 30}) # Missing name From 72fd0b61c9d52d04b00652a4b2c50a207b9001f6 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 27 Oct 2025 22:11:08 -0400 Subject: [PATCH 09/17] Rename "marshmallow_fix" to "marshmallow_compatibility" --- docker/pythonpath_dev/superset_config.py | 4 +++- ...marshmallow_fix.py => marshmallow_compatibility.py} | 8 ++++---- ...mallow_fix.py => test_marshmallow_compatibility.py} | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) rename superset/{marshmallow_fix.py => marshmallow_compatibility.py} (90%) rename tests/unit_tests/{test_marshmallow_fix.py => test_marshmallow_compatibility.py} (95%) diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py index 0b59968a5c78..6eadd8e16c52 100644 --- a/docker/pythonpath_dev/superset_config.py +++ b/docker/pythonpath_dev/superset_config.py @@ -107,7 +107,9 @@ class CeleryConfig: # Apply marshmallow 4.x compatibility fix for Flask-AppBuilder try: - from superset.marshmallow_fix import patch_marshmallow_for_flask_appbuilder + from superset.marshmallow_compatibility import ( + patch_marshmallow_for_flask_appbuilder, + ) patch_marshmallow_for_flask_appbuilder() except ImportError: diff --git a/superset/marshmallow_fix.py b/superset/marshmallow_compatibility.py similarity index 90% rename from superset/marshmallow_fix.py rename to superset/marshmallow_compatibility.py index 99a6da3716c5..23d99df8e262 100644 --- a/superset/marshmallow_fix.py +++ b/superset/marshmallow_compatibility.py @@ -1,9 +1,9 @@ """ -Marshmallow 4.x Compatibility Fix for Flask-AppBuilder 5.0.0 +Marshmallow 4.x Compatibility Module for Flask-AppBuilder 5.0.0 -This module provides a targeted fix for incompatibilities between -Flask-AppBuilder 5.0.0 and marshmallow 4.x, specifically handling -missing auto-generated fields during schema initialization. +This module provides compatibility between Flask-AppBuilder 5.0.0 and +marshmallow 4.x, specifically handling missing auto-generated fields +during schema initialization. """ from marshmallow import fields diff --git a/tests/unit_tests/test_marshmallow_fix.py b/tests/unit_tests/test_marshmallow_compatibility.py similarity index 95% rename from tests/unit_tests/test_marshmallow_fix.py rename to tests/unit_tests/test_marshmallow_compatibility.py index 27505852b1a3..57f2170f7f6d 100644 --- a/tests/unit_tests/test_marshmallow_fix.py +++ b/tests/unit_tests/test_marshmallow_compatibility.py @@ -16,9 +16,9 @@ # under the License. """ -Unit tests for marshmallow 4.x compatibility fix. +Unit tests for marshmallow 4.x compatibility module. -This module tests the marshmallow_fix.py module that provides compatibility +This module tests the marshmallow_compatibility.py module that provides compatibility between Flask-AppBuilder 5.0.0 and marshmallow 4.x by handling missing auto-generated fields during schema initialization. """ @@ -28,11 +28,11 @@ import pytest from marshmallow import Schema, fields -from superset.marshmallow_fix import patch_marshmallow_for_flask_appbuilder +from superset.marshmallow_compatibility import patch_marshmallow_for_flask_appbuilder -class TestMarshmallowFix: - """Test cases for the marshmallow 4.x compatibility fix.""" +class TestMarshmallowCompatibility: + """Test cases for the marshmallow 4.x compatibility module.""" def test_patch_marshmallow_for_flask_appbuilder_applies_patch(self): """Test that the patch function correctly replaces Schema._init_fields.""" From ca17fbbe2d0263277f47726be3633870a97a8598 Mon Sep 17 00:00:00 2001 From: Your Full Name Date: Sun, 2 Nov 2025 17:52:18 -0500 Subject: [PATCH 10/17] Move patch fn to app.py --- docker/pythonpath_dev/superset_config.py | 10 ---------- superset/app.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py index 6eadd8e16c52..d88d9899c27a 100644 --- a/docker/pythonpath_dev/superset_config.py +++ b/docker/pythonpath_dev/superset_config.py @@ -105,16 +105,6 @@ class CeleryConfig: CELERY_CONFIG = CeleryConfig -# Apply marshmallow 4.x compatibility fix for Flask-AppBuilder -try: - from superset.marshmallow_compatibility import ( - patch_marshmallow_for_flask_appbuilder, - ) - - patch_marshmallow_for_flask_appbuilder() -except ImportError: - pass # If the fix module doesn't exist, continue without it - FEATURE_FLAGS = {"ALERT_REPORTS": True} ALERT_REPORTS_NOTIFICATION_DRY_RUN = True WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501 diff --git a/superset/app.py b/superset/app.py index 54f1b79baea5..1011082f05dc 100644 --- a/superset/app.py +++ b/superset/app.py @@ -43,6 +43,16 @@ logger = logging.getLogger(__name__) +# Apply marshmallow 4.x compatibility patch for Flask-AppBuilder +try: + from superset.marshmallow_compatibility import ( + patch_marshmallow_for_flask_appbuilder, + ) + + patch_marshmallow_for_flask_appbuilder() +except ImportError: + logger.debug("marshmallow_compatibility module not found, skipping patch") + def create_app( superset_config_module: Optional[str] = None, From c0fe5d4263f05138a2494c104cc5acaaf32e59d3 Mon Sep 17 00:00:00 2001 From: Your Full Name Date: Mon, 3 Nov 2025 09:04:07 -0500 Subject: [PATCH 11/17] empty From fd7d995ffcab71f04b33c99d425c02f44d3f9b82 Mon Sep 17 00:00:00 2001 From: Your Full Name Date: Wed, 12 Nov 2025 19:09:59 -0500 Subject: [PATCH 12/17] Fix validates decorator --- superset/databases/schemas.py | 2 +- superset/models/helpers.py | 1 + superset/reports/schemas.py | 2 ++ superset/themes/schemas.py | 10 +++++----- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index ea24ba219b63..ebeef93f2754 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -1085,7 +1085,7 @@ def _deserialize( class BaseUploadFilePostSchemaMixin(Schema): @validates("file") - def validate_file_extension(self, file: FileStorage) -> None: + def validate_file_extension(self, file: FileStorage, **kwargs: Any) -> None: allowed_extensions = current_app.config["ALLOWED_EXTENSIONS"] file_suffix = Path(file.filename).suffix if not file_suffix: diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 6e6ef22bde93..05b9218a28d8 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -648,6 +648,7 @@ def ensure_extra_json_is_not_none( self, _: str, value: Optional[dict[str, Any]], + **kwargs: Any, ) -> Any: if value is None: return "{}" diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py index 305ccd9a04e3..d4a89b96e5c7 100644 --- a/superset/reports/schemas.py +++ b/superset/reports/schemas.py @@ -247,6 +247,7 @@ class ReportSchedulePostSchema(Schema): def validate_custom_width( self, value: Optional[int], + **kwargs: Any, ) -> None: if value is None: return @@ -385,6 +386,7 @@ class ReportSchedulePutSchema(Schema): def validate_custom_width( self, value: Optional[int], + **kwargs: Any, ) -> None: if value is None: return diff --git a/superset/themes/schemas.py b/superset/themes/schemas.py index 8ce5551eee04..de53b69998f3 100644 --- a/superset/themes/schemas.py +++ b/superset/themes/schemas.py @@ -35,7 +35,7 @@ class ImportV1ThemeSchema(Schema): version = fields.String(required=True) @validates("json_data") - def validate_json_data(self, value: dict[str, Any]) -> None: + def validate_json_data(self, value: dict[str, Any], **kwargs: Any) -> None: # Convert dict to JSON string for validation if isinstance(value, dict): json_str = json.dumps(value) @@ -70,12 +70,12 @@ class ThemePostSchema(Schema): json_data = fields.String(required=True, allow_none=False) @validates("theme_name") - def validate_theme_name(self, value: str) -> None: + def validate_theme_name(self, value: str, **kwargs: Any) -> None: if not value or not value.strip(): raise ValidationError("Theme name cannot be empty.") @validates("json_data") - def validate_and_sanitize_json_data(self, value: str) -> None: + def validate_and_sanitize_json_data(self, value: str, **kwargs: Any) -> None: # Parse JSON try: theme_config = json.loads(value) if isinstance(value, str) else value @@ -101,12 +101,12 @@ class ThemePutSchema(Schema): json_data = fields.String(required=True, allow_none=False) @validates("theme_name") - def validate_theme_name(self, value: str) -> None: + def validate_theme_name(self, value: str, **kwargs: Any) -> None: if not value or not value.strip(): raise ValidationError("Theme name cannot be empty.") @validates("json_data") - def validate_and_sanitize_json_data(self, value: str) -> None: + def validate_and_sanitize_json_data(self, value: str, **kwargs: Any) -> None: # Parse JSON try: theme_config = json.loads(value) if isinstance(value, str) else value From 84cddb38a1401c3a395783810976cda039f5e722 Mon Sep 17 00:00:00 2001 From: Eyang0612 Date: Sat, 22 Nov 2025 22:06:45 -0500 Subject: [PATCH 13/17] Apply JSON Schema Change --- superset/advanced_data_type/schemas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/advanced_data_type/schemas.py b/superset/advanced_data_type/schemas.py index 47918e5416f0..d94fd19873b5 100644 --- a/superset/advanced_data_type/schemas.py +++ b/superset/advanced_data_type/schemas.py @@ -23,10 +23,10 @@ advanced_data_type_convert_schema = { "type": "object", "properties": { - "type": {"type": "string", "dump_default": "port"}, + "type": {"type": "string", "default": "port"}, "values": { "type": "array", - "items": {"dump_default": "http"}, + "items": {"default": "http"}, "minItems": 1, }, }, From 3fcc002897cb51d27119fc20d0f2b6c82097f9ff Mon Sep 17 00:00:00 2001 From: Eyang0612 Date: Sat, 22 Nov 2025 23:40:47 -0500 Subject: [PATCH 14/17] Style: Add Typing + Remove Redundancy in Code Logic --- superset/marshmallow_compatibility.py | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/superset/marshmallow_compatibility.py b/superset/marshmallow_compatibility.py index 23d99df8e262..8a3b7e65aea8 100644 --- a/superset/marshmallow_compatibility.py +++ b/superset/marshmallow_compatibility.py @@ -1,3 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. """ Marshmallow 4.x Compatibility Module for Flask-AppBuilder 5.0.0 @@ -5,11 +21,14 @@ marshmallow 4.x, specifically handling missing auto-generated fields during schema initialization. """ +import logging +from typing import Any from marshmallow import fields +logger = logging.getLogger(__name__) -def patch_marshmallow_for_flask_appbuilder(): +def patch_marshmallow_for_flask_appbuilder() -> None: """ Patches marshmallow Schema._init_fields to handle Flask-AppBuilder 5.0.0 compatibility with marshmallow 4.x. @@ -24,7 +43,7 @@ def patch_marshmallow_for_flask_appbuilder(): # Store the original method original_init_fields = marshmallow.Schema._init_fields - def patched_init_fields(self): + def patched_init_fields(self) -> Any: """Patched version that handles missing declared fields.""" max_retries = 10 # Prevent infinite loops in case of unexpected errors retries = 0 @@ -45,19 +64,16 @@ def patched_init_fields(self): # Use Raw field as a safe fallback for unknown auto-generated fields self.declared_fields[missing_field] = fields.Raw( allow_none=True, - dump_only=True, # Prevent validation issues during serialization + dump_only=True, # Prevent validation issues on serialization ) - print( + logger.debug( f"Marshmallow compatibility: Added missing field " f"'{missing_field}' as Raw field" ) retries += 1 # Continue the loop to retry initialization - except Exception: - # For any other type of error, just propagate it - raise # If we've exhausted retries, something is seriously wrong raise RuntimeError( From e586e1621d79617320417093232cbcea65c8d70b Mon Sep 17 00:00:00 2001 From: Your Full Name Date: Sun, 23 Nov 2025 19:21:57 -0500 Subject: [PATCH 15/17] Allow optional fields to load as None --- superset/marshmallow_compatibility.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/superset/marshmallow_compatibility.py b/superset/marshmallow_compatibility.py index 8a3b7e65aea8..2a556bf9d1a2 100644 --- a/superset/marshmallow_compatibility.py +++ b/superset/marshmallow_compatibility.py @@ -62,9 +62,10 @@ def patched_init_fields(self) -> Any: # Only add if it doesn't already exist if missing_field not in self.declared_fields: # Use Raw field as a safe fallback for unknown auto-generated fields + # Allow both load and dump to support both input validation and serialization self.declared_fields[missing_field] = fields.Raw( allow_none=True, - dump_only=True, # Prevent validation issues on serialization + load_default=None, # Default to None if not provided (optional field) ) logger.debug( From 4fe8938f104e966169740b5478ca2db310026755 Mon Sep 17 00:00:00 2001 From: FLAME Date: Mon, 24 Nov 2025 02:05:46 -0500 Subject: [PATCH 16/17] Apply some formatting changes (e.g. make all lines <= 88 chars long, etc.) --- superset/marshmallow_compatibility.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/superset/marshmallow_compatibility.py b/superset/marshmallow_compatibility.py index 2a556bf9d1a2..434d35fe9983 100644 --- a/superset/marshmallow_compatibility.py +++ b/superset/marshmallow_compatibility.py @@ -61,16 +61,18 @@ def patched_init_fields(self) -> Any: # Only add if it doesn't already exist if missing_field not in self.declared_fields: - # Use Raw field as a safe fallback for unknown auto-generated fields - # Allow both load and dump to support both input validation and serialization + # Use Raw field as a safe fallback for unknown auto-generated + # fields. Allow both load and dump to support both input + # validation and serialization self.declared_fields[missing_field] = fields.Raw( allow_none=True, - load_default=None, # Default to None if not provided (optional field) + load_default=None, # Optional field (defaults to None) ) logger.debug( - f"Marshmallow compatibility: Added missing field " - f"'{missing_field}' as Raw field" + "Marshmallow compatibility: Added missing field " + "'%s' as Raw field", + missing_field, ) retries += 1 From 2e6a6d6315b76da829a5772d6002c7a521637882 Mon Sep 17 00:00:00 2001 From: Your Full Name Date: Fri, 28 Nov 2025 16:40:24 -0500 Subject: [PATCH 17/17] Fix ruff and mympy checks --- superset/marshmallow_compatibility.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/superset/marshmallow_compatibility.py b/superset/marshmallow_compatibility.py index 434d35fe9983..06f9967da4e6 100644 --- a/superset/marshmallow_compatibility.py +++ b/superset/marshmallow_compatibility.py @@ -21,13 +21,18 @@ marshmallow 4.x, specifically handling missing auto-generated fields during schema initialization. """ + import logging -from typing import Any +from typing import Any, TYPE_CHECKING from marshmallow import fields +if TYPE_CHECKING: + import marshmallow + logger = logging.getLogger(__name__) + def patch_marshmallow_for_flask_appbuilder() -> None: """ Patches marshmallow Schema._init_fields to handle Flask-AppBuilder 5.0.0 @@ -43,7 +48,7 @@ def patch_marshmallow_for_flask_appbuilder() -> None: # Store the original method original_init_fields = marshmallow.Schema._init_fields - def patched_init_fields(self) -> Any: + def patched_init_fields(self: "marshmallow.Schema") -> Any: """Patched version that handles missing declared fields.""" max_retries = 10 # Prevent infinite loops in case of unexpected errors retries = 0