mirror of
https://github.com/apache/superset.git
synced 2026-07-09 16:25:36 +00:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Evan <evan@preset.io> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
165 lines
6.2 KiB
Python
165 lines
6.2 KiB
Python
# 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.
|
|
"""Regression tests for marshmallow 4.x compatibility.
|
|
|
|
Marshmallow 4.x requires **kwargs on @validates-decorated methods and
|
|
ships with marshmallow-sqlalchemy >= 1.5.0. These tests confirm that
|
|
the codebase handles those requirements correctly.
|
|
"""
|
|
# pylint: disable=import-outside-toplevel
|
|
|
|
from importlib.metadata import version
|
|
from unittest.mock import MagicMock
|
|
|
|
import marshmallow
|
|
import pytest
|
|
from marshmallow import ValidationError
|
|
|
|
|
|
def test_validates_kwargs_database_schema() -> None:
|
|
"""Regression: marshmallow 4.x requires **kwargs on @validates methods.
|
|
|
|
Without **kwargs on validate_file_extension, marshmallow 4.x passes extra
|
|
keyword arguments to the validator and raises TypeError instead of
|
|
ValidationError when validating an uploaded file with a bad extension.
|
|
"""
|
|
from flask import Flask
|
|
|
|
from superset.databases.schemas import BaseUploadFilePostSchemaMixin
|
|
|
|
app = Flask(__name__)
|
|
app.config["ALLOWED_EXTENSIONS"] = {"csv", "json", "parquet", "zip"}
|
|
|
|
# Build a minimal schema that inherits the mixin so @validates fires
|
|
class TestUploadSchema(BaseUploadFilePostSchemaMixin):
|
|
file = marshmallow.fields.Raw()
|
|
|
|
schema = TestUploadSchema()
|
|
|
|
# Construct a mock FileStorage with a disallowed extension
|
|
mock_file = MagicMock()
|
|
mock_file.filename = "malware.exe"
|
|
|
|
with app.app_context():
|
|
with pytest.raises(ValidationError):
|
|
# Must raise ValidationError, not TypeError
|
|
schema.load({"file": mock_file})
|
|
|
|
|
|
def test_patch_marshmallow_for_flask_appbuilder_idempotent() -> None:
|
|
"""Regression: patch_marshmallow_for_flask_appbuilder must be idempotent.
|
|
|
|
Calling the patch function more than once must not create nested wrappers.
|
|
The _fab_patched sentinel on the patched method guards against re-patching.
|
|
"""
|
|
import marshmallow as mm
|
|
|
|
from superset.marshmallow_compatibility import (
|
|
patch_marshmallow_for_flask_appbuilder,
|
|
)
|
|
|
|
# Capture the state of _init_fields before the first call
|
|
original_init_fields = mm.Schema._init_fields
|
|
|
|
patch_marshmallow_for_flask_appbuilder()
|
|
after_first = mm.Schema._init_fields
|
|
assert getattr(after_first, "_fab_patched", False), (
|
|
"Expected _init_fields to be marked _fab_patched after first call"
|
|
)
|
|
|
|
patch_marshmallow_for_flask_appbuilder()
|
|
after_second = mm.Schema._init_fields
|
|
assert after_second is after_first, (
|
|
"Expected _init_fields to be unchanged after second call (idempotent)"
|
|
)
|
|
|
|
# Restore original to avoid side-effects on other tests
|
|
mm.Schema._init_fields = original_init_fields
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
tuple(int(p) for p in version("marshmallow").split(".")[:1]) < (4,),
|
|
reason="FAB _init_fields KeyError only occurs on marshmallow >= 4",
|
|
)
|
|
def test_fab_field_stubbing_is_single_pass() -> None:
|
|
"""Regression: FAB compat must stub missing fields in one pass per instance.
|
|
|
|
Flask-AppBuilder mints a fresh schema class on every call and marshmallow
|
|
rebuilds ``declared_fields`` for every instance, so any approach that re-runs
|
|
``_init_fields`` on KeyError (a retry loop) re-pays its cost on every single
|
|
instantiation. Under coverage tracing that blew the unit-test job past its
|
|
timeout. The patch must instead stub the undeclared FAB names before the
|
|
original runs, so ``_init_fields`` executes exactly once per instance.
|
|
"""
|
|
import marshmallow as mm
|
|
|
|
from superset.marshmallow_compatibility import (
|
|
patch_marshmallow_for_flask_appbuilder,
|
|
)
|
|
|
|
# Count invocations of the underlying (pre-patch) _init_fields, then patch.
|
|
base_init_fields = mm.Schema._init_fields
|
|
calls = {"n": 0}
|
|
|
|
def counting(self: mm.Schema) -> object:
|
|
calls["n"] += 1
|
|
return base_init_fields(self)
|
|
|
|
mm.Schema._init_fields = counting
|
|
try:
|
|
patch_marshmallow_for_flask_appbuilder()
|
|
|
|
# FAB-style schema: Meta.fields references undeclared relationship names,
|
|
# which marshmallow 4.x rejects with KeyError.
|
|
class FabStyleSchema(mm.Schema):
|
|
a = mm.fields.String()
|
|
|
|
class Meta:
|
|
fields = ("a", "related_model", "owner", "parent_id")
|
|
|
|
calls["n"] = 0
|
|
for _ in range(50):
|
|
FabStyleSchema()
|
|
# Exactly one underlying _init_fields per instance — no retry blowup.
|
|
assert calls["n"] == 50, (
|
|
f"expected 50 _init_fields calls for 50 instances, got {calls['n']} "
|
|
"— single-pass stubbing regressed into a retry loop"
|
|
)
|
|
|
|
# The undeclared FAB names resolve to Raw stubs in the built field set.
|
|
instance = FabStyleSchema()
|
|
assert isinstance(instance.fields["related_model"], mm.fields.Raw)
|
|
finally:
|
|
mm.Schema._init_fields = base_init_fields
|
|
|
|
|
|
def test_marshmallow_sqlalchemy_version() -> None:
|
|
"""Regression: marshmallow-sqlalchemy >= 1.5.0 is required.
|
|
|
|
marshmallow 4.x needs marshmallow-sqlalchemy >= 1.4.x, but 1.4.1/1.4.2 carry
|
|
a memory regression (issue #665) that exhausts memory in the test suite; only
|
|
1.5.0 fixes it. This test ensures the installed version is recent enough.
|
|
"""
|
|
import marshmallow_sqlalchemy # noqa: F401 # import to confirm it's installed
|
|
|
|
installed = version("marshmallow-sqlalchemy")
|
|
parts = [int(x) for x in installed.split(".")[:3]]
|
|
minimum = [1, 5, 0]
|
|
assert parts >= minimum, (
|
|
f"marshmallow-sqlalchemy {installed} is too old; need >= 1.5.0"
|
|
)
|