From 9f7e7e144ccfa399b4c447b956d3818c4f701f10 Mon Sep 17 00:00:00 2001
From: Eric Prestat <eric.prestat@gmail.com>
Date: Fri, 23 Jan 2026 20:37:58 +0000
Subject: [PATCH] Speed up loading extensions specification

---
 hyperspy/extensions.py  | 114 +++++++++++-----------------------------
 hyperspy/ui_registry.py |   4 +-
 2 files changed, 34 insertions(+), 84 deletions(-)

diff --git a/hyperspy/extensions.py b/hyperspy/extensions.py
index 0282a67ea0..fc2853b0fa 100644
--- a/hyperspy/extensions.py
+++ b/hyperspy/extensions.py
@@ -17,102 +17,52 @@
 # along with HyperSpy. If not, see <https://www.gnu.org/licenses/#GPL>.
 
 import copy
-import json
+import importlib
 import logging
 from pathlib import Path
-from urllib.parse import urlparse
-from urllib.request import url2pathname
 
-import importlib_metadata as metadata
 import yaml
 
 _logger = logging.getLogger(__name__)
 
+
+# libyaml C bindings may be missing
+loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
+
 # Load hyperspy's own extensions
-_ext_f = Path(__file__).resolve().parent.joinpath("hyperspy_extension.yaml")
-with open(_ext_f, "r") as stream:
-    EXTENSIONS = yaml.safe_load(stream)
+with open(Path(__file__).parent / "hyperspy_extension.yaml", "r") as stream:
+    EXTENSIONS = yaml.load(stream, Loader=loader)
+
 EXTENSIONS["GUI"]["widgets"] = {}
 
 # External extensions are not integrated into the API and not
 # import unless needed
 ALL_EXTENSIONS = copy.deepcopy(EXTENSIONS)
 
-_external_extensions = [
-    entry_point for entry_point in metadata.entry_points(group="hyperspy.extensions")
-]
+_extensions = importlib.metadata.entry_points(group="hyperspy.extensions")
 
-for _external_extension in _external_extensions:
-    _logger.info("Enabling extension %s" % _external_extension.name)
+for _extension in _extensions:
+    _logger.info("Enabling extension %s" % _extension.name)
+    _path = (
+        Path(importlib.util.find_spec(_extension.name).origin).parent
+        / "hyperspy_extension.yaml"
+    )
 
-    _files = [
-        file
-        for file in _external_extension.dist.files
-        if "hyperspy_extension.yaml" in str(file)
-    ]
-
-    if not _files:  # pragma: no cover
-        # Editable installs for pyproject.toml based builds
-        # https://peps.python.org/pep-0610/#example-pip-commands-and-their-effect-on-direct-url-json
-        # https://peps.python.org/pep-0660/#frontend-requirements
-        _files = [
-            file
-            for file in _external_extension.dist.files
-            if "direct_url.json" in str(file)
-        ]
-        with _files[0].locate().open() as json_data:
-            _basepath = url2pathname(urlparse(json.load(json_data)["url"]).path)
-            # for packages with flat layout
-            _path = (
-                Path(_basepath) / _external_extension.name / "hyperspy_extension.yaml"
-            )
-            if not _path.exists():
-                # for packages with src layout
-                _path = (
-                    Path(_basepath)
-                    / "src"
-                    / _external_extension.name
-                    / "hyperspy_extension.yaml"
-                )
-    else:
-        _path = _files.pop().locate()
-
-    if not _path:  # pragma: no cover
-        # empty list: "hyperspy_extension.yaml" is missing from the package
-        _logger.error(
-            "Failed to load hyperspy extension from {0}. Please report this issue to the {0} developers".format(
-                _external_extension.name
-            )
-        )
-
-    elif not _path.exists():  # pragma: no cover
-        # Possible extension editable installation issue
-        _logger.error(
-            "Failed to load hyperspy extension from {0}. The path {1} doesn't exist.".format(
-                _external_extension.name, _path
-            )
-        )
-
-    else:
-        with open(str(_path)) as stream:
-            _external_extension = yaml.safe_load(stream)
-            if "signals" in _external_extension:
-                ALL_EXTENSIONS["signals"].update(_external_extension["signals"])
-            if "components1D" in _external_extension:
-                ALL_EXTENSIONS["components1D"].update(
-                    _external_extension["components1D"]
-                )
-            if "components2D" in _external_extension:
-                ALL_EXTENSIONS["components2D"].update(
-                    _external_extension["components2D"]
+    with open(str(_path)) as stream:
+        _extension_dict = yaml.load(stream, Loader=loader)
+        if "signals" in _extension_dict:
+            ALL_EXTENSIONS["signals"].update(_extension_dict["signals"])
+        if "components1D" in _extension_dict:
+            ALL_EXTENSIONS["components1D"].update(_extension_dict["components1D"])
+        if "components2D" in _extension_dict:
+            ALL_EXTENSIONS["components2D"].update(_extension_dict["components2D"])
+        if "GUI" in _extension_dict:
+            if "toolkeys" in _extension_dict["GUI"]:
+                ALL_EXTENSIONS["GUI"]["toolkeys"].extend(
+                    _extension_dict["GUI"]["toolkeys"]
                 )
-            if "GUI" in _external_extension:
-                if "toolkeys" in _external_extension["GUI"]:
-                    ALL_EXTENSIONS["GUI"]["toolkeys"].extend(
-                        _external_extension["GUI"]["toolkeys"]
-                    )
-                if "widgets" in _external_extension["GUI"]:
-                    for toolkit, specs in _external_extension["GUI"]["widgets"].items():
-                        if toolkit not in ALL_EXTENSIONS["GUI"]["widgets"]:
-                            ALL_EXTENSIONS["GUI"]["widgets"][toolkit] = {}
-                        ALL_EXTENSIONS["GUI"]["widgets"][toolkit].update(specs)
+            if "widgets" in _extension_dict["GUI"]:
+                for toolkit, specs in _extension_dict["GUI"]["widgets"].items():
+                    if toolkit not in ALL_EXTENSIONS["GUI"]["widgets"]:
+                        ALL_EXTENSIONS["GUI"]["widgets"][toolkit] = {}
+                    ALL_EXTENSIONS["GUI"]["widgets"][toolkit].update(specs)
diff --git a/hyperspy/ui_registry.py b/hyperspy/ui_registry.py
index 38230557d9..ab1dd91103 100644
--- a/hyperspy/ui_registry.py
+++ b/hyperspy/ui_registry.py
@@ -31,10 +31,10 @@
 
 import importlib
 
-from hyperspy.extensions import ALL_EXTENSIONS, _external_extensions
+from hyperspy.extensions import ALL_EXTENSIONS, _extensions
 
 UI_REGISTRY = {toolkey: {} for toolkey in ALL_EXTENSIONS["GUI"]["toolkeys"]}
-_EXTENSION_NAMES = [e.name for e in _external_extensions]
+_EXTENSION_NAMES = [e.name for e in _extensions]
 
 TOOLKIT_REGISTRY = set()
 KNOWN_TOOLKITS = set(("ipywidgets", "traitsui"))
