Compare commits

...

1 Commits

Author SHA1 Message Date
Beto Dealmeida
91d018c52d 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 <noreply@anthropic.com>
2026-02-03 14:59:23 -05:00
4 changed files with 45 additions and 24 deletions

View File

@@ -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`

View File

@@ -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.

View File

@@ -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,

View File

@@ -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