modular.py
is a small library that helps you write a modular and
maintainable codebase.
The library works with class: the loaded plug-ins are "injected" in
the given class. You can specify the way of injection, either by stacking
functions, with the modulable
decorator, by overloading the base method, with
the overridable
decorator, and finally with the alternative
decorator,
which runs every function until it finds one that doesn't raise an exception.
Those decorators conserve the original method's informations, such as name, module, docstring, and annotations.
Let's say you want to build a modular shell, where the users can implement their own commands and prompt for example.
from modular import *
class Shell(Modular, plugin_directory='plugins'):
This declares a modular class, whose plug-ins are in the plugins
directory
relative to the current working directory.
The library will load every plug-in (must be a .py file) in that directory when the class is instantiated.
It is convenient to declare a init
method, called within the real
__init__
constructor, to allow users to initialize their plug-in specific
attributes:
def __init__(self, *args, **kwds):
self.init(*args, **kwds)
@modulable
def init(self, *args, **kwds):
self.running = False
Here, we decorate the init
method with modulable
. That means every
plug-ins' implementations of the init
method will be executed with the given
arguments.
Next, we want a function that is executed between every command, and, say, a method that returns the shell prompt:
@modulable
def update(self):
pass
@overridable
def prompt(self):
return '> '
This time, we use the overridable
decorator. This decorator uses the last
implementation of prompt
loaded.
Lastly, we want a function that reacts on user input. What we want here is to
run every implementation until one works (i.e. doesn't raise an error). To do
that, you can use the alternative
decorator which takes an exception type
and calls every implementation one-by-one until one doesn't raise an error of
this type:
@alternative(ValueError)
def react(self, i):
if i:
print('Unrecognized command:', repr(i))
We provide a default case here, if there isn't any implementation working.
Finally, we define some non-modulable methods to make the whole thing works:
def run(self):
self.running = True
while self.running:
i = input(self.prompt())
self.react(i)
self.update()
For instance, our shell doesn't implement any plug-in. Just for the example,
we'll implement a quit
plug-in, which stops the shell when the user types in
quit
, a greet
plug-in, and finally we'll customize our prompt.
The implementation of the quit
command is pretty straight-forward:
def react(self, i):
if i == 'quit':
self.running = False
else:
raise ValueError
By raising ValueError
, we delegate the input processing to the next
implementation of react
.
The greet
plug-in does the same, with a bit more complex parsing:
def react(self, i):
lexemes = i.split()
try:
if lexemes[0] == 'greet':
print('Hey', lexemes[1], '!')
else:
raise ValueError
except IndexError:
raise ValueError
Finally, lets define a prompt that displays the command count:
def init(self, *args, **kwds):
self.command_count = 0
def update(self):
self.command_count += 1
def prompt(self):
return '[{}]: '.format(self.command_count)
The plug-ins must be contained in the specified plug-in directory in the class
declaration, here, plugins
. You should have a similar directory tree:
. ├── plugins │ ├── command_count_prompt.py │ ├── greet.py │ └── quit.py └── shell.py
To use this class, simply instantiate a Shell
object and call its run
method:
sh = Shell()
sh.run()
Here's what it does:
[0]: [1]: [2]: greet Jonathan Hey Jonathan ! [3]: [4]: [5]: unknown command Unrecognized command: 'unknown command' [6]: [7]: quit
You can see the complete code in the example directory.
You can temporarily load a plug-in with the plugin
context manager:
with Shell.plugin('greet'):
sh.run()
You can also check the loaded plug-ins by typing Shell.loaded_plugins
.
Finally, there is an optional virtual
keyword argument at class definition.
virtual
is set to False
by default, but if set to True
, the class
will not load the plug-ins automatically:
class AbstractShell(Modular, plugins='plugins', virtual=True):
...
- Via pip:
$ pip install modulable
And, if you're on Linux, and face a permission error, make sure to
run sudo
with the -H
option:
$ sudo -H pip install modulable
- Via git:
$ git clone http://github.com/felko/modulable.git
$ cd modulable
$ sudo -H python3.4 setup.py install
Or, if you're on Windows:
$ git clone http://github.com/felko/modulable.git
$ cd modulable
$ py -3.4 setup.py install
If you don't have git, you can download the zip file here.
- GitHub: http://github.com/felko/modulable
- Issue Tracker: http://github.com/feko/modulable/issues
- PyPI: http://pypi.python.org/pypi/modulable
- Download: http://pypi.python.org/pypi/modulable#downloads
modulable
is distributed under the MIT license.