Extensible modules

Most of the time, the functionality provided by a module is known in advance. The module's source code implements a well-defined set of behavior, and that is all the module does. In some situations, however, you may need a module where the behavior of the module is not completely defined at the time you write it. Other parts of your system can extend the behavior of the module in various ways. Modules that are designed to be extended are called extensible modules.

One of the great things about Python is that it is a dynamic language. You don't need to define and compile all your code before it will run. This makes it easy to create extensible modules using Python.

In this section, we will look at three different ways in which a module can be made extensible: through the use of dynamic imports, by writing plugins, and using hooks.

Dynamic imports

In the previous chapter, we created a module called renderers.py which selected an appropriate renderer module to draw a chart element using a given output format. The following is an abbreviated copy of this module's source code:

from .png import title  as title_png
from .png import x_axis as x_axis_png

from .pdf import title  as title_pdf
from .pdf import x_axis as x_axis_pdf

renderers = {
    'png' : {
        'title'  : title_png,
        'x_axis' : x_axis_png,
    },
    'pdf' : {
        'title'  : title_pdf,
        'x_axis' : x_axis_pdf,
    }
}

def draw(format, element, chart, output):
    renderers[format][element].draw(chart, output)

This module is interesting because it implements, in a limited way, the concept of extensibility. Notice that the renderer.draw() function calls a draw() function within another module to do the actual work; which module is used depends on the desired chart format and the element to be drawn.

This module is not truly extensible because the list of possible modules is determined by the import statements at the top of the module. However, it is possible to turn this into a fully extensible module by making use of importlib. This is a module in the Python Standard Library that gives a developer access to the internal mechanism used to import modules; using importlib, you can import modules dynamically.

To understand how this works, let's look at an example. Create a new directory to hold your source code, and in this directory, create a new module named module_a.py. Enter the following code into this module:

def say_hello():
    print("Hello from module_a")

Now, create a copy of this module, named module_b.py, and edit the say_hello() function to print Hello from module_b. Then, repeat the process to create module_c.py.

We now have three modules that all implement a function named say_hello(). Now, create another Python source file in the same directory, and name it load_module.py. Then, enter the following into this file:

import importlib

module_name = input("Load module: ")
if module_name != "":
    module = importlib.import_module(module_name)
    module.say_hello()

This program prompts the user to enter a string using the input() statement. We then call importlib.import_module() to import the module with that name, and call that module's say_hello() function.

Try running this program, and when prompted, type in module_a. You should see the following message displayed:

Hello from module_a

Try repeating this process with the other modules. If you type in the name of a non-existent module, you'll get an ImportError.

Of course, importlib isn't limited to importing modules in the same directory as the current module; you can include package names if you want. For example:

module = importlib.import_module("package.sub_package.module")

Using importlib, you can import a module dynamically—you don't need to know the name of the module at the time you write your program. We could use this to rewrite the renderer.py module from the previous chapter to make it fully extensible:

from importlib import import_module

def draw(format, element, chart, output):
    renderer = import_module("{}.{}.{}".format(__package__,
                                               format,
                                               element))
    renderer.draw(chart, output)

Note

Notice the use of the special __package__ variable. This holds the name of the package enclosing the current module; using this allows us to import a module relative to the package that the renderer.py module is part of.

The great thing about dynamic imports is that you don't need to know what all the modules are at the time you create your program. Using the renderer.py example, you could add new chart formats or elements by creating new renderer modules, and the system will import them when requested, without having to make any changes at all to your renderer.py module.

Plugins

Plugins are modules that the user (or another developer) writes and "plugs in" to your program. Plugins are popular in many large systems such as WordPress, JQuery, Google Chrome, and Adobe Photoshop. Plugins are used to extend the functionality of an existing program.

In Python, it is easy to implement plugins using the same dynamic import mechanism we discussed in the previous section. The only difference is that instead of importing modules that are already part of your program's source code, you set up a separate directory where the user can place the plugins they want to add to your program. This could be as simple as creating a plugins directory at the top level of your program, or you could store your plugins in a directory outside of your program's source code, and modify sys.path so that the Python interpreter can find the modules in that directory. Either way, your program will use importlib.import_module() to load the desired plugin, and then access the functions and other definitions within the plugin just like you would access functions and other definitions in any other Python module.

The sample code available for this chapter includes a simple plugin loader which shows how this mechanism works.

Hooks

A hook is a way of allowing external code to be called at particular points in your program. A hook is usually a function—your program checks to see if a hook function has been defined, and if so, it calls this function at an appropriate time.

Let's look at a concrete example. Imagine that you have a program that includes the ability to log a user in and out. Part of your program may include the following module, which we will call login_module.py:

cur_user = None

def login(username, password):
    if is_password_correct(username, password):
        cur_user = username
        return True
    else:
        return False

def logout():
    cur_user = None

Now, imagine that you want to add a hook that gets called whenever the user logs in. Adding this to your program would involve the following changes to this module:

cur_user = None
login_hook = None

def set_login_hook(hook):
    login_hook = hook

def login(username, password):
    if is_password_correct(username, password):
        cur_user = username
        if login_hook != None:
            login_hook(username)
        return True
    else:
        return False

def logout():
    cur_user = None

With this code in place, other parts of your system can hook into your login process by setting their own login hook function, which does something whenever the user logs in. For example:

def my_login_hook(username):
    if user_has_messages(username):
        show_messages(username)

login_module.set_login_hook(my_login_hook)

By implementing this login hook, you have extended the behavior of the login process without altering the login module itself.

There are a couple of things to be aware of with hooks:

  • Depending on the behavior you are implementing a hook for, the value returned by the hook function might be used to alter the behavior of your code. For example, if the login hook returned False, the user might be blocked from logging in. This doesn't apply to every hook, but it can be a very useful way of giving a hook function more control over what happens in your program.
  • In this example, we only allow a single hook function to be defined for each hook. Another way of implementing this would be to have a list of registered hook functions, and let your program add or remove hook functions as required. In this way, you could have several hook functions, which get called one after the other whenever something happens.

Hooks are an excellent way of adding specific points of extensibility to your modules. They are easy to implement and use, and unlike dynamic imports and plugins, they don't require you to put your code into a separate module. This means that hooks are an ideal way of extending your modules in a very fine-grained way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.31.165