A dependency injection pattern for mod_python
Dependency Injection is a software design pattern well-known to software engineers, made more popular in recent years by tools such as Spring. I won't try to explain the pattern in it's entirety, but the basic idea is that rather than an object finding dependencies itself (the Service Locator pattern), the object is created by some sort of container and given the dependencies by the container. This makes it much easier to develop to an interface rather than a specific implementation, and to unit test because the class under test can be instantiated and given mock dependencies or stubs.
For one of our recent projects, we had to develop some special handlers at the Apache web server level. Our options were to use Python or Perl, and we went with Python, and the Apache/Python integration mod_python. Python is a nice dynamic programming language that is very easy to read and understand, and mod_python provides a great integration into the powerful web server. We soon realized the components we were developing were tightly-coupled and untestable. Consider this simple Apache configuration and Python handler:
#/etc/httpd/conf.d/helloworld.conf
<Location /helloworld/view/>
SetHandler mod_python
PythonPath "sys.path+['/usr/lib/helloworld/']"
PythonHandler users
</Location>
#/usr/lib/helloworld/users.py
...
userService = DatabaseBackedUserService(pgdb.connect(host='localhost'))
def handler(req):
name = req.uri.split('/').pop()
user = userService.getUser(name)
req.write("User Info: "+str(usr.getInfo()))
return apache.OK
class DatabaseBackedUserService:
def __init__(self, dbConnection):
self.dbConnection = dbConnection
def getUser(self, username):
...
return user
Although this works, and is easy to get going, consider how difficult it is to unit test the web handler. Because it is tightly-coupled with DatabaseBackedUserService, every time you run a unit test you will need to provide a database with a specific set of data, and you end up testing more than just your own code.
A better mod_python solution
With a dependency injection pattern, we can loosely-couple the components. You could use a framework such as Spring Python, but you can also cook up a simple solution yourself. The secret is to use mod_python's option for specifying a callable object or function using the characters "::", and a front controller-style design pattern. This is how I did it:
#/etc/httpd/conf.d/helloworld.conf
<Location /helloworld/view/>
SetHandler mod_python
PythonPath "sys.path+['/usr/lib/helloworld/']"
PythonHandler users.context::viewHandler
</Location>
#/usr/lib/helloworld/users/context.py
...
userService = DatabaseBackedUserService(pgdb.connect(host='localhost'))
viewHandler = ViewHandler(userService)
#/usr/lib/helloworld/users/web.py
...
class ViewHandler:
def __init__(self, userService):
self.userService = userService
def __call__(self, req):
name = req.uri.split('/').pop()
user = self.userService.getUser(name)
req.write("User Info: "+str(user.getInfo()))
return apache.OK
#/usr/lib/helloworld/users/service.py
...
class DatabaseBackedUserService:
def __init__(self, dbConnection):
self.dbConnection = dbConnection
def getUser(self, username):
...
return user
And what would be testable code, without a unit test?
#userstest.py
...
class ViewHandlerTest(unittest.TestCase):
def testURI(self):
request = StubRequest()
request.uri = "/helloworld/view/jsmith"
userService = StubUserService(getUserReturnValue=User("John Smith", "jsmith@example.com))
viewHandler = ViewHandler(userService)
val = viewHandler(request)
self.assertEquals(apache.OK, val)
self.assertEquals('jsmith', userService.getUser.called_with[0])
In the above code, I used some stub objects (code not shown). I would actually recommend using a mocking library such as the creatively-named Mock. Also, if you are interested in test coverage, you should check out coverage.py.
There you have it, a loosely-coupled web application. Now, as you flesh out your code, you can keep up with unit tests. If you want to provide a pluggable UserService (e.g., SOAPBackedUserService), you can easily plug it in by modifying your context, without modifying the web code.

