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
