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 281c0c9672 chore: add paths to backend extension stack traces (#37300)
281c0c9672 is described below
commit 281c0c9672297624b2ad9a8166b970440578a6b7
Author: Ville Brofeldt <[email protected]>
AuthorDate: Wed Jan 21 06:19:28 2026 -0800
chore: add paths to backend extension stack traces (#37300)
---
superset/extensions/discovery.py | 22 ++++++++++++++-
superset/extensions/types.py | 3 ++
superset/extensions/utils.py | 55 ++++++++++++++++++++++++++++++++-----
superset/initialization/__init__.py | 5 +++-
4 files changed, 76 insertions(+), 9 deletions(-)
diff --git a/superset/extensions/discovery.py b/superset/extensions/discovery.py
index 5727a9d14b..41ad69c18b 100644
--- a/superset/extensions/discovery.py
+++ b/superset/extensions/discovery.py
@@ -23,6 +23,7 @@ from zipfile import is_zipfile, ZipFile
from superset.extensions.types import LoadedExtension
from superset.extensions.utils import get_bundle_files_from_zip,
get_loaded_extension
+from superset.utils import json
logger = logging.getLogger(__name__)
@@ -59,8 +60,27 @@ def discover_and_load_extensions(
try:
with ZipFile(supx_file, "r") as zip_file:
+ # Read the manifest first to get the extension ID for the
+ # supx:// path
+ try:
+ manifest_content = zip_file.read("manifest.json")
+ manifest_data = json.loads(manifest_content)
+ extension_id = manifest_data["id"]
+ except (KeyError, json.JSONDecodeError) as e:
+ logger.error(
+ "Failed to read extension ID from manifest in %s:
%s",
+ supx_file,
+ e,
+ )
+ continue
+
+ # Use supx:// scheme for tracebacks
+ source_base_path = f"supx://{extension_id}"
+
files = get_bundle_files_from_zip(zip_file)
- extension = get_loaded_extension(files)
+ extension = get_loaded_extension(
+ files, source_base_path=source_base_path
+ )
logger.info(
"Loaded extension '%s' from %s", extension.id,
supx_file
)
diff --git a/superset/extensions/types.py b/superset/extensions/types.py
index 07d7a317b6..3f137abcdd 100644
--- a/superset/extensions/types.py
+++ b/superset/extensions/types.py
@@ -34,3 +34,6 @@ class LoadedExtension:
frontend: dict[str, bytes]
backend: dict[str, bytes]
version: str
+ source_base_path: (
+ str # Base path for traceback filenames (absolute path or supx:// URL)
+ )
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index d885676d4d..883c911472 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -55,15 +55,30 @@ class InMemoryLoader(importlib.abc.Loader):
)
if self.is_package:
module.__path__ = []
- exec(self.source, module.__dict__) # noqa: S102
+ # Compile with filename for proper tracebacks
+ code = compile(self.source, self.origin, "exec")
+ exec(code, module.__dict__) # noqa: S102
class InMemoryFinder(importlib.abc.MetaPathFinder):
- def __init__(self, file_dict: dict[str, bytes]) -> None:
+ def __init__(self, file_dict: dict[str, bytes], source_base_path: str) ->
None:
self.modules: dict[str, Tuple[Any, Any, Any]] = {}
+
+ # Detect if this is a virtual path (supx://) or filesystem path
+ is_virtual_path = source_base_path.startswith("supx://")
+
for path, content in file_dict.items():
mod_name, is_package = self._get_module_name(path)
- self.modules[mod_name] = (content, is_package, path)
+
+ # Reconstruct full path for tracebacks
+ if is_virtual_path:
+ # Virtual paths always use forward slashes
+ # e.g., supx://extension-id/backend/src/tasks.py
+ full_path = f"{source_base_path}/backend/src/{path}"
+ else:
+ full_path = str(Path(source_base_path) / "backend" / "src" /
path)
+
+ self.modules[mod_name] = (content, is_package, full_path)
def _get_module_name(self, file_path: str) -> Tuple[str, bool]:
parts = list(Path(file_path).parts)
@@ -88,8 +103,19 @@ class InMemoryFinder(importlib.abc.MetaPathFinder):
return None
-def install_in_memory_importer(file_dict: dict[str, bytes]) -> None:
- finder = InMemoryFinder(file_dict)
+def install_in_memory_importer(
+ file_dict: dict[str, bytes], source_base_path: str
+) -> None:
+ """
+ Install an in-memory module importer for extension backend code.
+
+ :param file_dict: Dictionary mapping relative file paths to their content
+ :param source_base_path: Base path for traceback filenames. For
LOCAL_EXTENSIONS,
+ this should be an absolute filesystem path to the dist directory.
+ For EXTENSIONS_PATH (.supx files), this should be a supx:// URL
+ (e.g., "supx://extension-id").
+ """
+ finder = InMemoryFinder(file_dict, source_base_path)
sys.meta_path.insert(0, finder)
@@ -121,7 +147,19 @@ def get_bundle_files_from_path(base_path: str) ->
Generator[BundleFile, None, No
yield BundleFile(name=rel_path, content=content)
-def get_loaded_extension(files: Iterable[BundleFile]) -> LoadedExtension:
+def get_loaded_extension(
+ files: Iterable[BundleFile], source_base_path: str
+) -> LoadedExtension:
+ """
+ Load an extension from bundle files.
+
+ :param files: Iterable of BundleFile objects containing the extension files
+ :param source_base_path: Base path for traceback filenames. For
LOCAL_EXTENSIONS,
+ this should be an absolute filesystem path to the dist directory.
+ For EXTENSIONS_PATH (.supx files), this should be a supx:// URL
+ (e.g., "supx://extension-id").
+ :returns: LoadedExtension instance
+ """
manifest: Manifest | None = None
frontend: dict[str, bytes] = {}
backend: dict[str, bytes] = {}
@@ -158,6 +196,7 @@ def get_loaded_extension(files: Iterable[BundleFile]) ->
LoadedExtension:
frontend=frontend,
backend=backend,
version=manifest.version,
+ source_base_path=source_base_path,
)
@@ -190,7 +229,9 @@ def get_extensions() -> dict[str, LoadedExtension]:
# Load extensions from LOCAL_EXTENSIONS configuration (filesystem paths)
for path in current_app.config["LOCAL_EXTENSIONS"]:
files = get_bundle_files_from_path(path)
- extension = get_loaded_extension(files)
+ # Use absolute filesystem path to dist directory for tracebacks
+ abs_dist_path = str((Path(path) / "dist").resolve())
+ extension = get_loaded_extension(files, source_base_path=abs_dist_path)
extension_id = extension.manifest.id
extensions[extension_id] = extension
logger.info(
diff --git a/superset/initialization/__init__.py
b/superset/initialization/__init__.py
index 1f18f7da0c..adc783c1e1 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -562,7 +562,10 @@ class SupersetAppInitializer: # pylint:
disable=too-many-public-methods
for extension in extensions.values():
if backend_files := extension.backend:
- install_in_memory_importer(backend_files)
+ install_in_memory_importer(
+ backend_files,
+ source_base_path=extension.source_base_path,
+ )
backend = extension.manifest.backend