POSTS

Packaging reuseabe & testable Django apps with virtualenv, pip, and Fabric

As someone noted the other day on one of my Facebook posts, I've been doing a lot of Python development. I've moved almost entirely to Python for development, web and otherwise. Instead of PHP, I reach for Django when I need to prototype an application quickly.

One of the things I've been struggling with is how to build re-usable applications that are testable without having the entire Django stack running. Until recently, I've used buildout to handle this. There's a djangorecipe for creating a Django repository. I include that, a sample project, the necessary requirements in a buildout.cfg and away we go.

All was well, until I included two project that had a sample project/ directory. The base Django project couldn't figure out what was what, problems abound. There are other solutions. I could have set the project (doesn't this seem like the Misses Bennett in Pride and Prejudice?) variable to change it on a per-app basis, but that still left me with some problems.

Namely, I don't like the default layout of djangorecipe's Django project. I wanted to change it, but after some digging around in buildout's internals, I realized it wasn't going to be a solution I could live with long term. I'd heard a lot of people (by that, I mean James) state their preference for virtualenv and pip. The separation of concerns (one application for isolation, another for installation) instead of the all-in-one approach of buildout felt better to me, so I started exploring.

And I came up empty.

A lot of people talk about using virutalenv and pip together; pip documents how to install into a virtual environment; but no one talks about how to use everything together with Django. Specifically, no one mentions what to include in your repository. Until now. :-)

Setting up the repository

The most important part of this for me was what to store in the repository. It's simple enough, really. First, you need a requirements.txt file. For most simple Django apps, it contains one line. For example, this is what the requirements.txt file for d51.django.apps.tagging looks like:

Django

The next thing I need is a simple .gitignore file. My mantra is to not commit anything that I can generate. This means all of the files generated by virtualenv and pip need to be ignored. I also ignore the swap files created by Vim (hey, I'm not the only one at the company who uses it, so might as well ignore it) and I ignore all .pyc and .pyo files. The resulting .gitignore file looks like:

bin/*
include/*
lib/*
.*.swp
*.py[co]

Now we're ready. Of course, you need to have virtualenv and pip installed, but once you've done that, running tests are pretty simple. First, you have to initialize the environment:

prompt> git clone git://github.com/domain51/d51.django.apps.tagging.git
prompt> cd d51.django.apps.tagging
prompt> virtualenv .
prompt> pip install -E . -r requirements.txt

The observant might notice my call to virtualenv. I've left out the parameter --no-site-packages. Two reasons. First, I don't keep things like Django installed at the site-packages level. Second, the things I do install, tools like Fabric, I want access to them while in the virtual environment without having to re-install them.

Now that the virtualenv has been initialized, now you need to activate the virtual environment:

prompt> source ./bin/activate
(d51.django.apps.tagging)prompt>

Notice that the prompt changes. It's prefixed with the name of the directory you're in to signify that you're inside virtualenv. Now running the tests are dead simple:

(d51.django.apps.tagging)prompt> python ./run_tests.py

Testing Django apps inside virtualenv

I need audio that plays when you get to this line. That screeching record player coming to halt. The visual question mark. What's this ./run_tests.py file you ask? The secret sauce.

Django wants to be setup in order to run. Normally that's requires a project, settings.py, and a partridge in a pear tree. Unless you call settings.configure. You can use that to mimic the normal Django settings, tweaking the settings to match your needs for testing.

For d51.django.apps.tagging, the settings are pretty simple. I need to make sure that my app is available along with django.contrib.contenttypes since I make use of the generic relationship code. There's also some cargo culting required, as Django won't run without a DATABASE_ENGINE specified. The end result looks like this:

from django.conf import settings
from django.core.management import call_command

def main():
    # Dynamically configure the Django settings with the minimum necessary to
    # get Django running tests
    settings.configure(
        INSTALLED_APPS=(
            'django.contrib.contenttypes',
            'd51.django.apps.tagging',
        ),
        # Django replaces this, but it still wants it. *shrugs*
        DATABASE_ENGINE='sqlite3'
    )

    # Fire off the tests
    call_command('test', 'tagging')

if __name__ == '__main__':
    main()

Running without activating virtualenv

This works, but requires that you always have virtualenv activated. For example, if you deactivate virtualenv and try to run the test, you get an ImportError:

(d51.django.apps.tagging)prompt> deactivate
prompt> python run_tests.py 
Traceback (most recent call last):
  File "run_tests.py", line 1, in <module>
    from django.conf import settings
ImportError: No module named django.conf

You can programmatically activate virtualenv, however, by including this snippet of code in a .py file located in the root of your repository:

execfile('./bin/activate_this.py',
         dict(__file__='./bin/activate_this.py'))

You can add that line to the top of the file and execute run_tests.py without needing to activate the virtualenv before hand. The line needs to go before the from django.conf line to make sure that Python knows where to find Django and any other requirements of the test.

This requires that you activate the virtual environment prior to running the test, or have Django installed at the system level. This can be further simplified and remove the need to activate and deactivate the environment prior to test runs by executing the bin/activate_this.py file that virtualenv ships with.

Making this reusable

There's a lot of code in that run_tests.py file that is going to be duplicated for every project. Actually, there's only three lines, well, two lines and one variable in a line, in it that are unique: the two line of INSTALLED_APPS and the app name to test in the call_command line.

To keep from repeating that for every single project, I created a simple harness for initializing tests. Introducing d51.django.virtualenv.test_runner, a very small package for running Django tests inside virtualenv.

Using this, run_tests.py now looks like:

try:
    from d51.django.virtualenv.test_runner import run_tests
except ImportError:
    print "Please install d51.django.virtualenv.test_runner to run these tests"

def main():
    settings = {
        "INSTALLED_APPS": (
            "django.contrib.contenttypes",
            "d51.django.apps.tagging",
        ),
    }
    run_tests(settings, 'tagging')

if __name__ == '__main__':
    main()

The first four lines give the user some input when they run the tests without test harness. That's optional, depending on how user friendly you want to be. After that, all I do is call run_tests with a settings dictionary and the name of the app I want to test.

There is one downside, though. You have have it installed outside of your virtualenv in order to run your tests. Personally, I'm not to worried about it, as I have it installed, but if you're really paranoid, you could include it in your requirements.txt file, which would require that the user be inside the virtualenv to run your tests.

Wrapping it all up in a Fabric cloth

The final step is to make all of this bullet proof is creating a Fabric file that handles all of the initialization and running of the tests for me. For good measure, it should also be capable of cleaning up after itself. I don't need a bazillion copies of Django laying around, afterall.

The end result looks something like this (using Fabric 1.0a):

from fabric.api import local

def test():
    """
    Run tests for d51.django.apps.schedules
    """
    local("python ./run_tests.py")

def init():
    """
    Initialize a virtualenv in which to run tests against this
    """
    local("virtualenv .")
    local("pip install -E . -r requirements.txt")

def clean():
    """
    Remove the cruft created by virtualenv and pip
    """
    local("rm -rf bin/ include/ lib/")

Now you can run the initialize the environment, run the tests, and clean up after yourself with three commands:

prompt> fab init
prompt> fab test
prompt> fab clean

One drawback to this method, Fabric's local command swallows the output of the test. This isn't a problem until you have a failure. local does contain a capture parameter, but it doesn't display the output from an failed command. That's fixable, but for the time being, my recommendation is to use Fabric as your quick sanity check, but rely on straight python ./run_tests.py for your real testing.

Conclusion

That brings us to the end of our quick tour. Hopefully this provides you with the information you need to get started using virtualenv and pip with Django. It's not that complicated. Actually, once you have your bearings, it's downright easy. The problem is more that people who have trodden down this path haven't documented their way. Hopefully, this post helps serve as a rough map.

Thanks

I'd like to thanks James Bennett for illuminating a few pieces of this puzzle for me (in particular, pointing me toward settings.configure) and his preview a draft of the article before I posted. I'd also like to thank Jeff Triplett for his comments on a draft and pointing out an unexplained inconsistency with the rest of the world's examples of virtualenv. And, as always, my good buddy Roder for his constant encouragement.