Hitting the Bottle

I find it amazing that I’ve posted pretty much nothing about coding in the two years I’ve actually been doing so as part of my day job.

Then again, my focus on coding is on doing minimalist, almost Zen-like hacks rather than delving into world-shattering insights regarding the deep, ingrained dogmas of software development. I don’t have much to add to the craft per se other than trying to make stuff that is as painless and long-lasting as possible, and that just can’t compete with all the noise.

But I’ve been using Bottle for a year now in various projects, and it’s been a smooth ride that is worth mentioning - the learning curve is flat as a lake bed, it gives me zero deployment headaches, and when coupled with gevent it becomes a veritable powerhouse for tiny application servers.

And, with simple, methodical arrangement of modules, you can do pretty complex (and yet easy to maintain) apps with it. It’s not Django, but that can be an advantage when you want a simple and straightforward app.

So now that we’re using it to implement RESTful APIs for a few things, I’ve decided to tackle the issue of having up-to-date, live documentation for those APIs.

The obvious way is, of course, to use docstrings. But since pydoc generates HTML that is deeply rooted in the ’90s, I decided to look inside inspect and bottle.py to craft a simple, straightforward function to dump all the docstrings associated with active Bottle routes and group them by module:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os, sys, logging, inspect

log = logging.getLogger()

from bottle import app

def docs():
    """
    Gather all docstrings related to routes and return them grouped by module
    """
    modules = {}
    for route in app().routes:
        doc = inspect.getdoc(route.callback) or inspect.getcomments(route.callback)
        if not doc:
            doc = ''
        module = inspect.getmodule(route.callback).__name__
        item = {
            # GET, PUT, POST, DELETE, etc.
            'method': route.method,
            # The full route, including things like '/gadget/<id>/foobar'
            'route': route.rule,
            # The name of the function inside the @route/@get/etc. decorator
            # I recommend you use .title().replace('_',' ') to prettify this
            'function': route.callback.__name__,
            # Handy to have this inside items as well
            'module': module,
            # Docstring with leading spacing removed - markdown() it.
            'doc': inspect.cleandoc(doc)
        }
        if not module in modules:
            modules[module] = []
        modules[module].append(item)
    return modules

You can then use inside your docstrings and magically end up with neat, effortlessly maintained documentation that looks like this:

All Gadgets

GET /api/v1/gadgets

Lists all the gadgets available. Takes the following GET arguments:

  • offset (int, defaults to 0)
  • limit (int, defaults to 25)

Returns a JSON structure of the form:

{
    "data": [array of gadgets, up to limit],
    "next": "pre-formatted url for next page of results"
}

And, of course, if you set up a view to render this inside your Bottle app, you’ll be able to deploy your app and your developer documentation in one fell swoop.

Simple, neat, and effective.