Since this chapter has been about both documentation and testing, let's build a script that takes a set of modules and prints out a complete report, showing all documentation as well as running any given tests.
This is a valuable recipe, because it shows us how to use Python's APIs to harvest a code-driven runnable report. This means the documentation is accurate and up to date, reflecting the current state of our code.
In the following steps, we will write an application and some doctests. Then we will build a script to harvest a useful report.
recipe21_report.py
to contain the script that harvests our report.inspect
library as the basis for drilling down into a module from inspect
import
*__doc__
string or prints out no documentation found.def print_doc(name, item): if item.__doc__: print "Documentation for %s" % name print "-------------------------------" print item.__doc__ print "-------------------------------" else: print "Documentation for %s - None" % name
def print_docstrings(m, prefix=""): print_doc(prefix + "module %s" % m.__name__, m) for (name, value) in getmembers(m, isclass): if name == '__class__': continue print_docstrings(value, prefix=name + ".") for (name, value) in getmembers(m, ismethod): print_doc("%s%s()" % (prefix, name), value) for (name, value) in getmembers(m, isfunction): print_doc("%s%s()" % (prefix, name), value)
if __name__ == "__main__": import sys import doctest for arg in sys.argv[1:]: if arg.startswith("-"): continue print "===============================" print "== Processing module %s" % arg print "===============================" m = __import__(arg) print_docstrings(m) print "Running doctests for %s" % arg print "-------------------------------" doctest.testmod(m)
recipe21.py
to contain an application with tests that we will run the earlier script against.recipe21.py
, create a shopping cart app and fill it with docstrings
and doctests
.""" This is documentation for the entire recipe. With it, we can demonstrate usage of the code. >>> cart = ShoppingCart().add("tuna sandwich", 15.0) >>> len(cart) 1 >>> cart.item(1) 'tuna sandwich' >>> cart.price(1) 15.0 >>> print round(cart.total(9.25), 2) 16.39 """ class ShoppingCart(object): """ This object is used to store the goods. It conveniently calculates total cost including tax. """ def __init__(self): self.items = [] def add(self, item, price): "Add an item to the internal list." self.items.append(Item(item, price)) return self def item(self, index): "Look up the item. The cart is a 1-based index." return self.items[index-1].item def price(self, index): "Look up the price. The cart is a 1-based index." return self.items[index-1].price def total(self, sales_tax): "Add up all costs, and then apply a sales tax." sum_price = sum([item.price for item in self.items]) return sum_price*(1.0 + sales_tax/100.0) def __len__(self): "Support len(cart) operation." return len(self.items) class Item(object): def __init__(self, item, price): self.item = item self.price = price
-v
, and look at the screen's output.=============================== == Processing module recipe21 =============================== Documentation for module recipe21 ------------------------------- This is documentation for the this entire recipe. With it, we can demonstrate usage of the code. >>> cart = ShoppingCart().add("tuna sandwich", 15.0) >>> len(cart) 1 >>> cart.item(1) 'tuna sandwich' >>> cart.price(1) 15.0 >>> print round(cart.total(9.25), 2) 16.39 ------------------------------- Documentation for Item.module Item - None Documentation for Item.__init__() - None Documentation for ShoppingCart.module ShoppingCart ------------------------------- This object is used to store the goods. It conveniently calculates total cost including tax. … Running doctests for recipe21 ------------------------------- Trying: cart = ShoppingCart().add("tuna sandwich", 15.0) Expecting nothing ok Trying: len(cart) Expecting: 1 ok 5 tests in 10 items. 5 passed and 0 failed. Test passed.
This script is tiny, yet harvests a lot of useful information.
By using Python's standard inspect
module, we are able to drill down starting at the module level. The reflective way to look up a docstring is by accessing the __doc__
property of an object. This is contained in modules, classes, methods, and functions. They exist in other places, but we limited our focus for this recipe.
We ran it in verbose mode, to show that the tests were actually executed. We hand parsed the command-line options, but doctest
automatically looks for -v
to decide whether or not to turn on verbose output. To prevent our module processor from catching this and trying to process it as another module, we added a line to skip any -xyz
style flags.
if arg.startswith("-"): continue
We could spend more time enhancing this script. For example, we could dump this out with an HTML markup, making it viewable in a web browser. We could also find third party libraries to export it in other ways.
We could also work on refining where it looks for docstrings
and how it handles them. In our case, we just printed them to the screen. A more reusable approach would be to return some type of structure containing all the information. Then the caller can decide whether to print to screen, encode it in HTML, or generate a PDF document.
This isn't necessary, however, because this recipe's focus is on seeing how to mix these powerful out-of-the-box options which Python provides into a quick and useful tool.
18.224.54.168