Over the weekend I put together django-shorturls, the latest in a series of small plugable Django apps I’ve written.
This time, though, I used zc.buildout and djangorecipe to build, test, package, and distribute the app, and (with the exception of a few annoyances) it’s an exceedingly civilized way to develop an app.
In the interest of helping improve Buildout’s still-nascent documentation, I documented my steps along the way.
So, here’s how build, test, and distribute a reusable Django app using Buildout, with every step along the way explained in excruciating detail.
Create the Buildout environment
First, create the minimal structure for your app. Make a structure like:
django-shorturls/
LICENSE
README
bootstrap.py
buildout.cfg
setup.py
src/
shorturls/
__init__.py
Okay, so this looks like a bunch of crap, but really it’s all either empty files or ones you can copy from elsewhere at this point; we’ll fill it all in later.
Let’s look at what’s here:
- LICENSE
If you’re releasing open source software, you need to have a license. Remember that in the absence of a license the default is “All rights reserved.” That is, if there’s no LICENSE, it’s not open source.
I use the BSD license; I always have one floating around I can copy.
- README
Code without documentation makes Baby Jesus cry. Please: at least — at the very least — include a README with your contact info and basic usage instructions.
When I’m just getting started I just touch README to remind myself that I need to write one before I’m done.
- bootstrap.py
This is Buildout’s bootstrap script. This is a little script that you can include with your package to bootstrap the Buildout environment. You should do so: it saves you from having to install Buildout system-wide, since system-wide packages are the road to hell. This also makes it very easy for others to contribute to your app: they don’t have to know how to use Buildout since they can just run the bootstrap.
Getting this file is easy: wget http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py
- buildout.cfg
Buildout’s config file. We’ll spend some time in here later, but for getting started you only need this to contain:
[buildout] parts =
- setup.py
- Eventually this’ll contain the magic distutils incantations so that you can install this app and push it to the Cheeseshop. We’ll get to this in a second.
- src/
The convention in the Buildout world appears to be putting the source to modules in a src directory, so might as well follow convention.
We’ll stuff all our code in here, later; now it’s just an empty shorturls module.
Just to prove that the above really is easy, here’s what it looks like in reality:
$ mkdir -p django-shorturls/src/shorturls $ cd django-shorturls $ cp ~/Shrapnel/BSD_LICENSE LICENSE $ touch README $ wget http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py $ cat - > buildout.cfg [buildout] parts = ^D $ touch setup.py $ touch src/shorturls/__init__.py
One of these days I’ll probably write a shell script to automate this; it’s really quite easy, if a bit repetitive.
Now that our boilerplate is written, we can have Buildout generate the rest of its boilerplate:
$ python bootstrap.py Creating directory '.../django-shorturls/bin'. Creating directory '.../django-shorturls/parts'. Creating directory '.../django-shorturls/eggs'. Creating directory '.../django-shorturls/develop-eggs'. Generated script '.../django-shorturls/bin/buildout'. $ ./bin/buildout Getting distribution for 'zc.buildout'. Got zc.buildout 1.2.1. Upgraded: zc.buildout version 1.2.1; restarting. Generated script '.../django-shorturls/bin/buildout'.
Now we’ve got the latest version of Buildout, and a nice isolated environment to fool around in. Anything we do inside this django-shorturls directory will be nicely isolated, so we can develop in a little sandbox, protected from anything that might differ in the outside world.
Note
What’s kinda cool is that these two steps — python bootstrap.py && ./bin/buildout — are all that anyone who contributes to your project needs to do to replicate your development setup. They’ll get your app, your test suite, and all the requisite bootstrapping. So this work we’re doing up front now can be seen as a one-time expense against all the time all your future contributors would otherwise have to spend getting up and running.
Let’s take a look at what’s here now, with the stuff bootstrap.py and buildout created in bold:
django-shorturls/
bin/
buildout
bootstrap.py
buildout.cfg
develop-eggs/
eggs/
setuptools-0.6c9-py2.5.egg/
zc.buildout-1.1.1-py2.5.egg/
zc.buildout-1.2.1-py2.5.egg/
parts/
src/
shorturls/
What’s all this, then?
- /bin/buildout
This is the main entry point to interacting with Buildout. Every time you change your buildout.cfg, you’ll re-run bin/buildout to update the environment to reflect the changes in your config.
Buildout actually keeps track (in a file called .installed.cfg)of what was in your config file the last time you ran buildout and what was installed, so only new or changed stuff gets installed each time.
- develop-eggs/
- A placeholder for eggs you’re currently developing; we’ll see this used later on.
- eggs/
- Any eggs that Buildout installs end up here. You can see that in my case I ended up with setuptools (which Buildout builds on top of) as well as two versions of zc.buildout — bootstrap.py installed one, and then buildout automatically detected the availability of a newer version and upgraded it for me.
- parts
- Certain other bits of Buildout cache, download, or otherwise store things here in parts. Think of this as a staging area and cruft zone.
Make a setup.py
So what we’ll be doing next is starting to develop our shorturls app as a Python package, a.k.a. an “egg.” Buildout requires that the package we’re developing be properly egg-ified, so we need to make a setup.py. A minimal one is fine for now:
from setuptools import setup, find_packages
setup(
name = "django-shorturls",
version = "1.0",
url = 'http://github.com/jacobian/django-shorturls',
license = 'BSD',
description = "A short URL handler for Django apps.",
author = 'Jacob Kaplan-Moss',
packages = find_packages('src'),
package_dir = {'': 'src'},
install_requires = ['setuptools'],
)
Note
Wait, how’d I know what to put in my setup.py? Well, this illustrates one of my main complaints about setuptools and Buildout: you basically have to cargo-cult your config files from other projects. I can’t for the life of me tell you what that package_dir = {”: ‘src’} business is about, for example; all I know is that if I leave it out, Buildout doesn’t work.
Set up Buildout, and a sandbox
Next we need to inform Buildout about the egg we’re developing. While we’re at it, we’ll also generate a python that’s local to this development sandbox (this way we can interact with the code as we develop it without needing to install anything).
Modify your buildout.cfg to read:
[buildout]
parts = python
develop = .
eggs = django-shorturls
[python]
recipe = zc.recipe.egg
interpreter = python
eggs = ${buildout:eggs}
Since this is the first full Buildout recipe, let me pause and explain what’s going on here bit by bit. The important bits to know are:
- buildout.cfg is a INI-style config file, with the added ability to interpolate variables using ${varname} syntax. You can refer to keys defined in other sections as ${section:variable}.
- The file is processed in a sort of waterfall style: work starts with the [buildout] section, and then proceeds through every other section defined in the buildout:parts key. This file has a single part, python.
- Since we’re developing an egg (as opposed to just pulling together outside code), we use develop = . to inform Buildout of that fact. This line basically says that we’re developing a package whose setup.py lives in ., the current directory.
- We want the egg we’re developing to be available to any scripts that run inside this environment, so we set eggs = django-shorturls to get the egg we’re developing installed.
- The next part, [python], we’ll use to generate a Python interpreter local to this Buildout.
- Parts are associated with a “recipe.” Recipes are just other eggs, available from PyPI or elsewhere, that define how to process a part of a Buildout.
- Here, we’ve got recipe = zc.recipe.egg. This recipe is the standard way of installing eggs, creating wrapper scripts and interpreters, and others.
- The interpreter = python bit tells zc.recipe.egg to generate a Python interpreter named python and install it locally to the Buildout.
- We want this interpreter to have our development egg available, so we set eggs = ${buildout:eggs} to copy the list of eggs from the buildout part.
Clear as mud, right?
Now that we’ve changed our buildout.cfg, we re-run bin/buildout to pick up those changes:
$ ./bin/buildout Develop: '.../django-shorturls/.' Getting distribution for 'zc.recipe.egg'. Got zc.recipe.egg 1.2.2. Installing python. Generated interpreter '.../django-shorturls/bin/python'.
What happened here was:
- Buildout noted that we’re using a develop egg, ran the local setup.py file, and created a file develop-eggs/django-shorturls.egg-link, a little link file making src/shorturls available to the rest of Buildout.
- We asked for the zc.recipe.egg recipe but didn’t have it already installed, so Buildout fetched and installed it from the Cheeseshop.
- Finally, Buildout generated a bin/python interpreter.
Let’s take a look at what this bin/python interpreter is. We haven’t installed our app anywhere, so using the system Python we can’t see the package:
$ /usr/bin/python >>> import shorturls Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named shorturls
However, we can get to our package from this little wrapper:
$ ./bin/python >>> import shorturls >>> shorturls <module 'shorturls' from '.../django-shorturls/src/shorturls/__init__.py'>
This shows off one of the key concepts of Buildout: what happens in Buildout, stays in Buildout. That is, everything that you do inside this isolated environment is only available inside the environment, and doesn’t clutter up your system.
Note
By the way, now’s a good time to create an “ignore” file for your revision control system — you are using revision control, right? Since a bunch of this stuff gets generated, we don’t want to check it into revision control.
Here’s what I use for my .gitignore:
*.pyc *.pyo .installed.cfg bin develop-eggs dist downloads eggs parts src/*.egg-info
If you’re using a different SCM the syntax might be slightly different, but that right there is the list of stuff you’ll want to avoid checking into SCM.
Create a test wrapper
Code without tests is broken as designed. Therefore, the next step is to set up an easy way to run tests.
We’ll do this with djangorecipe, a Buildout recipe that automates much of the process of using Django with Buildout. It actually does a lot more than we’ll touch on here, but that’s another show.
Note
You might feel like the next bit is somewhat clunky, and it is. The basic problem is that Django’s test suite needs at least a minimal settings.py to be able to run unit tests, and there’s not really a slick way of doing that automatically (yet). I’ve played with a few ways of doing this, and what follows is the least-clunky way I could figure out.
Open up src/shorturls/testsettings.py and make a minimal settings file:
DATABASE_ENGINE = 'sqlite3' DATABASE_NAME = '/tmp/shorturls.db' INSTALLED_APPS = ['shorturls'] ROOT_URLCONF = ['shorturls.urls']
These four settings are the absolute minimum set required just to get the test suite bootstrapped and running. Note that the DATABASE_NAME given there won’t actually be used — tests under SQLite are run in-memory — but you do need one anyway.
Also, a limitation of the test suite is that it’ll only test apps with a models.py (even an empty one), so:
$ touch src/shorturls/models.py
Next, inform Buildout and djangorecipe of that file and ask it to build a test runner by appending the following to buildout.cfg:
[django]
recipe = djangorecipe
version = 1.0.2
project = shorturls
projectegg = shorturls
settings = testsettings
test = shorturls
eggs = ${buildout:eggs}
Oh, and remember to add django to the list of parts in the buildout:parts setting (so it should now read parts = python django).
Since I’m doing the explain-even-the-obvious thing, let’s go line-by-line:
- recipe = djangorecipe
- We want to use djangorecipe for this part.
- version = 1.0.2
- Develop against Django 1.0.2. djangorecipe can take a few options here: we could say version = trunk to develop against the trunk, and a few other options detailed in the djangorecipe docs.
- project = shorturls
The name of the Django “project” that the Django installation will use.
The main purpose of djangorecipe is to pull together all the dependancies for a deployed Django app, and in that situation you need a project to contain the settings, root URLconf, and such. The recipe will actually generate this project module for you, which is nice. We don’t want that to happen, however, so…
- projectegg = shorturls
- This tells djangorecipe that our project module already exists in the package shorturls, so don’t generate a project module.
- settings = testsettings
- The name (relative to project) of the module containing settings. So between this and project, the DJANGO_SETTINGS_MODULE used by the test runner will be shorturls.testsettings.
- test = shorturls
- This tells the recipe to generate a test runner for the apps listed. We just have one, shorturls.
- eggs = ${buildout:eggs}
- Just as for the python part, we want to make the eggs that Buildout knows about available to the generated scripts.
Just as before, since we’ve made changes to the buildout.cfg, we re-run buildout to pick up those changes:
$ ./bin/buildout Develop: '.../django-shorturls/.' Installing 'djangorecipe'. Getting distribution for 'djangorecipe'. Got djangorecipe 0.17.1. Installing django. Downloading Django from: http://www.djangoproject.com/download/1.0.2/tarball/ Generated script '.../django-shorturls/bin/django'. Generated script '.../django-shorturls/bin/test'.
Note
There’s currently some sort of weird bug in djangorecipe that makes downloading Django take for-freeking-ever. I’m assuming it’ll be fixed at some point, but for now running buildout -v works around the slowness.
We’ve got two new scripts here.
bin/django is a manage.py wrapper that bootstraps Django into the Buildout environment. Try, for example, ./bin/django shell to get into a shell already bootstraped both for Buildout and for Django.
bin/test is the one we’re really interested in, though: this is the one that runs the test for the app we’re developing. To make sure everything works, let’s create a simple passing test to double-check that it gets picked up and run correctly. Put something like the following into src/shorturls/tests.py:
from django.test import TestCase
class ShortURLTests(TestCase):
def test_environment(self):
"""Just make sure everything is set up correctly."""
self.assert_(True)
And then run the tests:
$ ./bin/tests
Creating test database...
.
------------------------------
Ran 1 test in 0.003s
OK
Destroying test database...
Sweet.
Write some code
Hopefully you already know how to do this part. Just write the app, OK? This isn’t Buildout-specific, but a few hints anyway:
- Designing your application for re-use is a whole field unto itself. James is really the expert here; if you haven’t already, you should check out his talk on building reusable apps (slides, video).
- Please, for the love of $DEITY, do use this test setup. I don’t care if you’re a TDD or a BDD or a DDD kind of guy, but please write tests and run them early and often.
- If it turns out that your app requires some other external package, you can easily make it available by adding it to the list of eggs in the buildout:eggs section. They’ll get installed into the Buildout environment automatically for you.
Upload to the Cheeseshop
Now that we’ve got code, it’s time to share it with the world. To be good citizens, let’s fill out the setup.py with all the extra info a good package should have. Here’s my final setup.py:
import os
from setuptools import setup, find_packages
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name = "django-shorturls",
version = "1.0",
url = 'http://github.com/jacobian/django-shorturls',
license = 'BSD',
description = "A short URL (rev=canonical) handler for Django apps.",
long_description = read('README'),
author = 'Simon Willison, Jacob Kaplan-Moss',
author_email = 'jacob@jacobian.org',
packages = find_packages('src'),
package_dir = {'': 'src'},
install_requires = ['setuptools'],
classifiers = [
'Development Status :: 4 - Beta',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP',
]
)
I added a trick to read in the README into the package’s long description; this becomes available on the PyPI detail page. I’ve also added trove classifiers to help people navigate through the Cheeseshop and find my package.
Finally, at long last, we can push our package to the Cheeseshop:
$ ./bin/buildout setup . sdist register upload
This does the three things we need to do to get this package listed:
- Create a distribution (sdist). This generates a tarball in dist/.
- Register the package with the Cheeseshop (register). If you’ve not already created a PyPI account this’ll ask you to create one now.
- Upload your package (upload).
Whiskey
I suggest Bulleit, double, rocks.
Comments:
That is... thorough. Thanks for letting me get an overall feel without doing any work. :-)
There's an error in your last numbered list though.
There was in my RSS, sorry.
Thanks! Just forwarded this to my illustrious sys admin!
Great post, very detailed!
Oh, and you don't put good whiskey on rocks, for gods sake. My recommendation goes out to Glenfiddich 30 yr ( http://www.glenfiddich.com/... )
Great post, thanks.
I started to create a template for paste to create a basic buildout layout.
Funny enough, I could not use the paster script created by my buildout; paste at the end of the template creation try to run a setup command on the newly created package using the python interpreter in my path and not the buildout interpreter.
Is there a buildout equivalent to virtualenv's activate script?
anyway, here is the template:
http://github.com/dinoboff/...
Thanks again!
Now if only it didn't require setuptools packages and eggs...
Also, for sake of completeness it's worth pointing out that Django apps have their own PyPi trove classifier, and for ease of finding it's a good idea to use it:
Framework :: Django
Thank you so much for this.
Between this and some of the screen casts/slides I was able to set up buildout after having never heard of it, and it does everything _much_ nicer then I was trying to do it. Also my designer thanks you for letting me get her something reasonable to work with.
Thanks again.
Great post !
"One of these days I’ll probably write a shell script to automate this; it’s really quite easy, if a bit repetitive."
People creates templates using Paster.
Look at ZopeSkel for inspiration.
Indeed, great post! To echo Tarek's comment, you guys might consider a Paste Script like the ones in ZopeSkel, see: http://svn.plone.org/svn/co.... Great to see buildout spreading in the wild! Also, I had no idea buildout would run 'python setup.py sdist…', for you, interesting.
Great stuff, thanks, Jacob! Where were you five days ago when I was starting this process myself? ;-)
You have given me a few of the pieces I wasn't sure of. I also found the following post helpful. It builds a virtualenv sandbox for several versions of Python, and includes PIL, isolating the buildout environment even further.
http://blog.crowproductions...
I will post my buildout scripts some time over the next couple of days in case they may help someone else. (They're not quite done ;-) I've also got it compiling MySQL, Xapian, and the beta version of Django 1.1.
For what it's worth, to use Django 1.1 beta 1, just change the version in your django/djangorecipe section lika so:
version = http://code.djangoproject.c...
Yeah... PasteScript provides a nice template for what goes in your setup.py and the scripts itself steps you through creating it. It will even create your svn directory structure and do your initial checkin.
@Damien: As far as using pastescript in a buildout sandbox, you have to tell buildout explicitly that you want to load the pastescript egg and any console scripts it has (using repoze.recipe.egg or some other similar recipe).
Buildout treats every executable as it's own sandbox with it's own specially crafted sys.path, so there is no equivalent to virtualenv's activate script (which is more about putting the environment bin and your sandboxed python on the your path).
In general, I work on a buildout within a virtualenv, which lets me load tools like pastescript and zc.buildout as global scripts for my work area, and then focus the buildout on a particular project. Then the buildout stays uncluttered with tool packages not needed in deployment.
Buildout also falls down over indirection occasionally. Your buildout can become a tangled web of interdependent recipes to do simple things that could be done easier in a few line of python. If you get the feeling things are getting much more complex and turtlely than you like... check out http://www.blueskyonmars.co...
Works with bare disutils and setuptools, lots of sensible helpers, super simple use. Could be the right hammer...
@Damien:
Buildout doesn't have any recipes with a pre-baked "activate" script like virtualenv, but you can use the more generic recipes such as collective.recipe.template to provide your own shell profile for a project. This does has the advantage of letting you toss other project-specific shell shortcuts n' tricks into there, which can be quite nice.
http://www.bud.ca/blog/usin...
The fez.djangoskel (http://www.stereoplex.com/t...) works fine for me, and makes all the boilerplate code with a paster template. Quite handy
Good stuff!
Some more suggestions for making the buildout experience a happy one:
1. You can list additional dependencies in the 'install_requires' field of setup.py instead of buildout:eggs. This lets other tools use your dependency metadata should someone want to use a different tool (e.g. pip). Since install_requires should only state dependencies at the API level, it's then possible if requried to override this metadata in buildout:eggs if you want to pin a dependency to a specific version.
2. 'Django' should be listed as a dependency in 'install_requires'.
3. Cache your eggs!
Eggs have some drawbacks. But they do have the advantage of letting you compose a working set of python packages by simply picking from a list of PATHs. If you've got an egg cache, the 2nd time you install a Buildout that requires the Django egg, it will be very quick as it simply picks the egg from your cache. In dependency-heavy projects, it's a beautiful thing when you can do a fresh checkout, run ./bin/buildout, and 10 seconds later be ready to run ./bin/test with a complete, isolated environment with 50+ packages.
To cache the eggs you can have user-account level settings by putting a file in ~/.buildout/default.cfg and adding:
[buildout]
eggs-directory = /someplace/eggs
4. Use omelettes to flatten your eggs!
A big drawback of eggs is that you no longer have a single directory which you can grep, point an IDE at, etc. Fortunately there is a recipe to take a list of eggs and create a "flat" view by creating a collection of symlinks. The following config will give you a /parts/omelette/ which you can use (one can even PYTHONPATH this location and use it in a completely "egg unaware" manner).
[omelette]
recipe = collective.recipe.omelette
eggs = ${buildout:eggs}
When combined with egg caching, it's extra juicy to have a large central location of python packages, and let buildout simply create symlink-views into this cache that express different working sets of packages.
5. List eggs as part of the 'app' part.
This is a small change, but instead of buildout:eggs, I find it makes more sense to have the list of eggs defined in the 'app' part (the part named [django] in your example). Then you can do 'django:eggs' if you want to refer to that list of packages in another recipe.
In Grok, the 'app' part is by default named 'app' and not 'grok'. This makes it more clear that you are defining your application here and not just experessing some framework-y configuration.
There is an interpreter argument you can use to give yourself something called bin/python which works in the environment, similar to virtualenv's bin/python. virtualenv's bin/activate just updates $PATH (and $PS1 so you don't forget). So it'd work similarly to that, except you can't install libraries -- all installation has to go through buildout.
If you don't like how buildout uses eggs and the script mangling stuff, there is a buildout recipe for using pip: http://pypi.python.org/pypi...
Oh, a few more tid bits ...
6. Some people append 'dev' to the version in setup.py, to indicate the version that you are developing towards. e.g. If 1.0 is the latest release, then setup.py would have 1.1dev to indicate you are working towards that version. But then you do have to remember to remove the 'dev' suffix before releasing.
7. Everybody loves a history!
In addition to a README.txt, a HISTORY.txt is really nice for every project. This provides a concise summary of changes for each release. Something in the format of:
Changelog for django-shorturl
=============================
1.0 (2009-03-18)
----------------
- Initial release.
8. If you have a dev suffix and HISTORY file, it adds a lot of manual fiddly bits to doing releases. zest.releaser and collective.releaser are a couple tools to help automate that stuff (generating tags, updating version numbers, updating HISTORY.txt file, creating sdists).
http://pypi.python.org/pypi...
These tools aren't buildout-specific, they can be installed globally as part of a developer's toolkit, but it can be nice to include configuration in buildout to install these tools. This way when a new developer starts on a project, you can say, "here are some tools this particular project is using for development, and the dev buildout provides those tools for you in the project's bin directory".
Tarek mentioned ZopeSkel. I wrote something similar, fez.djangoskel, which provides Paster templates for Django buildouts, projects, apps, and namespace projects and apps. You can find it here:
http://pypi.python.org/pypi...
Please contribute if you want to, the code's on github:
http://github.com/danfairs/...
On cargo-culting the pacakges/package_dir arguments, these are documented in Distutils:
http://docs.python.org/dist...
I don't know where the src/ sub-directory convention started from. There is reference to using src/ in the Distutils docs, so I think this convention goes back a little ways:
http://docs.python.org/dist...
But in my experience using src/ with packages/package_dir args, or just putting the package in the top-level and using only the packages arg varies quite a bit in packages produced by folks using Buildout. The later option is simpler, especially for the typical use case of just having a single package, but with the find_packages and package_dir boilerplate, one can just cargo-cult those two args and just put all packages in 'src/' and not have to think about (such as when a project grows a second package and you spend an hour banging your head because you forgot to go back and re-adjust the setup.py to reflect this change).
That last comment *sort of* looks like spam. Great post, Jacob.
Distributing Django apps as eggs is actually straightforward, greate place for debate in the Django community about the best way to ship static, and that's something I confess to have not solved myself yet!
Leave a comment: