This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch sl-1-extension-loading
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 91d018c52dd1f3a362de94018c3eab4c38a40d04
Author: Beto Dealmeida <[email protected]>
AuthorDate: Mon Feb 2 17:23:53 2026 -0500

    feat(extensions): improve extension loading for backend modules
    
    - Add load_extension_backend() to install in-memory modules and import 
entry points
    - Entry points are module names that self-register on import
    - Add volume mounts in docker-compose-light.yml for extensions development:
      - superset-core for local Pydantic models
      - extensions directory for .supx bundles
    - Add EXTENSIONS_PATH config support
    - Simplify init_extensions() to delegate to get_extensions()
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
---
 docker-compose-light.yml                 |  2 ++
 docker/pythonpath_dev/superset_config.py | 10 +++++++++-
 superset/extensions/utils.py             | 30 ++++++++++++++++++++++++++++++
 superset/initialization/__init__.py      | 27 ++++-----------------------
 4 files changed, 45 insertions(+), 24 deletions(-)

diff --git a/docker-compose-light.yml b/docker-compose-light.yml
index 09b0c65b0f8..a3774ac1c65 100644
--- a/docker-compose-light.yml
+++ b/docker-compose-light.yml
@@ -64,9 +64,11 @@ x-superset-volumes: &superset-volumes
   # /app/pythonpath_docker will be appended to the PYTHONPATH in the final 
container
   - ./docker:/app/docker
   - ./superset:/app/superset
+  - ./superset-core:/app/superset-core
   - ./superset-frontend:/app/superset-frontend
   - superset_home_light:/app/superset_home
   - ./tests:/app/tests
+  - ./extensions:/app/extensions
 x-common-build: &common-build
   context: .
   target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
diff --git a/docker/pythonpath_dev/superset_config.py 
b/docker/pythonpath_dev/superset_config.py
index d88d9899c27..e647b7827ad 100644
--- a/docker/pythonpath_dev/superset_config.py
+++ b/docker/pythonpath_dev/superset_config.py
@@ -105,7 +105,15 @@ class CeleryConfig:
 
 CELERY_CONFIG = CeleryConfig
 
-FEATURE_FLAGS = {"ALERT_REPORTS": True}
+# Extensions configuration
+# For local development, point to the extensions directory
+# Note: If running in Docker, this path needs to be accessible from inside the 
container
+EXTENSIONS_PATH = os.getenv("EXTENSIONS_PATH", "/app/extensions")
+
+FEATURE_FLAGS = {
+    "ALERT_REPORTS": True,
+    "ENABLE_EXTENSIONS": True,
+}
 ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
 WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', 
'/')}/"  # When using docker compose baseurl should be 
http://superset_nginx{ENV{BASEPATH}}/  # noqa: E501
 # The base URL for the email report hyperlinks.
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index 883c9114728..07bd118cd65 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -223,6 +223,34 @@ def build_extension_data(extension: LoadedExtension) -> 
dict[str, Any]:
     return extension_data
 
 
+def load_extension_backend(extension: LoadedExtension) -> None:
+    """
+    Load an extension's backend code by installing modules and importing entry 
points.
+
+    Entry points are module names that get imported. The modules are expected 
to
+    self-register any capabilities (e.g., semantic layers) when imported.
+    """
+    # Install backend modules in-memory if present
+    if extension.backend:
+        install_in_memory_importer(
+            extension.backend,
+            source_base_path=extension.source_base_path,
+        )
+
+    # Import entry point modules - they self-register on import
+    manifest = extension.manifest
+    if manifest.backend:
+        for module_name in manifest.backend.entryPoints:
+            try:
+                eager_import(module_name)
+            except Exception:
+                logger.exception(
+                    "Failed to load entry point '%s' from extension %s",
+                    module_name,
+                    extension.name,
+                )
+
+
 def get_extensions() -> dict[str, LoadedExtension]:
     extensions: dict[str, LoadedExtension] = {}
 
@@ -234,6 +262,7 @@ def get_extensions() -> dict[str, LoadedExtension]:
         extension = get_loaded_extension(files, source_base_path=abs_dist_path)
         extension_id = extension.manifest.id
         extensions[extension_id] = extension
+        load_extension_backend(extension)
         logger.info(
             "Loading extension %s (ID: %s) from local filesystem",
             extension.name,
@@ -248,6 +277,7 @@ def get_extensions() -> dict[str, LoadedExtension]:
             extension_id = extension.manifest.id
             if extension_id not in extensions:  # Don't override 
LOCAL_EXTENSIONS
                 extensions[extension_id] = extension
+                load_extension_backend(extension)
                 logger.info(
                     "Loading extension %s (ID: %s) from discovery path",
                     extension.name,
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index adc783c1e12..4f2eb9d7648 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -546,37 +546,18 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
             self.init_extensions()
 
     def init_extensions(self) -> None:
-        from superset.extensions.utils import (
-            eager_import,
-            get_extensions,
-            install_in_memory_importer,
-        )
+        from superset.extensions.utils import get_extensions
 
         try:
-            extensions = get_extensions()
+            # get_extensions() discovers and loads all extensions,
+            # including installing in-memory importers and registering entry 
points
+            get_extensions()
         except Exception:  # pylint: disable=broad-except  # noqa: S110
             # If the db hasn't been initialized yet, an exception will be 
raised.
             # It's fine to ignore this, as in this case there are no extensions
             # present yet.
             return
 
-        for extension in extensions.values():
-            if backend_files := extension.backend:
-                install_in_memory_importer(
-                    backend_files,
-                    source_base_path=extension.source_base_path,
-                )
-
-            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)
-
     def init_app_in_ctx(self) -> None:
         """
         Runs init logic in the context of the app

Reply via email to