Siv Scripts

Solving Problems Using Code

Thu 06 September 2018

Implementing a Plugin Architecture in a Python Application

Posted by Aly Sivji in Quick Hits   

In July, I released my first open source project. It's an apispec plugin that generates OpenAPI Specification (aka Swagger docs) for Falcon web applications.

Apispec's design made it easy to extend core functionality for a specific use case. I extended the apispec.BasePlugin class, overrode a couple of methods, and I was done. Had to dig into apispec internals to figure out how things were wired together, but it was easy to build upon what was already there

This got me thinking about how to implement a plugin system in one of my own applications. Creating a plugin architecture? It sounded hard. An advanced computer science concept, if you will.

I mulled over a few different implementations based on software I had previously used. I realized this wasn't a difficult problem. Like everything else in programming, once we deconstruct the problem into smaller chunks, we can reason about implementation details clearly.

We assume things are more difficult than they appear. This is especially true for problems we have not seen before. Take a step back. Breathe. Break the problem down into smaller pieces. You got this.

Motivational You Got This poster with dog

In this Quick Hit, we will walk through the implementation of a simple plugin system.


Background

A plugin is a software component that adds a specific feature to an existing computer program. When a program supports plug-ins, it enables customization (Wikipedia)

There are many benefits to building apps with a plugin framework:

  • 3rd party developers can create and extend upon your app
  • new features are easier to developer
  • your application becomes smaller and easier to understand

Sample Application Flow

We have a program that starts, does a few things, and exits.

Program Flow

Plugin Architecture Workflow

We refactored our Business Logic into a Plugin Framework that can run registered plugins. The plugins will need to meet the specifications defined by our framework in order to run.

Program flow with Plugin


Toy Example

Let's implement a toy example where we have a plugin framework to print to the console. Our project will have the following structure:

.
├── internal.py  # internal business logic
├── external.py  # contains user-created plugins
└── main.py      # initialize and run application

Internal

This module contains the application class and an internal plugin.

# internal.py

class InternalPrinter:
    """Internal business logic"""
    def process(self):
        print("Internal Hello")


class MyApplication:
    """First attempt at a plugin system"""
    def __init__(self, *, plugins: list=list()):
        self.internal_modules = [InternalPrinter()]
        self._plugins = plugins

    def run(self):
        print("Starting program")
        print("-" * 79)

        modules_to_execute = self.internal_modules + self._plugins
        for module in modules_to_execute:
            module.process()

        print("-" * 79)
        print("Program done")
        print()

External

# external.py

class HelloWorldPrinter:
    def process(self):
        print("Hello World")


class AlohaWorldPrinter:
    def process(self):
        print("Aloha World")

Main

In this module, we run instances of our application with the external plugins we want to enable.

# main.py

from internal import MyApplication
from external import HelloWorldPrinter, AlohaWorldPrinter


if __name__ == "__main__":
    # Run with one plugin
    app = MyApplication(plugins=[HelloWorldPrinter()])
    app.run()

    # Run with another plugin
    app = MyApplication(plugins=[AlohaWorldPrinter()])
    app.run()

    # Run with both plugins
    app = MyApplication(plugins=[HelloWorldPrinter(), AlohaWorldPrinter()])
    app.run()

Discussion

The Application's plugin framework works for both internal and external plugins. We define internal plugins in the application code. External plugins are initialized and passed into the application at runtime.

Each plugin inherits from the base Python object and has a process() method. Nothing complex, want to keep this example as simple as possible.

We can run our plugins by calling the application's run() method. This method loops over all the plugins and calls each instance's process() function. As we see from the output above, the plugins are executed in the same order as the list.

Running Toy Application

$ python main.py
Starting program
-------------------------------------------------------------------------------
Internal Hello
Hello World
-------------------------------------------------------------------------------
Program done

Starting program
-------------------------------------------------------------------------------
Internal Hello
Aloha World
-------------------------------------------------------------------------------
Program done

Starting program
-------------------------------------------------------------------------------
Internal Hello
Hello World
Aloha World
-------------------------------------------------------------------------------
Program done

Real World Example

This pattern can be used in conjunction with the Adapter pattern to simplify application development.

Let's say we have a large number of external clients we want to interface with. Each API is different, but the tasks we need to perform for each client are the same.

One possible implementation of this is to write an adapter around each of the client APIs, resulting in a common interface. Next, we can leverage the plugin framework to solve our business problem, and then we can use plugins to make it work for all of our clients.

This is a very high level description of the solution. I leave implementation as an exercise to the reader.


Additional Resources


 
    
 
 

Comments