The advantage of dynamically typed language is that it's much easier to write some code that dynamically loads another code and uses it. The downside of lack of types is that it's harder to enforce the contract on the loaded code. There are ways around that and this post is about that. How to write a simple plugin system in your application, shown on an example.
Then in plugins/ python package(which means that there is __init__.py file inside) I wrote some example plugins like the time one you saw above
As simple as that, the last missing piece is loading all the modules in plugins/ directory, for that I needed to bind all of the files inside plugins/ to plugins module. So __init__.py contains:
MessengerAssistant
I just wrote a simple plugin driven Messenger Assistant. It's a bot that listens to your messages when you chat on Messenger and can execute commands based on that. It uses fbchat package from pypi. The simplest example would be:
- Command time, which gives prints to us current time.
In prototype version, it was just big if/elif statement to decide if the message is a command and if we need to reply to it. I wanted to do something more powerful and modular.
Plugin driven MessengerAssistant
I started off by doing a base for plugin, some template that every plugin has to follow to be compatible with my bot. If it was C# or Java, we would use interfaces which would enforce existence of some methods that we could later use. Python does not have an idea of interfaces, but it does provide module called abc(Abstract Base Classes).
Our AbstractPluginBase could look like this:
import abc class AbstractPluginBase: __metaclass__ = abc.ABCMeta @abc.abstractmethod def __init__(self): pass @abc.abstractmethod def check_pattern(self, message): # Check if we should handle this message return False @abc.abstractmethod def handle_message(self, message): # Handle the message return ""
abc works by using metaclass, class that sole purpose is to create classes(don't mix that with instances of classes). This use of abc works like interfaces, classes dervied from AbstractPluginBase must implement those three methods(__init__ exists by default so we won't see it implemented in concrete plugins, I added it so it is impossible to change initialization, which will be helpful later on).
Then in plugins/ python package(which means that there is __init__.py file inside) I wrote some example plugins like the time one you saw above
import time from plugin_base import AbstractPluginBase class TimePlugin(AbstractPluginBase): def check_pattern(self, message): if message == "time": return True else: return False def handle_message(self, message): return time.ctime()
As simple as that, the last missing piece is loading all the modules in plugins/ directory, for that I needed to bind all of the files inside plugins/ to plugins module. So __init__.py contains:
import glob from os.path import dirname, basename, isfile modules = glob.glob(dirname(__file__) + "/*.py") __all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
Iterating over all files in directory, adding them to the package.
At this points we have plugins package that we can load from, last thing to do is to load all of the modules and get instances of them, in doing that we have to remember to only load subclasses of AbstractPluginBase, cause some plugins can use other classes for their implementations.
We had to use some reflection here, to check types of members of modules, so that we only get classes that satisfy three conditions: object is a class, object is a subclass of AbstractPluginBase and object is not AbstractPluginBase itself.
Then if we want to use the plugins, we just iterate over them and check if given plugin is able to handle given message.
At this points we have plugins package that we can load from, last thing to do is to load all of the modules and get instances of them, in doing that we have to remember to only load subclasses of AbstractPluginBase, cause some plugins can use other classes for their implementations.
import inspect import importlib import plugins class PluginLoader: @staticmethod def get_all_plugins(): plugins_classes = [] # Load all modules for module in [importlib.import_module("plugins." + x) for x in plugins.__all__]: # Get only subclasses of AbstractPluginBase plugins_classes += inspect.getmembers(module, PluginLoader.is_plugin) # Return instances of the classes return [(x[0], x[1]()) for x in plugins_classes] @staticmethod def is_plugin(object): return inspect.isclass(object) and issubclass(object, AbstractPluginBase) and object is not AbstractPluginBase
We had to use some reflection here, to check types of members of modules, so that we only get classes that satisfy three conditions: object is a class, object is a subclass of AbstractPluginBase and object is not AbstractPluginBase itself.
Then if we want to use the plugins, we just iterate over them and check if given plugin is able to handle given message.
for plugin in self.plugins: name, plugin_inst = plugin if plugin_inst.check_pattern(message): if self.debug: print("%s is handling the message" % name) output = plugin_inst.handle_message(message) self.send(send_id, output, is_user=is_user)
It wasn't so hard, was it? Python is really powerful when it comes to reflection and inspecting itself at runtime which gives us a lot of control when we know what we are doing.
I added some plugins like getting current value of my cryptocurrencies if I were to sell them at the current price at the exchange.
Full source code: https://github.com/hub2/Messenger-Assistant
I added some plugins like getting current value of my cryptocurrencies if I were to sell them at the current price at the exchange.
Full source code: https://github.com/hub2/Messenger-Assistant