This is an automated email from the ASF dual-hosted git repository.
villebro 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 c35bf344a96 chore(extensions): clean up backend entrypoints and file
globs (#38360)
c35bf344a96 is described below
commit c35bf344a962c35fa3dfb8aefe0c2fc651295e80
Author: Ville Brofeldt <[email protected]>
AuthorDate: Tue Mar 3 09:45:35 2026 -0800
chore(extensions): clean up backend entrypoints and file globs (#38360)
---
docs/developer_docs/extensions/development.md | 33 ++-
.../src/superset_core/extensions/types.py | 9 +-
.../src/superset_extensions_cli/cli.py | 123 ++++++++-
.../templates/backend/pyproject.toml.j2 | 7 +
.../templates/extension.json.j2 | 6 -
superset-extensions-cli/tests/test_cli_build.py | 275 +++++++++++++++++----
superset-extensions-cli/tests/test_cli_init.py | 13 +-
superset-extensions-cli/tests/test_cli_validate.py | 14 +-
superset-extensions-cli/tests/test_templates.py | 20 +-
superset/initialization/__init__.py | 13 +-
tests/unit_tests/extensions/test_types.py | 34 ++-
11 files changed, 428 insertions(+), 119 deletions(-)
diff --git a/docs/developer_docs/extensions/development.md
b/docs/developer_docs/extensions/development.md
index 57810965810..c1f7a51be5e 100644
--- a/docs/developer_docs/extensions/development.md
+++ b/docs/developer_docs/extensions/development.md
@@ -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
diff --git a/superset-core/src/superset_core/extensions/types.py
b/superset-core/src/superset_core/extensions/types.py
index fc24576063b..bfaeba5a43c 100644
--- a/superset-core/src/superset_core/extensions/types.py
+++ b/superset-core/src/superset_core/extensions/types.py
@@ -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):
diff --git a/superset-extensions-cli/src/superset_extensions_cli/cli.py
b/superset-extensions-cli/src/superset_extensions_cli/cli.py
index 13c9d3f60a8..18d45f4b682 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/cli.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/cli.py
@@ -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")
diff --git
a/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
b/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
index 135f45e28a9..77543efc145 100644
---
a/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
+++
b/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
@@ -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 = []
diff --git
a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
index dc750a22545..9cc05302dbb 100644
---
a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
+++
b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
@@ -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": []
}
diff --git a/superset-extensions-cli/tests/test_cli_build.py
b/superset-extensions-cli/tests/test_cli_build.py
index 2e95ff4f3c1..76327af703a 100644
--- a/superset-extensions-cli/tests/test_cli_build.py
+++ b/superset-extensions-cli/tests/test_cli_build.py
@@ -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"
[email protected]
-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)
-
- with pytest.raises(SystemExit) as exc_info:
- copy_backend_files(isolated_filesystem)
-
- assert exc_info.value.code == 1
+ # config.py (pattern: "config.py")
+ assert_file_exists(dist_dir / "backend" / "config.py")
+
+ # 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"
+ )
+
+ # 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
diff --git a/superset-extensions-cli/tests/test_cli_init.py
b/superset-extensions-cli/tests/test_cli_init.py
index ce9fa963b6a..5a03a48f917 100644
--- a/superset-extensions-cli/tests/test_cli_init.py
+++ b/superset-extensions-cli/tests/test_cli_init.py
@@ -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
diff --git a/superset-extensions-cli/tests/test_cli_validate.py
b/superset-extensions-cli/tests/test_cli_validate.py
index e3f7e6a139d..970a2ce13ce 100644
--- a/superset-extensions-cli/tests/test_cli_validate.py
+++ b/superset-extensions-cli/tests/test_cli_validate.py
@@ -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"])
diff --git a/superset-extensions-cli/tests/test_templates.py
b/superset-extensions-cli/tests/test_templates.py
index 80dee4fae09..918cb75cd94 100644
--- a/superset-extensions-cli/tests/test_templates.py
+++ b/superset-extensions-cli/tests/test_templates.py
@@ -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")
diff --git a/superset/initialization/__init__.py
b/superset/initialization/__init__.py
index c3dacc7d6ea..25cc42e16d3 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -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:
"""
diff --git a/tests/unit_tests/extensions/test_types.py
b/tests/unit_tests/extensions/test_types.py
index 79182c652c5..c2b4e1c1de9 100644
--- a/tests/unit_tests/extensions/test_types.py
+++ b/tests/unit_tests/extensions/test_types.py
@@ -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)