I'm enjoying this series of articles; plugins are something I had been wondering how to do in Python. One thing though: in English désactiver is 'to deactivate' (not 'to desactivate'.)
(Note: the code indentation may appear to be wrong due to some blogspot's quirks...)
While plugins are a great way to extend the functionality of an application, sometimes it makes sense to limit the number of available features, based on a user's preference. For example, gedit, the official text editor of the Gnome environment, offers the possibility to activate or deactivate a plugin. [Image] In this post, using the class-based plugin approach, I will explain how to add the possibility to activate or deactivate a given plugin. Furthermore, I will show how to use this capability to dynamically load new plugins.
Starting from the beginning...
Our starting point will be the following modified core application (calculator.py): import re
from plugins.base import OPERATORS, init_plugins, activate, deactivate
class literal_token(object): def __init__(self, value): self.value = value def nud(self): return self.value
class end_token(object): lbp = 0
def tokenize(program): for number, operator in re.findall("\s*(?:(\d+)|(\*\*|.))", program): if number: yield literal_token(int(number)) elif operator in OPERATORS: yield OPERATORS[operator]() else: raise SyntaxError("unknown operator: %r" % operator) yield end_token()
def expression(rbp=0): global token t = token token = next() left = t.nud() while rbp < token.lbp: t = token token = next() left = t.led(left) return left
def calculate(program): global token, next next = tokenize(program).next token = next() return expression()
# "**" has not been activated at the start in base.py try: assert calculate("2**3") == 8 except SyntaxError: print "Correcting error..." activate("**") assert calculate("2*2**3") == 16
The new features are indicated by different colours. In blue, we have two new functions imported to either activate or deactivate a given plugin. When the application is started, exponentiation is disabled - this can only be seen by looking at the modified version of base.py. When a disabled plugin is called, a SyntaxError already present in the old version) is raised and we activate the plugin.
To make this possible, we need to modify base.py. Before showing the new version, here's the result of running the above code: Activating + Activating - Activating * Activating / Correcting error... Activating ** Deactivating + Activating + Done!
And here's the new version of base.py:
import os import sys
OPERATORS = {}
# We simulate a configuration file that would be based on a user's preference # as to which plugin should be activated by default # We will leave one symbol "**" out of the list as a test. preferences = ['+', '-', '*', '/']
# We also keep track of all available plugins, activated or not all_plugins = {}
class Plugin(object): '''base class for all plugins'''
def activate(self): '''activate a given plugin''' if self.symbol not in OPERATORS: print "Activating %s" % self.symbol OPERATORS[self.symbol] = self.__class__ if self.symbol not in all_plugins: all_plugins[self.symbol] = self.__class__
def deactivate(self): '''deactivate a given plugin''' print "Deactivating %s" % self.symbol if self.symbol in OPERATORS: del OPERATORS[self.symbol]
def activate(symbol): '''activate a given plugin based on its symbol''' if symbol in OPERATORS: return all_plugins[symbol]().activate()
def deactivate(symbol): '''deactivate a given plugin, based on its symbol''' if symbol not in OPERATORS: return all_plugins[symbol]().deactivate()
def find_plugins(expression): '''find all files in the plugin directory and imports them''' plugin_dir = os.path.dirname(os.path.realpath(__file__)) plugin_files = [x[:-3] for x in os.listdir(plugin_dir) if x.endswith(".py")] sys.path.insert(0, plugin_dir) for plugin in plugin_files: mod = __import__(plugin) mod.expression = expression
def register_plugins(): '''Register all class based plugins.
Uses the fact that a class knows about all of its subclasses to automatically initialize the relevant plugins ''' for plugin in Plugin.__subclasses__(): # only register plugins according to user's preferences if plugin.symbol in preferences: plugin().activate() else: # record its existence all_plugins[plugin.symbol] = plugin Changes from the old version are indicated in blue (with corresponding comments in green). Note that we did not change a single line of code for the actual plugins! We did use the same names (activate and deactivate) both for a function and a class method. This should probably be avoided in a larger application. In this example, the code is short enough that it should not create too much confusion. In a real application we would also give the possibility of changing the user's preferences, storing the information in some configuration file.
Dynamic activation
Now that we now how to activate and deactivate a plugin, it might be useful to consider dynamic activation of an external plugin, not located in the normal plugins directory. For example, consider the following plugin (located in op_3.py):
from plugins.base import Plugin
class operator_mod_token(Plugin): symbol = '%' lbp = 10 def nud(self): return expression(100) def led(self, left): return left % expression(10) This file is located in subdirectory "external" which is at the same level as "plugins" in our sample code. To invoke this plugin from our base application, we need to add the following code to calculator.py:
if __name__ == "__main__": #...
# Simulating dynamic external plugin initialization external_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'external') sys.path.insert(0, external_dir) mod = __import__('op_3') mod.expression = expression # register this plugin using our default method register_plugins() # Since it is not activated by default, we need to do it explictly activate('%') assert calculate("7%2") == 1
print "Done!" Note that we also need to import register_plugins() from base.py to make this work.
That's it! If you get the code from the py-fun repository, you can try it out yourself.
posted by André Roberge at 3:18 PM on Dec 20, 2008
"Plugins - part 5: Activation and Deactivation"
2 Comments -
I'm enjoying this series of articles; plugins are something I had been wondering how to do in Python. One thing though: in English désactiver is 'to deactivate' (not 'to desactivate'.)
6:10 PM
Thanks Matthew!
6:16 PM