Skip to content

Subclass Scheme

The subclass scheme is the simplest to implement and to understand. We need the base class to define the scheme, then inherit the plugins.

Plugin Base

First, let's define the plugins base class:

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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


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


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

Import the Python modules sys and typing. We need the typing to define the loader's type. sys is used 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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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. In the subclass scheme plugins are classes, derived from the BasePlugin class. In Python's typing terms, the subclass of BasePlugin has the Type[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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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.

__main__.py
import sys
from typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


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


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

Loader returns the class. We create the instance to show we can use some plugin initialization tasks. We can also define the execute method as a @classmethod to skip the initialization step.

__main__.py
import sys
from typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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 typing import Type

from gufo.loader import Loader

from .base import BasePlugin

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


def main(op: str, x: int, y: int) -> None:
    kls = loader[op]
    item = kls()
    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
from ..base import BasePlugin


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

The code is pretty and clean

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


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y
Import the base class BasePlugin. Note, we use relative import to clean up our code.

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


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y
Create new plugin class and inherit it from BasePlugin.

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


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y
Then override the execute function.

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


class AddPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x + y
All we need is to add two numbers and return the result. 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
from ..base import BasePlugin


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

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

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


class SubPlugin(BasePlugin):
    def execute(self, x: int, y: int) -> int:
        return x - y
All we need is to subtract two numbers and return the result. 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 subclass-based approach.