Skip to main content

A lightweight library for writing modular code

Project description

Latest PyPI Version License Supported Python Versions Format Downloads

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.

Example

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.

Advanced use

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):
    ...

Installation

$ 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
$ 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.

License

modulable is distributed under the MIT license.

Project details


Release history Release notifications | RSS feed

This version

1.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

modulable-1.0.zip (11.0 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page