5.4 Example: Write your own handler class

We have seen how to use the handlers classes available, now we are going to learn how create our own handler classes.

The explanation will be driven by an example: we are going to write a task tracker. The code can be found in the directory examples/handlers.

5.4.1 Functional scope

Lets start by defining the functional scope of our task tracker. It is going to be very simple, it will be a collection of tasks where every task will have three fields:

The task tracker will provide an API to manipulate the collection of tasks: create a new task, see either the open or the closed tasks, and close a task.

5.4.2 The file format

Now that we know what we want to do, we have to decide where and how the information will be stored.

We will keep the tasks in a single text file, with a format somewhat similar to the one used by the standards vCard and iCal, for example:

    title:Re-write the chapter about writing handler classes.
    description:A new chapter that explains how to write file
     handler classes must be written, it should go immediately
     after the chapter that introduces file handlers.
    state:closed

    title:Finish the chapter about folder handlers.
    description:The chapter about folder handlers needs much
     more work.  For example the skeleton of folder handlers
     must be explained.
    state:open

Each task is separated from the next one by a blank line. Every field starts by the field name followed by the field value, both separated by a colon. If a field value is very long it can be written in multiple lines, where the second and next lines start by a space.

This very same file can be found in the examples directory with the itools.tt name. Using our own filename extension (tt) will prove useful, as we will see later.

5.4.3 De-serialization

The first draft of our handler class will be able to load (de-serialize) the resource into a data structure on memory.

    from itools.handlers import TextFile


    class Task(object):
        def __init__(self, title, description, state='open'):
            self.title = title
            self.description = description
            self.state = state


    class TaskTracker(TextFile):

        def _load_state_from_file(self, file):
            # Split the raw data in lines.
            lines = file.readlines()
            # Append None to signal the end of the data.
            lines.append(None)

            # Initialize the internal data structure
            self.tasks = []
            # Parse and load the tasks
            fields = {}
            for line in lines:
                if line is None or line.strip() == '':
                    if fields:
                        task = Task(fields['title'],
                                    fields['description'],
                                    fields['state'])
                        self.tasks.append(task)
                        fields = {}
                else:
                    if line.startswith(' '):
                        fields[field_name] += line.rstrip()
                    else:
                        field_name, field_value = line.split(':', 1)
                        fields[field_name] = field_value.rstrip()

First, our handler class TaskTracker inherits from the handler class TextFile, because it is intended to manage a text file.

The method _load_state_from_file is the one to implement to parse and load a new file format. It is responsible to de-serialize the resource and build a data structure on memory that represents it.

Lets try the code:

    >>> from pprint import pprint
    >>> from textwrap import fill
    >>> from tracker import TaskTracker
    >>>
    >>> task_tracker = TaskTracker('itools.tt')
    >>>
    >>> pprint(task_tracker.tasks)
    [<tracker.Task object at 0xb7aebd4c>,
     <tracker.Task object at 0xb7aebe6c>]
    >>>
    >>> task = task_tracker.tasks[0]
    >>> print task.title
    Re-write the chapter about writing handler classes.

    >>> print fill(task.description, width=60)
    A new chapter that explains how to write file handler
    classes must be written, it should go immediately after the
    chapter  that introduces file handlers.
    >>> print task.state
    closed

5.4.4 Serialization

Now we are going to write the other half, the serialization process, just adding the to_str method to the TaskTracker class:

        def to_str(self, encoding='utf-8'):
            lines = []
            for task in self.tasks:
                lines.append('title:%s' % task.title)
                description = 'description:%s' % task.description
                description = wrap(description)
                lines.append(description[0])
                for line in description[1:]:
                    lines.append(' %s' % line)
                lines.append('state:%s' % task.state)
                lines.append('')
            return '\n'.join(lines)

Lets try our new code:

    >>> print task_tracker.to_str()
    title:Re-write the chapter about writing handler classes.
    description:A new chapter that explains how to write file handler
     classes must be written, it should go immediately after the chapter
     that introduces file handlers.
    state:closed

    title:Finish the chapter about folder handlers.
    description:The chapter about folder handlers needs much more work.
     For example the skeleton of folder handlers must be explained.
    state:open

5.4.5 The API

Now it is time to write the API to manage the tasks, here is an excerpt:

    def add_task(self, title, description):
        task = Task(title, description)
        self.tasks.append(task)


    def show_open_tasks(self):
        for id, task in enumerate(self.tasks):
            if task.state == 'open':
                print 'Task #%d: %s' % (id, task.title)
                print
                print fill(task.description)
                print
                print


    def close_task(self, id):
        task = self.tasks[id]
        task.state = u'closed'

The first method, add_task creates a new task, whose state will be open. The method show_open_tasks prints the list of open tasks with a human readable format (we could write a method that returns HTML instead, to use our task tracker on the web). Finally, the method close_task closes the task.

5.4.6 Init new handlers

To illustrate the new method we are going to initialize the handler with a dummy task:

    def new(self):
        self.tasks = []
        task = Task('Read the docs!',
            'Read the itools documentation, it is so gooood.',
            'open')
        self.tasks.append(task)

To exercise the whole thing we are going to create a new task tracker, we will close the first task, add a new one, and look at what we have.

    >>> from tracker import TaskTracker
    >>>
    >>> task_tracker = TaskTracker()
    >>> task_tracker.show_open_tasks()
    Task #0: Read the docs!

    Read the itools documentation, it is so gooood.


    >>> task_tracker.close_task(0)
    >>> task_tracker.add_task('Join itools!',
    ...   'Subscribe to the itools mailing list.')
    >>> task_tracker.show_open_tasks()
    Task #1: Join itools!

    Subscribe to the itools mailing list.

Now, don’t forget to save the task tracker in the file system, so you can come back to it later:

    >>> from itools.handlers import Database
    >>>
    >>> db = Database()
    >>> db.set_handler('/tmp/test_tracker.tt', task_tracker)
    >>> db.save_changes()

5.4.7 Register

However:

    >>> from itools.handlers import get_handler
    >>>
    >>> task_tracker = get_handler('/tmp/test_tracker.tt')
    >>> print task_tracker
    <itools.handlers.text.TextFile object at 0xb7c00f0c>

It would be nice if the code above worked. To achieve it we will associate the new mimetype text/x-task-tracker to the file extension tt, we will tell our handler class is able to manage that mimetype with the variable class class_mimetypes, and we will register our handler class to its parent:

    from mimetypes import add_type
    from itools.handlers import register_handler_class

    add_type('text/x-task-tracker', '.tt')

    class TaskTracker(TextFile):

        class_mimetypes = ['text/x-task-tracker']
        [...]


    register_handler_class(TaskTracker)

And voilà:

    >>> task_tracker = get_handler('/tmp/test_tracker.tt')
    >>> print task_tracker
    <tracker.TaskTracker object at 0xb7af084c>

The full code can be found in examples/handlers/tracker.py.