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)


Reply via email to