How to write a custom PyZEAL plugin

Imports

[1]:
# some standard library imports
from json import load
from os.path import dirname, join
from typing import Callable, Optional, Tuple, Type

# the core imports for writing your custom logic and the plugin
from pyzeal.algorithms.finder_algorithm import FinderAlgorithm
from pyzeal.plugins.pyzeal_plugin import PyZEALPlugin
from pyzeal.plugins.installation_helper import InstallationHelper
from pyzeal.utils.root_context import RootContext

Implement your custom logic

[2]:
class TestAlgorithm(FinderAlgorithm):
    "My awesome custom root finding algorithm!"

    def __init__(self, arg1: str, arg2: int) -> None:
        self.arg1 = arg1
        self.arg2 = arg2

    def calcRoots(self, context: RootContext) -> None:
        "Override this to implement an algorithm."
        raise NotImplementedError(
            f"test algorithm [{self.arg1} | {self.arg2}] is not implemented!"
        )

    def __str__(self) -> str:
        "Just for printing, you don't actually need this."
        return f"TestAlgorithm({self.arg1}, {self.arg2})...! :)"

How to include custom configuration data

[3]:
# here is an example of how to include custom configuration data with your
# plugin:
configFile = "./algorithm_data.json"
# once you installed the 'algorithm_data.json' data file you should use the
# following line instead:
# configFile = InstallationHelper.returnDataPath("algorithm_data.json")

with open(configFile, "r", encoding="utf-8") as f:
    customData = load(f)

The actual plugin class

[4]:
class AlgorithmPlugin(PyZEALPlugin[FinderAlgorithm]):
    "My custom plugin (providing my algorithm to PyZEAL)."

    _instance: Optional[PyZEALPlugin[FinderAlgorithm]] = None

    @staticmethod
    def initialize() -> Callable[..., FinderAlgorithm]:
        "This is the hook of plugins into `PyZEAL`."
        arg1, arg2 = customData["arg1"], customData["arg2"]
        return lambda: TestAlgorithm(arg1=arg1, arg2=arg2)

    @staticmethod
    def getInstance() -> PyZEALPlugin[FinderAlgorithm]:
        "Plugins should be realized as singletons."
        if AlgorithmPlugin._instance is None:
            AlgorithmPlugin._instance = AlgorithmPlugin()
        return AlgorithmPlugin._instance

    @property
    def pluginType(self) -> Type[FinderAlgorithm]:
        "The type provided by the plugin."
        return FinderAlgorithm

    @property
    def pluginName(self) -> str:
        "The name of the plugin."
        return "MyTestAlgorithmPlugin"

    @property
    def pluginVersion(self) -> Tuple[int, int, int]:
        """
        The version of the plugin (the combination of version and name should
        be unique). The semantics are (`major`, `minor`, `patch`).
        """
        return (22, 1, 0)


if __name__ == "__main__":
    print(f"the plugin itself:     {AlgorithmPlugin.getInstance()}")
    print(f"the provided service:  {AlgorithmPlugin.initialize()()}")
the plugin itself:     MyTestAlgorithmPlugin           @ v22.1.0
the provided service:  TestAlgorithm(hi from algorithm_data.json!, 9)...! :)

Installing the plugin

With the prerequisits above placed in a single .py source file named algorithm_plugin.py you can now install your plugin as follows:

$ pyzeal plugin --install algorithm_plugin.py
$ pyzeal plugin --install algorithm_data.json

Your algorithm will now be used by any root finder instances! If you would like to change the configuration data provided via algorithm_data.json, simple adjust the file contents and issue the second command again.

Note that your plugin overrides the default algorithms contained in PyZEAL. To restore these defaults simply --uninstall your plugin. By adapting the example above it is also quite straightforward to replace a given default algorithm with your custom plugin and leave the remaining ones untouched. To achieve this the return value of your plugin’s initialize must accept an algorithmType parameter of type pyzeal.pyzeal_types.algorithm_types.AlgorithmTypes. You may then return any algorithm of your choosing upon a given value of algorithmType.