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 arequirement to have you App reused and improved by the community.
The RapidSMS community follows thePEP8 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:
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 TrueRegular comments are very useful in RapidSMS Apps:
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 TrueInline 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 ofmodule,class,function ormethod 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 callhelp() 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 refrom datetime import datetimeimport rapidsmsfrom django.utils.translation import ugettext_lazy as _from babel.dates import format_datetimefrom 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 fdef 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 TrueEven 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 (nammedREADME at the root of your project by convention) describing your app.
It should contain:
Should your application be complex, please, also create adocs/ folder and add any further documentation to it.
It isvery 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 andbad documentation is worse than no documentation.This is a habit you want to learn.