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 8923378bf0fe6e3c9bb65fba35a1f97e0843b433 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 - Fix docker-compose-light.yml for development: - Add superset-core volume mount - Fix EXAMPLES_HOST to use db-light service - Change WEBPACK_DEVSERVER_HOST default to 0.0.0.0 - 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 | 8 +++++++- docker/pythonpath_dev/superset_config.py | 10 +++++++++- superset/extensions/utils.py | 30 ++++++++++++++++++++++++++++++ superset/initialization/__init__.py | 27 ++++----------------------- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/docker-compose-light.yml b/docker-compose-light.yml index 09b0c65b0f8..4b805798a4e 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` @@ -138,6 +140,10 @@ services: DATABASE_DB: superset_light POSTGRES_DB: superset_light SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py + # Override examples host to use light DB service + EXAMPLES_HOST: db-light + # Skip example loading for faster startup + SUPERSET_LOAD_EXAMPLES: "no" healthcheck: disable: true @@ -160,7 +166,7 @@ services: # configuring the dev-server to use the host.docker.internal to connect to the backend superset: "http://superset-light:8088" # Webpack dev server configuration - WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-127.0.0.1}" + WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-0.0.0.0}" WEBPACK_DEVSERVER_PORT: "${WEBPACK_DEVSERVER_PORT:-9000}" ports: - "${NODE_PORT:-9001}:9000" # Parameterized port, accessible on all interfaces 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
