.. _extension_plugins: Extension Plugins ================= .. toctree:: :maxdepth: 3 :caption: Contents: PyMoDAQ's plugins allows to add functionnalities to PyMoDAQ from external packages. You should be well aware of the instrument type plugins and somehow of the PID models plugins. Here we are highlighting how to built dashboard extensions such as the :ref:`DAQ_Scan_module`. .. note:: The base classes of an extension, ``CustomExt`` and ``CustomApp``, are explained in the previous section of the developer's guide, :ref:`custom_app`, with diagrams of their structure, of their lifecycle and of the objects they use. Read it first: this page only covers how to package an extension in a plugin. An extension is a user interface that runs inside the Dashboard and uses the instruments (control modules) the Dashboard already manages, for instance to scan an actuator while recording a detector, or to log their data. The base class for such an interface is ``CustomExt``: it is ``CustomApp`` (the generic user-interface skeleton also used by standalone applications and by the Dashboard itself) plus what deals with instruments and the Dashboard. The big picture, with diagrams, is in :ref:`custom_app_context`. For your package to be considered as a PyMoDAQ's dashboard extension, you should make sure of a few things: * The entrypoint in the *pyproject.toml* file should be correctly configured, see :numref:`extension_entrypoint_fig` * The presence of an *extensions* module at the root of the package * each module within the *extensions* module will define an extension. It should contains three attributes: * EXTENSION_NAME: a string used to display the extension name in the dashboard extension menu * CLASS_NAME: a string giving the name of the extension class * a class deriving from the ``CustomExt`` base class (see :ref:`custom_app`). ``CustomExt`` itself extends ``CustomApp`` with what deals with instruments and the Dashboard (``dashboard``, ``modules_manager``, experiment and state managers, Dashboard toolbar), through the ``DashboardMixin`` (see :ref:`dashboard_mixin`) The *pymodaq_plugins_template* contains already all this, so make sure to start from there when you wish to build an extension. ``check_plugin`` (see :ref:`instrument_plugin_doc`) also reads the modules of the *extensions* folder, without importing them, and reports the codes PMQ330 to PMQ339: missing ``EXTENSION_NAME`` or ``CLASS_NAME``, a class that does not derive from ``CustomExt``, deprecated imports and hooks, managers created by the extension instead of those of the Dashboard, an ``__init__`` that does not take ``(parent, dashboard)`` or never calls ``self.setup_ui()``, a ``_quit_fun`` missing or never returning ``True``. ``_quit_fun`` is the guard of ``quit_fun``: return ``False`` while something is left running (threads, workers...) and ``True`` when everything is released, otherwise the extension is never closed. Do not override ``quit_fun`` itself: it calls the guard, then closes the window and shows the Dashboard again. To check an extension while you write it, without the rest of the package, or an extension of PyMoDAQ itself, use the ``check_extension`` command on a file or a folder:: check_extension src/pymodaq_plugins_xxxx/extensions/my_extension.py check_extension [--fail-on error|warning|todo] [--core] [--color auto|always|never] path [path ...] Like ``check_plugin`` it returns 1 if there are findings of the ``--fail-on`` level, and it reads the sources without importing them. The extensions of PyMoDAQ (``pymodaq/extensions``) are recognized from their path and need not define ``EXTENSION_NAME`` and ``CLASS_NAME`` (PMQ330 is then not reported, ``--core`` forces it). .. _extension_entrypoint_fig: .. figure:: /image/extensions/entrypoint.png :alt: dashboard The correct configuration of your package. The class itself defining the extension derives from the ``CustomExt`` base class (``pymodaq.utils.custom_ext``), which extends the ``CustomApp`` base class. As such, it's ``__init__`` method takes two attributes, a ``DockArea`` instance and a ``DashBoard`` instance (the one from which the extension will be loaded and that contains all the actuators/detectors needed for your extension). The ``DashBoard`` will smoothly initialize your class when launching it from the menu. Below you'll find a sample of an extension module with an extension class called ``MyExtension`` (from the *pymodaq_plugins_template* package) .. code-block:: EXTENSION_NAME = 'MY_EXTENSION_NAME' CLASS_NAME = 'MyExtension' class MyExtension(CustomExt): # list of dicts enabling the settings tree on the user interface params = [ {'title': 'Main settings:', 'name': 'main_settings', 'type': 'group', 'children': [ {'title': 'Save base path:', 'name': 'base_path', 'type': 'browsepath', 'value': config['data_saving']['h5file']['save_path']}, {'title': 'File name:', 'name': 'target_filename', 'type': 'str', 'value': "", 'readonly': True}, {'title': 'Date:', 'name': 'date', 'type': 'date', 'value': QtCore.QDate.currentDate()}, {'title': 'Do something, such as showing data:', 'name': 'do_something', 'type': 'bool', 'value': False}, {'title': 'Something done:', 'name': 'something_done', 'type': 'led', 'value': False, 'readonly': True}, {'title': 'Infos:', 'name': 'info', 'type': 'text', 'value': ""}, {'title': 'push:', 'name': 'push', 'type': 'bool_push', 'value': False} ]}, {'title': 'Other settings:', 'name': 'other_settings', 'type': 'group', 'children': [ {'title': 'List of stuffs:', 'name': 'list_stuff', 'type': 'list', 'value': 'first', 'limits': ['first', 'second', 'third'], 'tip': 'choose a stuff from the list'}, {'title': 'List of integers:', 'name': 'list_int', 'type': 'list', 'value': 0, 'limits': [0, 256, 512], 'tip': 'choose a stuff from this int list'}, {'title': 'one integer:', 'name': 'an_integer', 'type': 'int', 'value': 500, }, {'title': 'one float:', 'name': 'a_float', 'type': 'float', 'value': 2.7, }, ]}, ] def __init__(self, dockarea, dashboard): super().__init__(dockarea, dashboard) self.setup_ui() With such a file in the extensions folder, the dashboard will be able to see it and will list it into its available extensions. Then you'll have to code the inners of your extension following the ``CustomExt`` class, which extends ``CustomApp`` (see :ref:`custom_app`). The big difference between extensions and Standalone apps resides in the fact that your dashboard instance is available here, hence all the control modules it contains. You'll be able to use all their functionalities only focusing on your extension layout! .. _testing_extension: Testing an extension -------------------- The tools of ``pymodaq.utils.plugin_testing`` (see :ref:`instrument_plugin_doc` for those testing instrument plugins) load an extension in an empty Dashboard, without hardware and without showing any window, and close everything cleanly afterwards (the windows are deleted: left alive, they slow down all the tests run after them). .. versionadded:: 5.3.7 ``loaded_extension`` is a context manager giving the Dashboard and the extension instance, to write the test of the behaviour of your extension. ``PluginExtensionChecks`` is a pytest mixin that adds, for free, one test per extension of your package, checking that it loads and closes: .. code-block:: python from pymodaq.utils.plugin_testing import loaded_extension, PluginExtensionChecks from pymodaq_plugins_myplugin.extensions.my_extension import MyExtension class TestExtensions(PluginExtensionChecks): # package_name is found from the folder if omitted pass def test_do_something(): with loaded_extension(MyExtension) as (dashboard, extension): extension.settings.child('main_settings', 'do_something').setValue(True) assert extension.settings.child('other_settings', 'something_done').value() The Dashboard is empty: no instrument is loaded in it. Set ``strict_imports = True`` on the mixin in the CI of your plugin, so that an extension that cannot be imported fails instead of being skipped. The extensions of PyMoDAQ itself are tested with the same function (``packages/pymodaq/tests/extensions``). .. _plugin_models: Models of the extensions ------------------------ Some extensions delegate the part that depends on your experiment to a *model*: a small class that you write and that the extension finds in your plugin package. Four extensions use models: .. list-table:: :header-rows: 1 :widths: 30 30 40 * - Extension - Base class of the model - Example in *pymodaq_plugins_mockexamples* * - :ref:`PID ` - ``PIDModelGeneric`` - ``PIDModelBeamSteering``, ``PIDModelMockHeater`` * - :ref:`Bayesian ` and :ref:`Adaptive ` - ``OptimizerModelGeneric`` or ``OptimizerModelDefault`` - ``BayesianModelMock`` * - :ref:`DataMixer ` - ``DataMixerModel`` - ``DataMixerModelFit`` * - Sequencer, see :ref:`Extending the Sequencer ` - ``ChoiceModelBase``, registered with the ``@ChoiceModelFactory.register_choice()`` decorator (the model is not found by its base class) - ``UserInput``, ``ThresholdChoiceModel``, ``TrueChoiceModel`` and ``FalseChoiceModel`` in ``pymodaq/extensions/sequencer/models`` To make your models visible: * set ``models = true`` in the ``[features]`` section of the *pyproject.toml* of your package (this creates the ``pymodaq.models`` entry point); * put each model in its own module of the ``models`` folder at the root of the package (the file name is the name of the model in the extension). The class of the module that derives from the base class of the extension is the model. It can derive from an intermediate class of your own, and the base class itself or classes imported from other modules are ignored. The package `pymodaq_plugins_mockexamples `__ gathers working examples of models together with the mock actuators and detectors to try them with: start from there rather than from a blank file, and see the page of each extension for the methods to implement.