RapidSMS Developers Guide/Coding standards and documentation
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
editThe 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
editRegular 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
editInline 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
editEven 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
editIt 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.