Skip to content

Singleton Scheme

The singleton scheme is similar to the subclass one with the exception: Loader returns plugin instance, rather than class. Instance is initialized on the first load, and only one instance of plugin exists.

We need the base class to define the scheme, then inherit the plugins.

Plugin Base

First, let's define the plugins base class just like in subclass scheme.

base.py
1
2
3
4
5
6
class BasePlugin(object):
    """Base class for our plugin."""

    def execute(self, x: int, y: int) -> int:
        """Plugin performs operation on two integers and returns integer."""
        ...

The code is straightforward

base.py
1
2
3
4
5
6
class BasePlugin(object):
    """Base class for our plugin."""

    def execute(self, x: int, y: int) -> int:
        """Plugin performs operation on two integers and returns integer."""
        ...

Define a base class for our plugins. We have no particular requirements, so we can derive it from object.

base.py
1
2
3
4
5
6
class BasePlugin(object):
    """Base class for our plugin."""

    def execute(self, x: int, y: int) -> int:
        """Plugin performs operation on two integers and returns integer."""
        ...

Though docstrings are advisory, it will help to navigate our code, so describe plugin tasks and features.

base.py
1
2
3
4
5
6
class BasePlugin(object):
    """Base class for our plugin."""

    def execute(self, x: int, y: int) -> int:
        """Plugin performs operation on two integers and returns integer."""
        ...

Our main function of the plugin, do not to forget to place the proper type hints to allow static type checking.

base.py
1
2
3
4
5
6
class BasePlugin(object):
    """Base class for our plugin."""

    def execute(self, x: int, y: int) -> int:
        """Plugin performs operation on two integers and returns integer."""
        ...

The main function of the plugin already should be documented.

base.py
1
2
3
4
5
6
class BasePlugin(object):
    """Base class for our plugin."""

    def execute(self, x: int, y: int) -> int:
        """Plugin performs operation on two integers and returns integer."""
        ...

Ellipses operator (...) shows the implementation is abstract. If missed, mypy will complain the function doesn't return an integer value.

Application Core

First, lets define the application's core:

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Lets explain the code:

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Import sys module to parse the CLI argument.

Warning

We use sys.argv only for demonstration purposes. Use argsparse or alternatives in real-world applications.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Import Loader class.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Import PluginBase class to define Loader type.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Then let's create a loader instance. You need only one loader instance per each type for your application. So loaders are usually singletons.

Loader is the generic type, so we must pass the exact plugin type. The instances of BasePlugin type has the BasePlugin type. We'd placed the type into the brackets just after the Loader.

After defining the plugin's type, we need to initialize the loader itself. Loader has several initialization parameters, see Reference for details. Here we consider our plugins will be in plugins folder of our applications.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Our main function accepts the operation's name and two integer arguments. Then it prints the result.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Loader supports dict-like interface to access the modules. For this example, we will use bracket notation. We use op parameter as the plugin name. Unlike the subclass scheme, our loader returns the initialized instance directly.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Then we call execute method of the plugin. Your editor must show the r variable has the type of int

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))

Then we print the result, and our core function is finally complete.

__main__.py
import sys

from gufo.loader import Loader

from .base import BasePlugin

loader = Loader[BasePlugin](base="myapp.plugins")


def main(op: str, x: int, y: int) -> None:
    item = loader[op]
    r = item.execute(x, y)
    print(r)


main(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
We're extracting our arguments directly from sys.argv. Then we call our core function. The core is complete.

Plugins

Next we need to implement plugins itself. First, create directory plugins for our plugins packages. Then add empty __init__.py file.

We're ready to write our plugins.

add Plugin

Lets implement the plugin for adding numbers. Our plugin has the name add, so we're placing it into add.py file.

plugins/add.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()

The code is pretty and clean

plugins/add.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()
Import the base class BasePlugin. Note, we use relative import to clean up our code.

plugins/add.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()
Create new plugin class and inherit it from BasePlugin.

plugins/add.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()
Then override the execute function.

plugins/add.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()

All we need is to add two numbers and return the result.

plugins/add.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()

Let's define a singleton. Our plugin is complete.

sub Plugin

Let's create another plugin for subtraction. Our plugin has the name sub, so we're placing it into sub.py file.

plugins/sub.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class SubPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x - y


sub_plugin = SubPlugin()

Pretty like the add plugin, only the class name and implementation differ.

plugins/sub.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class SubPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x - y


sub_plugin = SubPlugin()
All we need is to subtract two numbers and return the result.

plugins/sub.py
1
2
3
4
5
6
7
8
9
from ..base import BasePlugin


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y


add_plugin = AddPlugin()

Let's define a singleton. Our plugin is complete.

Testing

$ python3 -m myapp add 1 2
3
$ python3 -m myapp sub 2 1
1

Summary

We have learned how to create simple and extendable applications using singleton-base approach.