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 .
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()
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()
And finally, we are run our main file which result is the following:
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()
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}")
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
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()
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
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)
Thank you @charlesw001 ! I will try it. Can I make a GitHub repo with it?