chore(extensions): clean up backend entrypoints and file globs (#38360)

This commit is contained in:
Ville Brofeldt
2026-03-03 09:45:35 -08:00
committed by GitHub
parent 016417f793
commit c35bf344a9
11 changed files with 426 additions and 117 deletions

View File

@@ -91,7 +91,7 @@ The `README.md` file provides documentation and instructions for using the exten
## Extension Metadata
The `extension.json` file contains the metadata necessary for the host application to identify and load the extension. Backend contributions (entry points and files) are declared here. Frontend contributions are registered directly in code from `frontend/src/index.tsx`.
The `extension.json` file contains the metadata necessary for the host application to identify and load the extension. Extensions follow a **convention-over-configuration** approach where entry points and build configuration are determined by standardized file locations rather than explicit declarations.
```json
{
@@ -100,15 +100,36 @@ The `extension.json` file contains the metadata necessary for the host applicati
"displayName": "Dataset References",
"version": "1.0.0",
"license": "Apache-2.0",
"backend": {
"entryPoints": ["superset_extensions.dataset_references.entrypoint"],
"files": ["backend/src/superset_extensions/dataset_references/**/*.py"]
},
"permissions": []
}
```
The `backend` section specifies Python entry points to load eagerly when the extension starts, and glob patterns for source files to include in the bundle.
### Convention-Based Entry Points
Extensions use standardized entry point locations:
- **Backend**: `backend/src/superset_extensions/{publisher}/{name}/entrypoint.py`
- **Frontend**: `frontend/src/index.tsx`
### Build Configuration
Backend build configuration is specified in `backend/pyproject.toml`:
```toml
[project]
name = "my_org-dataset_references"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
# Files to include in the extension build/bundle
include = [
"src/superset_extensions/my_org/dataset_references/**/*.py",
]
exclude = []
```
The `include` patterns specify which files to bundle, while `exclude` patterns can filter out unwanted files (e.g., test files, cache directories).
## Interacting with the Host

View File

@@ -87,10 +87,6 @@ class BaseExtension(BaseModel):
class ExtensionConfigBackend(BaseModel):
"""Backend section in extension.json."""
entryPoints: list[str] = Field( # noqa: N815
default_factory=list,
description="Python module entry points to load",
)
files: list[str] = Field(
default_factory=list,
description="Glob patterns for backend Python files",
@@ -131,10 +127,7 @@ class ManifestFrontend(BaseModel):
class ManifestBackend(BaseModel):
"""Backend section in manifest.json."""
entryPoints: list[str] = Field( # noqa: N815
default_factory=list,
description="Python module entry points to load",
)
entrypoint: str
class Manifest(BaseExtension):

View File

@@ -162,8 +162,13 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
)
backend: ManifestBackend | None = None
if extension.backend and extension.backend.entryPoints:
backend = ManifestBackend(entryPoints=extension.backend.entryPoints)
backend_dir = cwd / "backend"
if backend_dir.exists():
# Generate conventional entry point
publisher_snake = kebab_to_snake_case(extension.publisher)
name_snake = kebab_to_snake_case(extension.name)
entrypoint = f"superset_extensions.{publisher_snake}.{name_snake}.entrypoint"
backend = ManifestBackend(entrypoint=entrypoint)
return Manifest(
id=composite_id,
@@ -217,17 +222,34 @@ def copy_frontend_dist(cwd: Path) -> str:
def copy_backend_files(cwd: Path) -> None:
"""Copy backend files based on pyproject.toml build configuration (validation already passed)."""
dist_dir = cwd / "dist"
extension = read_json(cwd / "extension.json")
if not extension:
click.secho("❌ No extension.json file found.", err=True, fg="red")
sys.exit(1)
backend_dir = cwd / "backend"
for pat in extension.get("backend", {}).get("files", []):
for f in cwd.glob(pat):
# Read build config from pyproject.toml
pyproject = read_toml(backend_dir / "pyproject.toml")
assert pyproject
build_config = (
pyproject.get("tool", {}).get("apache_superset_extensions", {}).get("build", {})
)
include_patterns = build_config.get("include", [])
exclude_patterns = build_config.get("exclude", [])
# Process include patterns
for pattern in include_patterns:
for f in backend_dir.glob(pattern):
if not f.is_file():
continue
tgt = dist_dir / f.relative_to(cwd)
# Check exclude patterns
relative_path = f.relative_to(backend_dir)
should_exclude = any(
relative_path.match(excl_pattern) for excl_pattern in exclude_patterns
)
if should_exclude:
continue
tgt = dist_dir / "backend" / relative_path
tgt.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(f, tgt)
@@ -272,6 +294,89 @@ def app() -> None:
def validate() -> None:
validate_npm()
cwd = Path.cwd()
# Validate extension.json exists and is valid
extension_data = read_json(cwd / "extension.json")
if not extension_data:
click.secho("❌ extension.json not found.", err=True, fg="red")
sys.exit(1)
try:
extension = ExtensionConfig.model_validate(extension_data)
except Exception as e:
click.secho(f"❌ Invalid extension.json: {e}", err=True, fg="red")
sys.exit(1)
# Validate conventional backend structure if backend directory exists
backend_dir = cwd / "backend"
if backend_dir.exists():
# Check for pyproject.toml
pyproject_path = backend_dir / "pyproject.toml"
if not pyproject_path.exists():
click.secho(
"❌ Backend directory exists but pyproject.toml not found",
err=True,
fg="red",
)
sys.exit(1)
# Validate pyproject.toml has build configuration
pyproject = read_toml(pyproject_path)
if not pyproject:
click.secho("❌ Failed to read backend pyproject.toml", err=True, fg="red")
sys.exit(1)
build_config = (
pyproject.get("tool", {})
.get("apache_superset_extensions", {})
.get("build", {})
)
if not build_config.get("include"):
click.secho(
"❌ Missing [tool.apache_superset_extensions.build] section with 'include' patterns in pyproject.toml",
err=True,
fg="red",
)
sys.exit(1)
# Check conventional backend entry point
publisher_snake = kebab_to_snake_case(extension.publisher)
name_snake = kebab_to_snake_case(extension.name)
expected_entry_file = (
backend_dir
/ "src"
/ "superset_extensions"
/ publisher_snake
/ name_snake
/ "entrypoint.py"
)
if not expected_entry_file.exists():
click.secho(
f"❌ Backend entry point not found at expected location: {expected_entry_file.relative_to(cwd)}",
err=True,
fg="red",
)
click.secho(
f" Convention requires: backend/src/superset_extensions/{publisher_snake}/{name_snake}/entrypoint.py",
fg="yellow",
)
sys.exit(1)
# Validate conventional frontend entry point if frontend directory exists
frontend_dir = cwd / "frontend"
if frontend_dir.exists():
expected_frontend_entry = frontend_dir / "src" / "index.tsx"
if not expected_frontend_entry.exists():
click.secho(
f"❌ Frontend entry point not found at expected location: {expected_frontend_entry.relative_to(cwd)}",
err=True,
fg="red",
)
click.secho(" Convention requires: frontend/src/index.tsx", fg="yellow")
sys.exit(1)
click.secho("✅ Validation successful", fg="green")

View File

@@ -2,3 +2,10 @@
name = "{{ backend_package }}"
version = "{{ version }}"
license = "{{ license }}"
[tool.apache_superset_extensions.build]
# Files to include in the extension build/bundle
include = [
"src/{{ backend_path|replace('.', '/') }}/**/*.py",
]
exclude = []

View File

@@ -4,11 +4,5 @@
"displayName": "{{ display_name }}",
"version": "{{ version }}",
"license": "{{ license }}",
{% if include_backend -%}
"backend": {
"entryPoints": ["{{ backend_entry }}"],
"files": ["backend/src/{{ backend_path|replace('.', '/') }}/**/*.py"]
},
{% endif -%}
"permissions": []
}

View File

@@ -46,10 +46,50 @@ def extension_with_build_structure():
frontend_dir = base_path / "frontend"
frontend_dir.mkdir()
# Create conventional frontend entry point
frontend_src_dir = frontend_dir / "src"
frontend_src_dir.mkdir()
(frontend_src_dir / "index.tsx").write_text("// Frontend entry point")
if include_backend:
backend_dir = base_path / "backend"
backend_dir.mkdir()
# Create conventional backend structure
backend_src_dir = (
backend_dir
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_extension"
)
backend_src_dir.mkdir(parents=True)
# Create conventional entry point file
(backend_src_dir / "entrypoint.py").write_text("# Backend entry point")
(backend_src_dir / "__init__.py").write_text("")
# Create parent __init__.py files for namespace packages
(backend_dir / "src" / "superset_extensions" / "__init__.py").write_text("")
(
backend_dir / "src" / "superset_extensions" / "test_org" / "__init__.py"
).write_text("")
# Create pyproject.toml matching the template structure
pyproject_content = """[project]
name = "test_org-test_extension"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
# Files to include in the extension build/bundle
include = [
"src/superset_extensions/test_org/test_extension/**/*.py",
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
# Create extension.json
extension_json = {
"publisher": "test-org",
@@ -59,13 +99,6 @@ def extension_with_build_structure():
"permissions": [],
}
if include_backend:
extension_json["backend"] = {
"entryPoints": [
"superset_extensions.test_org.test_extension.entrypoint"
]
}
(base_path / "extension.json").write_text(json.dumps(extension_json))
return {
@@ -96,7 +129,18 @@ def test_build_command_success_flow(
"""Test build command success flow."""
# Setup mocks
mock_rebuild_frontend.return_value = "remoteEntry.abc123.js"
mock_read_toml.return_value = {"project": {"name": "test"}}
mock_read_toml.return_value = {
"project": {"name": "test"},
"tool": {
"apache_superset_extensions": {
"build": {
"include": [
"src/superset_extensions/test_org/test_extension/**/*.py"
]
}
}
},
}
# Create extension structure
dirs = extension_with_build_structure(isolated_filesystem)
@@ -117,7 +161,9 @@ def test_build_command_success_flow(
@patch("superset_extensions_cli.cli.validate_npm")
@patch("superset_extensions_cli.cli.init_frontend_deps")
@patch("superset_extensions_cli.cli.rebuild_frontend")
@patch("superset_extensions_cli.cli.read_toml")
def test_build_command_handles_frontend_build_failure(
mock_read_toml,
mock_rebuild_frontend,
mock_init_frontend_deps,
mock_validate_npm,
@@ -128,6 +174,18 @@ def test_build_command_handles_frontend_build_failure(
"""Test build command handles frontend build failure."""
# Setup mocks
mock_rebuild_frontend.return_value = None # Indicates failure
mock_read_toml.return_value = {
"project": {"name": "test"},
"tool": {
"apache_superset_extensions": {
"build": {
"include": [
"src/superset_extensions/test_org/test_extension/**/*.py"
]
}
}
},
}
# Create extension structure
extension_with_build_structure(isolated_filesystem)
@@ -225,9 +283,16 @@ def test_init_frontend_deps_exits_on_npm_ci_failure(
# Build Manifest Tests
@pytest.mark.unit
def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
def test_build_manifest_creates_correct_manifest_structure(
isolated_filesystem, extension_with_build_structure
):
"""Test build_manifest creates correct manifest from extension.json."""
# Create extension.json
# Create extension structure with both frontend and backend
extension_with_build_structure(
isolated_filesystem, include_frontend=True, include_backend=True
)
# Update extension.json with additional fields
extension_data = {
"publisher": "test-org",
"name": "test-extension",
@@ -235,9 +300,6 @@ def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
"version": "1.0.0",
"permissions": ["read_data"],
"dependencies": ["some_dep"],
"backend": {
"entryPoints": ["superset_extensions.test_org.test_extension.entrypoint"]
},
}
extension_json = isolated_filesystem / "extension.json"
extension_json.write_text(json.dumps(extension_data))
@@ -258,11 +320,12 @@ def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
assert manifest.frontend.remoteEntry == "remoteEntry.abc123.js"
assert manifest.frontend.moduleFederationName == "testOrg_testExtension"
# Verify backend section
# Verify backend section and conventional entrypoint
assert manifest.backend is not None
assert manifest.backend.entryPoints == [
"superset_extensions.test_org.test_extension.entrypoint"
]
assert (
manifest.backend.entrypoint
== "superset_extensions.test_org.test_extension.entrypoint"
)
@pytest.mark.unit
@@ -413,7 +476,8 @@ def test_rebuild_backend_calls_copy_and_shows_message(isolated_filesystem):
def test_copy_backend_files_skips_non_files(isolated_filesystem):
"""Test copy_backend_files skips directories and non-files."""
# Create backend structure with directory
backend_src = isolated_filesystem / "backend" / "src" / "test_ext"
backend_dir = isolated_filesystem / "backend"
backend_src = backend_dir / "src" / "superset_extensions" / "test_org" / "test_ext"
backend_src.mkdir(parents=True)
(backend_src / "__init__.py").write_text("# init")
@@ -421,16 +485,27 @@ def test_copy_backend_files_skips_non_files(isolated_filesystem):
subdir = backend_src / "subdir"
subdir.mkdir()
# Create extension.json with backend file patterns
# Create pyproject.toml with build configuration
pyproject_content = """[project]
name = "test_org-test_ext"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
include = [
"src/superset_extensions/test_org/test_ext/**/*",
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
# Create extension.json
extension_data = {
"publisher": "test-org",
"name": "test-ext",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
"backend": {
"files": ["backend/src/test_ext/**/*"] # Will match both files and dirs
},
}
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
@@ -441,10 +516,26 @@ def test_copy_backend_files_skips_non_files(isolated_filesystem):
# Verify only files were copied, not directories
dist_dir = isolated_filesystem / "dist"
assert_file_exists(dist_dir / "backend" / "src" / "test_ext" / "__init__.py")
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "__init__.py"
)
# Directory should not be copied as a file
copied_subdir = dist_dir / "backend" / "src" / "test_ext" / "subdir"
copied_subdir = (
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "subdir"
)
# The directory might exist but should be empty since we skip non-files
if copied_subdir.exists():
assert list(copied_subdir.iterdir()) == []
@@ -452,21 +543,35 @@ def test_copy_backend_files_skips_non_files(isolated_filesystem):
@pytest.mark.unit
def test_copy_backend_files_copies_matched_files(isolated_filesystem):
"""Test copy_backend_files copies files matching patterns from extension.json."""
"""Test copy_backend_files copies files matching patterns from pyproject.toml."""
# Create backend source files
backend_src = isolated_filesystem / "backend" / "src" / "test_ext"
backend_dir = isolated_filesystem / "backend"
backend_src = backend_dir / "src" / "superset_extensions" / "test_org" / "test_ext"
backend_src.mkdir(parents=True)
(backend_src / "__init__.py").write_text("# init")
(backend_src / "main.py").write_text("# main")
# Create extension.json with backend file patterns
# Create pyproject.toml with build configuration
pyproject_content = """[project]
name = "test_org-test_ext"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
include = [
"src/superset_extensions/test_org/test_ext/**/*.py",
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
# Create extension.json
extension_data = {
"publisher": "test-org",
"name": "test-ext",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
"backend": {"files": ["backend/src/test_ext/**/*.py"]},
}
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
@@ -477,37 +582,117 @@ def test_copy_backend_files_copies_matched_files(isolated_filesystem):
# Verify files were copied
dist_dir = isolated_filesystem / "dist"
assert_file_exists(dist_dir / "backend" / "src" / "test_ext" / "__init__.py")
assert_file_exists(dist_dir / "backend" / "src" / "test_ext" / "main.py")
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "__init__.py"
)
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "main.py"
)
@pytest.mark.unit
def test_copy_backend_files_handles_no_backend_config(isolated_filesystem):
"""Test copy_backend_files handles extension.json without backend config."""
def test_copy_backend_files_handles_various_glob_patterns(isolated_filesystem):
"""Test copy_backend_files correctly handles different glob pattern formats."""
# Create backend structure with files in different locations
backend_dir = isolated_filesystem / "backend"
backend_src = backend_dir / "src" / "superset_extensions" / "test_org" / "test_ext"
backend_src.mkdir(parents=True)
# Create files that should match different pattern types
(backend_src / "__init__.py").write_text("# init")
(backend_src / "main.py").write_text("# main")
(backend_dir / "config.py").write_text("# config") # Root level file
# Create subdirectory with files
subdir = backend_src / "utils"
subdir.mkdir()
(subdir / "helper.py").write_text("# helper")
# Create pyproject.toml with various glob patterns that would fail with old logic
pyproject_content = """[project]
name = "test_org-test_ext"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
include = [
"config.py", # No '/' - would break old logic
"**/*.py", # Starts with '**' - would break old logic
"src/superset_extensions/test_org/test_ext/main.py", # Specific file
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
# Create extension.json
extension_data = {
"publisher": "frontend-org",
"name": "frontend-only",
"displayName": "Frontend Only Extension",
"publisher": "test-org",
"name": "test-ext",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
}
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
# Create dist directory
clean_dist(isolated_filesystem)
# Should not raise error
copy_backend_files(isolated_filesystem)
# Verify files were copied according to patterns
dist_dir = isolated_filesystem / "dist"
@pytest.mark.unit
def test_copy_backend_files_exits_when_extension_json_missing(isolated_filesystem):
"""Test copy_backend_files exits when extension.json is missing."""
clean_dist(isolated_filesystem)
# config.py (pattern: "config.py")
assert_file_exists(dist_dir / "backend" / "config.py")
with pytest.raises(SystemExit) as exc_info:
copy_backend_files(isolated_filesystem)
# All .py files should be included (pattern: "**/*.py")
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "__init__.py"
)
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "utils"
/ "helper.py"
)
assert exc_info.value.code == 1
# Specific file (pattern: "src/superset_extensions/test_org/test_ext/main.py")
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "superset_extensions"
/ "test_org"
/ "test_ext"
/ "main.py"
)
# Removed obsolete tests:
# - test_copy_backend_files_handles_no_backend_config: This scenario can't happen since copy_backend_files is only called when backend exists
# - test_copy_backend_files_exits_when_extension_json_missing: Validation catches this before copy_backend_files is called
# Frontend Dist Copy Tests

View File

@@ -226,17 +226,8 @@ def test_extension_json_content_is_correct(
# Verify frontend section is not present (contributions are code-first)
assert "frontend" not in content
# Verify backend section exists and has correct structure
assert "backend" in content
backend = content["backend"]
assert "entryPoints" in backend
assert "files" in backend
assert backend["entryPoints"] == [
"superset_extensions.test_org.test_extension.entrypoint"
]
assert backend["files"] == [
"backend/src/superset_extensions/test_org/test_extension/**/*.py"
]
# Verify no backend section in extension.json (moved to pyproject.toml)
assert "backend" not in content
@pytest.mark.cli

View File

@@ -25,8 +25,20 @@ from superset_extensions_cli.cli import app, validate_npm
# Validate Command Tests
@pytest.mark.cli
def test_validate_command_success(cli_runner):
def test_validate_command_success(cli_runner, isolated_filesystem):
"""Test validate command succeeds when npm is available and valid."""
# Create minimal extension.json for validation
extension_json = {
"publisher": "test-org",
"name": "test-extension",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
}
import json
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_json))
with patch("superset_extensions_cli.cli.validate_npm") as mock_validate:
result = cli_runner.invoke(app, ["validate"])

View File

@@ -81,15 +81,8 @@ def test_extension_json_template_renders_with_both_frontend_and_backend(
# Verify frontend section is not present (contributions are code-first)
assert "frontend" not in parsed
# Verify backend section exists
assert "backend" in parsed
backend = parsed["backend"]
assert backend["entryPoints"] == [
"superset_extensions.test_org.test_extension.entrypoint"
]
assert backend["files"] == [
"backend/src/superset_extensions/test_org/test_extension/**/*.py"
]
# Verify no backend section in extension.json (moved to pyproject.toml)
assert "backend" not in parsed
@pytest.mark.unit
@@ -97,7 +90,7 @@ def test_extension_json_template_renders_with_both_frontend_and_backend(
"include_frontend,include_backend,expected_sections",
[
(True, False, []),
(False, True, ["backend"]),
(False, True, []),
(False, False, []),
],
)
@@ -220,12 +213,7 @@ def test_template_rendering_with_different_ids(
assert parsed["publisher"] == publisher
assert parsed["name"] == technical_name
assert parsed["displayName"] == display_name
assert parsed["backend"]["entryPoints"] == [
f"superset_extensions.{publisher_snake}.{name_snake}.entrypoint"
]
assert parsed["backend"]["files"] == [
f"backend/src/superset_extensions/{publisher_snake}/{name_snake}/**/*.py"
]
assert "backend" not in parsed
# Test package.json template
template = jinja_env.get_template("frontend/package.json.j2")

View File

@@ -587,13 +587,12 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
backend = extension.manifest.backend
if backend and (entrypoints := backend.entryPoints):
for entrypoint in entrypoints:
try:
eager_import(entrypoint)
except Exception as ex: # pylint: disable=broad-except # noqa: S110
# Surface exceptions during initialization of extensions
print(ex)
if backend and backend.entrypoint:
try:
eager_import(backend.entrypoint)
except Exception as ex: # pylint: disable=broad-except # noqa: S110
# Surface exceptions during initialization of extensions
print(ex)
def init_app_in_ctx(self) -> None:
"""

View File

@@ -62,7 +62,6 @@ def test_extension_config_full():
"dependencies": ["other-extension"],
"permissions": ["can_read", "can_view"],
"backend": {
"entryPoints": ["query_insights.entrypoint"],
"files": ["backend/src/query_insights/**/*.py"],
},
}
@@ -76,7 +75,6 @@ def test_extension_config_full():
assert config.dependencies == ["other-extension"]
assert config.permissions == ["can_read", "can_view"]
assert config.backend is not None
assert config.backend.entryPoints == ["query_insights.entrypoint"]
assert config.backend.files == ["backend/src/query_insights/**/*.py"]
@@ -221,11 +219,16 @@ def test_manifest_with_backend():
"publisher": "my-org",
"name": "my-extension",
"displayName": "My Extension",
"backend": {"entryPoints": ["my_extension.entrypoint"]},
"backend": {
"entrypoint": "superset_extensions.my_org.my_extension.entrypoint"
},
}
)
assert manifest.backend is not None
assert manifest.backend.entryPoints == ["my_extension.entrypoint"]
assert (
manifest.backend.entrypoint
== "superset_extensions.my_org.my_extension.entrypoint"
)
def test_manifest_backend_no_files_field():
@@ -236,7 +239,9 @@ def test_manifest_backend_no_files_field():
"publisher": "my-org",
"name": "my-extension",
"displayName": "My Extension",
"backend": {"entryPoints": ["my_extension.entrypoint"]},
"backend": {
"entrypoint": "superset_extensions.my_org.my_extension.entrypoint"
},
}
)
# ManifestBackend should not have a 'files' field
@@ -246,11 +251,20 @@ def test_manifest_backend_no_files_field():
def test_extension_config_backend_defaults():
"""Test ExtensionConfigBackend has correct defaults."""
backend = ExtensionConfigBackend.model_validate({})
assert backend.entryPoints == []
assert backend.files == []
def test_manifest_backend_defaults():
"""Test ManifestBackend has correct defaults."""
backend = ManifestBackend.model_validate({})
assert backend.entryPoints == []
def test_manifest_backend_required_entrypoint():
"""Test ManifestBackend requires entrypoint field."""
# Test positive case - entrypoint provided
backend = ManifestBackend.model_validate(
{"entrypoint": "superset_extensions.test_org.test_extension.entrypoint"}
)
assert (
backend.entrypoint == "superset_extensions.test_org.test_extension.entrypoint"
)
# Test negative case - entrypoint missing should raise ValidationError
with pytest.raises(ValidationError) as exc_info:
ManifestBackend.model_validate({})
assert "entrypoint" in str(exc_info.value)