RapidSMS Developers Guide/Coding standards and documentation

From Wikibooks, open books for an open world
< RapidSMS Developers Guide
Jump to: navigation, search

Documentation is important to every piece of code but even more for RapidSMS.

Because of the openness of RapidSMS and most of the Apps for it, it development focus and the community behind, there is a great need for reuse and improvements.

Proper documentation is a requirement to have you App reused and improved by the community.

Coding standards[edit]

The RapidSMS community follows the PEP8 coding standard. It's a convention of how to write code which will ensure readability and easiness of contribution.

The standard is well written, please go read it. Some highlights though:

  • Lines should not contain more than 79 characters
  • No new line at end of file
  • One space before and after operators
  • 2 lines separation before classes or functions

Also, along the PEP8 standard, RapidSMS expects each file to contains formatting and encoding comments after shebang. Your files should thus always start with the following:

#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8

Example of actual PEP8 compliant code:

def handle(self, message):

    if not re.match(r'^ping( +|$)', message.text.lower()):
        return False

    identifier_match = re.match(r'^ping (?P<identifier>.+).*$', \
                                message.text.lower(), re.U)
    if not identifier_match:
        identifier = False
    else:
        identifier = identifier_match.group('identifier')

    if self.disallow:
        return False

    if self.allow or \
        (self.allowed and self.allowed.count(message.peer) > 0) or \
        (self.func and self.func(message)):

        now = datetime.now()
        if identifier:
            message.respond(_(u"%(pingID)s on %(date)s") % \
                            {'pingID': identifier, \
                            'date': format_datetime(now, \
                            locale=self.locale)})
        else:
            message.respond(_(u"pong on %(date)s") % \
                            {'date': format_datetime(now, \
                            locale=self.locale)})
        return True

Comments[edit]

Regular comments are very useful in RapidSMS Apps:

  • helps beginners learn from example
  • allows other developers to read your code in english instead of code
  • will help yourself in the future when you'll have no idea why you wrote that line.
  • helps you construct a consistent program by forcing you to concisely describe what you wrote ; thus enlightening mistakes.

Above example actually have some comments:

def handle(self, message):

    # We only want to answer ping alone, or ping followed by a space
    # and other characters
    if not re.match(r'^ping( +|$)', message.text.lower()):
        return False

    identifier_match = re.match(r'^ping (?P<identifier>.+).*$', \
                                message.text.lower(), re.U)
    if not identifier_match:
        identifier = False
    else:
        identifier = identifier_match.group('identifier')

    # deny has higher priority
    if self.disallow:
        return False

    # allow or number in auth= or function returned True
    if self.allow or \
        (self.allowed and self.allowed.count(message.peer) > 0) or \
        (self.func and self.func(message)):

        now = datetime.now()
        if identifier:
            message.respond(_(u"%(pingID)s on %(date)s") % \
                            {'pingID': identifier, \
                            'date': format_datetime(now, \
                            locale=self.locale)})
        else:
            message.respond(_(u"pong on %(date)s") % \
                            {'date': format_datetime(now, \
                            locale=self.locale)})
        return True

Docstrings[edit]

Inline documentation is python feature that allows you to write some comments inside the code of your classes, functions and modules. Python will then automatically parse those comments and formats them into a nice documentation.

Example:

def handle(self, message):
    ''' check authorization and respond

    if auth contained deny string => return
    if auth contained allow string => answer
    if auth contained number and number is asking => reply
    if auth_func contained function and it returned True => reply
    else return'''

    # We only want to answer ping alone, or ping followed by a space
    # and other characters
    if not re.match(r'^ping( +|$)', message.text.lower()):
        return False

We added a multi-line comment (with triple-quotes) at the beginning of our method. Python understands that every multi-line comment at the beginning of module, class, function or method is a docstring.

That docstring can be only one line long (although it still needs to use triple-quotes).

In the example above, we first added a short description (this is a convention), then some more detailed information after one line break.

To access the documentation, simply start a Python shell and call help() on the target object.

./rapidsms shell
>> from apps.ping import app
>> help(app.App.handle)
Help on method handle in module apps.ping.app:

handle(self, message) unbound apps.ping.app.App method
    check authorization and respond
    
    if auth contained deny string => return
    if auth contained allow string => answer
    if auth contained number and number is asking => reply
    if auth_func contained function and it returned True => reply
    else return

This means that any developer can now access a well formatted documentation from the shell.

It is also used by external tools to generate standalone documentation in HTML or other.

Docstrings are required by the community in order for you app to be reused. Make sure you add docstrings to all your modules (files), classes, functions and methods.

Full example:

#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
# maintainer: rgaudin

''' Reply to `ping` messages to confirm system is up and running. '''

import re
from datetime import datetime

import rapidsms
from django.utils.translation import ugettext_lazy as _
from babel.dates import format_datetime
from bonjour.utils import *


def import_function(func):
    ''' import a function from full python path string

    returns function.'''Before
    if '.' not in func:
        f = eval(func)
    else:
        s = func.rsplit(".", 1)
        x = __import__(s[0], fromlist=[s[0]])
        f = getattr(x, s[1])
    return f


def parse_numbers(sauth):
    ''' transform a string of comma separated cell numbers into a list

    return array. '''
    nums = sauth.replace(" ", "").split(",")
    return [num for num in nums if num != ""]


class App (rapidsms.app.App):

    ''' Reply to `ping` messages to confirm system is up and running.

    One can specify a number or authentication function to
    limit users who can ping the system. '''

    def configure(self, auth_func=None, auth=None):
        ''' set up authentication mechanism
        configured from [ping] in rapidsms.ini '''

        # store locale
        self.locale = Bonjour.locale()

        # add custom function
        try:
            self.func = import_function(auth_func)
        except:
            self.func = None

        # add defined numbers to a list
        try:
            self.allowed = parse_numbers(auth)
        except:
            self.allowed = []

        # allow everybody trigger
        self.allow = auth in ('*', 'all', 'true', 'True')

        # deny everybody trigger
        self.disallow = auth in ('none', 'false', 'False')

    def handle(self, message):
        ''' check authorization and respond

        if auth contained deny string => return
        if auth contained allow string => answer
        if auth contained number and number is asking => reply
        if auth_func contained function and it returned True => reply
        else return'''

        # We only want to answer ping alone, or ping followed by a space
        # and other characters
        if not re.match(r'^ping( +|$)', message.text.lower()):
            return False

        identifier_match = re.match(r'^ping (?P<identifier>.+).*$', \
                                    message.text.lower(), re.U)
        if not identifier_match:
            identifier = False
        else:
            identifier = identifier_match.group('identifier')

        # deny has higher priority
        if self.disallow:
            return False

        # allow or number in auth= or function returned True
        if self.allow or \
            (self.allowed and message.peer in self.allowed) or \
            (self.func and self.func(message)):

            now = datetime.now()
            if identifier:
                message.respond(_(u"%(pingID)s on %(date)s") % \
                                {'pingID': identifier, \
                                'date': format_datetime(now, \
                                locale=self.locale)})
            else:
                message.respond(_(u"pong on %(date)s") % \
                                {'date': format_datetime(now, \
                                locale=self.locale)})
            return True

Documentation[edit]

Even if your code is well written and correctly commented, no one wants to spend hours looking at source files just to check if the feature he's looking for exist.

That's why it is important that you create at least one file (nammed README at the root of your project by convention) describing your app.

It should contain:

  • Name of App
  • Your name and contact
  • Description of what it does
  • What dependencies it has
  • How to install/use it.

Should your application be complex, please, also create a docs/ folder and add any further documentation to it.

Important Note[edit]

It is very important that you write those comments and docstring as you write the code because if you don't, it will result in errors in this documentation and bad documentation is worse than no documentation. This is a habit you want to learn.

Internationalization · Customizing Admin U.I