From 088644ba332ec551598decbef049aaa6c1508a72 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 21:27:26 +0000 Subject: [PATCH] Load algorithms declared by other packages through entry points Packages declaring a module in the pymodaq_plugins_beam_shaping.algorithms entry point group get it imported at startup, so the algorithms registered with AlgorithmFactory appear in the extension. A module failing to import is logged and ignored. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- docs/src/user_guide/algorithms.rst | 17 +++++- .../algorithms/factory.py | 26 ++++++++- tests/algorithm_entry_points_test.py | 54 +++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/algorithm_entry_points_test.py diff --git a/docs/src/user_guide/algorithms.rst b/docs/src/user_guide/algorithms.rst index 12a151e..39b3763 100644 --- a/docs/src/user_guide/algorithms.rst +++ b/docs/src/user_guide/algorithms.rst @@ -205,4 +205,19 @@ shaping for the MRAF algorithm using different initial phase type and mixing par .. note:: The metrics (NRMSE, Fidelity, Efficiency) are **automatically saved** in the ``.h5beam`` file under - ``/RawData/Algorithm/``. See :numref:`sec-h5beam-structure` for details. \ No newline at end of file + ``/RawData/Algorithm/``. See :numref:`sec-h5beam-structure` for details. + +Algorithms provided by other packages +------------------------------------- + +Another Python package can add algorithms to the list of BeamShaping. It writes an algorithm class derived from +``AlgoBase``, registers it with the decorator ``AlgorithmFactory.register_algorithm()`` and declares the module +containing it in the entry point group ``pymodaq_plugins_beam_shaping.algorithms`` of its ``pyproject.toml``: + +.. code-block:: toml + + [project.entry-points."pymodaq_plugins_beam_shaping.algorithms"] + my_algorithms = "my_package.algorithms" + +BeamShaping imports these modules when it starts, after its own algorithms. A package that cannot be imported is +reported in the log and ignored. diff --git a/src/pymodaq_plugins_beam_shaping/algorithms/factory.py b/src/pymodaq_plugins_beam_shaping/algorithms/factory.py index 15dc46e..4e02e07 100644 --- a/src/pymodaq_plugins_beam_shaping/algorithms/factory.py +++ b/src/pymodaq_plugins_beam_shaping/algorithms/factory.py @@ -1,4 +1,5 @@ from importlib import import_module +from importlib.metadata import entry_points from pathlib import Path from typing import Callable @@ -10,8 +11,30 @@ logger = set_logger(get_module_name(__file__)) +ENTRY_POINT_GROUP = 'pymodaq_plugins_beam_shaping.algorithms' + + +def register_external_algorithms(group: str = ENTRY_POINT_GROUP): + """ Load the modules declared by other packages in the entry point group of the algorithms + + A package providing algorithms declares, in the entry point group *pymodaq_plugins_beam_shaping.algorithms*, a + module whose import registers its algorithms with the decorator *AlgorithmFactory.register_algorithm*. A package + failing to load is reported in the log and ignored. + """ + modules = [] + for entry_point in entry_points(group=group): + try: + modules.append(entry_point.load()) + except Exception as e: # a broken third party package should not prevent BeamShaping to start + logger.warning(f'Could not load the algorithms of {entry_point.value} ({entry_point.name}): {e!r}') + return modules + + def register_algorithms(parent_module_name: str = 'pymodaq_plugins_beam_shaping'): - """ Browse modules containing algorithms for optical shaping and register them in the factory""" + """ Browse modules containing algorithms for optical shaping and register them in the factory + + The algorithms provided by other packages through the entry point group *ENTRY_POINT_GROUP* are registered last. + """ algorithms = [] try: algorithm_module = import_module(f'{parent_module_name}.algorithms.algorithms') @@ -27,6 +50,7 @@ def register_algorithms(parent_module_name: str = 'pymodaq_plugins_beam_shaping' except ModuleNotFoundError as e: logger.warning(str(e)) finally: + algorithms.extend(register_external_algorithms()) return algorithms diff --git a/tests/algorithm_entry_points_test.py b/tests/algorithm_entry_points_test.py new file mode 100644 index 0000000..83f52e8 --- /dev/null +++ b/tests/algorithm_entry_points_test.py @@ -0,0 +1,54 @@ +from types import SimpleNamespace + +import pytest + +from pymodaq_plugins_beam_shaping.algorithms import factory +from pymodaq_plugins_beam_shaping.algorithms.factory import AlgorithmFactory, register_external_algorithms +from pymodaq_plugins_beam_shaping.algorithms.utils import LensSetup + + +def fake_entry_point(name, load): + return SimpleNamespace(name=name, value=f'fake_package.{name}', load=load) + + +@pytest.fixture +def clean_registry(): + before = dict(AlgorithmFactory.algorithms_registry) + yield + AlgorithmFactory.algorithms_registry.clear() + AlgorithmFactory.algorithms_registry.update(before) + + +def test_external_algorithm_is_registered(monkeypatch, clean_registry): + def load(): + @AlgorithmFactory.register_algorithm() + class External(factory.AlgoBase): + ALGO_NAME = 'External test algorithm' + SETUP_TYPE = LensSetup.TwoF + return External + + monkeypatch.setattr(factory, 'entry_points', lambda group: [fake_entry_point('external', load)]) + assert 'External test algorithm' not in AlgorithmFactory.algorithms_registry + + register_external_algorithms() + + assert 'External test algorithm' in AlgorithmFactory.algorithms_registry + + +def test_entry_point_group_is_the_documented_one(monkeypatch): + groups = [] + monkeypatch.setattr(factory, 'entry_points', lambda group: groups.append(group) or []) + register_external_algorithms() + assert groups == ['pymodaq_plugins_beam_shaping.algorithms'] + + +def test_broken_package_does_not_prevent_the_others(monkeypatch, clean_registry): + def broken(): + raise ModuleNotFoundError('missing dependency') + + def good(): + return 'module' + + monkeypatch.setattr(factory, 'entry_points', + lambda group: [fake_entry_point('broken', broken), fake_entry_point('good', good)]) + assert register_external_algorithms() == ['module']