DEV Community

Cover image for Plugin Architecture in Python
Charles White
Charles White

Posted on

Plugin Architecture in Python

For some of your Python applications, a plugin architecture could really help you to extend the functionality of your applications without affecting the core structure of your application. Why would you want to do this? Well, it helps you to separate the core system and allows either yourself or others to extend the functionality of the core system safely and reliably.

Some of the advantages include that when a new plugin is created, for example, you would just need to test the plugin and the whole application. The other big advantage is that your application can grow by your community making your application even more appealing. Two classic examples are the plugins for Wordpress blog and the plugins for Sublime text editor. In both cases, the plugins enhance the functionality of the core system but the core system developer did not need to create the plugin.

There are disadvantages too however with one of the main ones being that you can only extend the functionality based on the constraints that is imposed on the plugin placeholder e.g. if an app allows plugins for formatting text in a GUI, it's unlikely you can create a plugin to play videos.

There are several methods to create a plugin architecture, here we will walkthrough the approach using importlib.

The Basic Structure Of A Plugin Architecture

At its core, a plugin architecture consists of two components: a core system and plug-in modules. The main key design here is to allow adding additional features that are called plugins modules to our core system, providing extensibility, flexibility, and isolation to our application features. This will provide us with the ability to add, remove, and change the behaviour of the application with little or no effect on the core system or other plug-in modules making our code very modular and extensible .
plugin2

The Core System

The core system defines how it operates and the basic business logic. It can be understood as the workflow, such as how the data flow inside the application, but, the steps involved inside that workflow is up to the plugin(s). Hence, all extending plugins will follow that generic flow providing their customised implementation, but not changing the core business logic or the application's workflow.

In addition, it also contains the common code being used (or has to be used) by multiple plugins as a way to get rid of duplicate and boilerplate code, and have one single structure.

The Plug-in Modules

On the other hand, plug-ins are stand-alone, independent components that contain, additional features, and custom code that is intended to enhance or extend the core system. The plugins however, must follow a particular set of standards or a framework imposed by the core system so that the core system and plugin must communicate effectively. A real world example would be a car engine - only certain car engines ("plugins") would fit into a Toyota Prius as they follow the specifications of the chassis/car ("core system")

The independence of each plugin is the best approach to take. It is not advisable to have plugins talk to each other, unless, the core system facilitates that communication in a standardized way so that independent plugins can talk to each other. Either way, it is simpler to keep the communication and the dependency between plug-ins as minimal as possible.

Building a Core System

As mentioned before, we will have a core system and zero or more plugins which will add features to our system, so, first of all, we are going to build our core system (we will call this file core.py) to have the basis in which our plugins are going to work. To get started we are going to create a class called "MyApplication" with a run() method which prints our workflow

#core.py
class MyApplication:
    def __init__(self, plugins:list=[]):
        pass
    # This method will print the workflow of our application
    def run(self):
        print("Starting my application")
        print("-" * 10)
        print("This is my core system")
        print("-" * 10)
        print("Ending my application")
        print()
Enter fullscreen mode Exit fullscreen mode

Now we are going to create the main file, which will import our application and execute the run() method

#main.py
# This is a main file which will initialise and execute the run method of our application

# Importing our application file
from core import MyApplication

if __name__ == "__main__":
    # Initialising our application
    app = MyApplication() 
    # Running our application 
    app.run()  
Enter fullscreen mode Exit fullscreen mode

And finally, we are run our main file which result is the following:
Screenshot-2020-12-28-145146

Once that we have a simple application which prints it's own workflow, we are going to enhance it so we can have an application which supports plugins, in order to perform this, we are going to modify the init() and run() methods.

The importlib package

In order to achieve our next goal, we are going to use the importlib which provide us with the power of implement the import statement in our init() method so we are going to be able to dynamically import as many packages as needed. It's these packages that will form our plugins

#core.py
import importlib

class MyApplication:
    # We are going to receive a list of plugins as parameter
    def __init__(self, plugins:list=[]):
        # Checking if plugin were sent
        if plugins != []:
            # create a list of plugins
            self._plugins = [
                # Import the module and initialise it at the same time
                importlib.import_module(plugin,".").Plugin() for plugin in plugins
            ]
        else:
            # If no plugin were set we use our default
            self._plugins = [importlib.import_module('default',".") .Plugin()]


    def run(self):
        print("Starting my application")
        print("-" * 10)
        print("This is my core system")

        # We is were magic happens, and all the plugins are going to be printed
        for plugin in self._plugins:
            print(plugin)

        print("-" * 10)
        print("Ending my application")
        print() 
Enter fullscreen mode Exit fullscreen mode

The key line is "importlib.import_module" which imports the package specified in the first string variable with a ".py" extension under the current directory (specified by "." second argument). So for example, the file "default.py" which is present in the same directory would be imported by calling: importlib.import_module('default', '.')

The second thing to note is that we have ".Plugin()" appended to the importlib statement: importlib.import_module(plugin,".").Plugin()

This (specifically the trailing brackets Plugin() is present) to create an instance of the class and store it into _plugins internal variable.

We are now ready to create our first plugin, the default one, if we run the code at this moment it is going to raise a ModuleNotFoundError exception due to we have not created our plugin yet. So let's do it!.

Creating default plugin

Keep in mind that we are going to call all plugins in the same way, so files have to be named as carefully, in this sample, we are going to create our "default" plugin, so, first of all, we create a new file called "default.py" within the same folder than our main.py and core.py.

Once we have a file we are going to create a class called "Plugin", which contains a method called process. This can also be a static method in case you want to call the method without instantiating the calls. It's important that any new plugin class is named the same so that these can be called dynamically

#deafult.py
# Define our default class 
class Plugin:
    # Define static method, so no self parameter 
    def process(self, num1,num2):
        # Some prints to identify which plugin is been used
        print("This is my default plugin") 
        print(f"Numbers are {num1} and {num2}") 
Enter fullscreen mode Exit fullscreen mode

At this moment we can run our main.py file which will print only the plugin name. We should not get any error due to we have created our default.py plugin. This will print out the module (from the print statement under the MyApplicaiton.run() module) object itself to show that we have successfully imported out the plugin

Screenshot-2020-12-28-145146-1

Let's now modify just one line in our core.py file so we call the process() method instead of printing the module object

#core.py
import importlib

class MyApplication:
    # We are going to receive a list of plugins as parameter
    def __init__(self, plugins:list=[]):
        # Checking if plugin were sent
        if plugins != []:
            # create a list of plugins
            self._plugins = [
                importlib.import_module(plugin,".").Plugin() for plugin in plugins
            ]
        else:
            # If no plugin were set we use our default
            self._plugins = [importlib.import_module('default',".").Plugin()]


    def run(self):
        print("Starting my application")
        print("-" * 10)
        print("This is my core system")

        # Modified for in order to call process method
        for plugin in self._plugins:
            plugin.process(5,3)

        print("-" * 10)
        print("Ending my application")
        print() 
Enter fullscreen mode Exit fullscreen mode

Output as follows:

# Output
$ py main.py 
 Starting my application
 This is my core system   
 This is my default plugin
 Numbers are 5 and 3      
 Ending my application  

Enter fullscreen mode Exit fullscreen mode

We have successfully created our first plugin, and it is up and running. You can see the statement "This is my default plugin" which comes from the plugin default.py rather than the main.py program.

Next Steps

Using this framework, you can easily extend this to add more plugins easily. The key is that you don't need to change the core.py nor the main.py which helps to keep your code clean but at the same time help to extend your application.

To see a more in depth version of this article and more examples, you can see the full article at PythonHowToProgram.com

Top comments (1)

Collapse
 
fitoprincipe profile image
Rodrigo E. Principe

Thank you @charlesw001 ! I will try it. Can I make a GitHub repo with it?