Python

Python development with virtualenvwrapper, django, buildout and mercurial – a basic example

I have been trying to streamline my python development processes over the years. Earlier this year @rossjones introduced me to virtualenvwrapper which made things even simpler. I’m about to go into quite a concentrated period of development so I thought I’d take the opportunity to document what I do to make an easily reproduceable python environment. Please let me know of any errors or suggestions for better ways of doing this as I go along.

Note, I have just noticed this is not working on OS X Lion. Looking now at why. 20110816 17:28 BST

We are going to use pip to install virtualenvwrappper. If you have pip this should be as simple as

sudo pip install virtualenvwrappper

If you don’t have pip please read http://pypi.python.org/pypi/pip.

Assuming we have virtualenvwrapper installed and configured (read here for the nitty gritty) we can now create our virtual environment. My project is to sync some files to Dropbox automatically so I will call it dropbox-sync.

ian@vm:~$ mkvirtualenv dropbox-sync
New python executable in dropbox-sync/bin/python2.7
Also creating executable in dropbox-sync/bin/python
Installing setuptools............done.
Installing pip...............done.
etc etc etc etc etc

Now I am in my virtual environment, any packages I install will only be available to that environment. I have the habit of working within a projects directory of my home directory, so …

(dropbox-sync)ian@vm:~$ mkdir ~/projects/dropbox-sync
(dropbox-sync)ian@vm:~$ cd ~/projects/dropbox-sync/
(dropbox-sync)ian@vm:~/projects/dropbox-sync$

To deactivate your working environment you can use ‘deactivate’.

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ deactivate
ian@vm:~/projects/dropbox-sync$

To reactivate the environment again use the workon command.

ian@vm:~/projects$ workon dropbox-sync
(dropbox-sync)ian@vm:~/projects/dropbox-sync$ 

You can see from this that your virtual environment is not explicitly linked to the directory you’ll be working in. As I virtually always want to go to my working directory at the same time as I enable my working environment I can automate this.

By default the virtualenvwrapper files go into ~/.virtualenvs. Within that directory you’ll see a script called postactivate. This is run straight after an activation of an environment takes place. We can utilise this to change to our new directory. In my case I have modified it to read

[cc lang=”bash”]

#!/bin/bash
# This hook is run after every virtualenv is activated.
set envname=”$(basename $VIRTUAL_ENV)”
if [ ! -d “$HOME/projects/$envname” ]; then
mkdir ~/projects/$envname
fi
cd ~/projects/$envname
[/cc]

The first line of above sets our envname variable to be the name of our current virtual environment directory. I refer to this sometimes as the project name. The script checks if a similar directory exists in projects and if not creates it. Finally it changes to that directory.

Similarly when I deactivate an environment I want to move back to my projects directory (to remind myself I should no longer work in that directory when the virtual environment is not activated), so the script at ~/virtualenvs/postdeactivate reads simply

[cc lang=”bash”]

#!/bin/bash
# This hook is run after every virtualenv is deactivated.
cd ~/projects

[/cc]

If you look in the .virtualenvs directory you’ll see a lot of scripts that can be modified. The documentation explains this more and can be found at http://www.doughellmann.com/docs/virtualenvwrapper/. There are hooks you can also use to customise specific environments if you find you need to do something repeatedly for just certain projects when they are activated.

It may be that you already have a stack of libraries you have installed to your own site packages. Those will be available in your virtual environment. This may not be a bad thing as there may be certain packages you want to use repeatedly and not want to download and install each time. If however you want something clean you can use the “–no-site-packages” option. e.g.

mkvirtualenv --no-site-packages dropbox-sync

In my case, projects are either quick throwaway projects, or larger projects which need to be stored in a mercurial repository for source control and backup. I’m also always using Django (or very nearly always) and there are a number of utilities I can’t do without. I can use the hooks that virtualenvwrapper gives us to allow for these environments to be created very quickly.

Firstly I want any project I setup to be under mercurial control and to allow me to easily to push to the code to a bitbucket repository should I require. The following lines in our global postactivate script (the one we were editing before) do this. Add this below the code we already added.
[cc lang=”bash”]
if [ ! -e “$HOME/projects/$envname/.hgignore” ]; then
cp ~/.virtualenvs/.hgignore .
hg init
reset
echo ********************** IMPORTANT ***************************
echo “[paths]” > .hg/hgrc
echo “default = ssh://hg@bitbucket.org/zobbo/$envname” >> .hg/hgrc
echo if this is a long term project, create a repository on bitbucket
echo and then hg push. Check the value of default in paths in
echo .hg/hgrc – may need to switch from zobbo to bfs
echo ********************** IMPORTANT ***************************
hg add ./.hgignore
hg commit -m “Adding initial .hgignore”
fi
[/cc]
This looks for the presence of a file called .hgignore in our current directory. If it doesn’t exist it copies a template one from our ~/.virtualenvs directory which I have placed there. The contents of this file are currently:

syntax: glob
*.pyc
*~
bin
.installed.cfg
.DS_Store
(.*/)?\#[^/]*\#$

This is a set of files that I do not wish to have checked into mercurial (e.g. .pyc files).

Having copied our .hgignore file into the project directory I then do a hginit to mark the directory as a mercurial repository. The ‘reset’ then clears the screen so I don’t miss any of the next output. I create a .hg/hgrc file which tells my repository where to push itself remotely (should I choose to do so). I then remind myself that I’ll need to setup bitbucket if I want to do that remote push. Finally I add .hgignore and commit it to my local repository.

The long and short of this is that I have a working project directory which is checked in and ready to go. And where I may have to do something I have reminders on the screen about what to do.

The next thing I wish to do is create a buildout file for this install. I’ll go into buildouts in more detail another time. For this article I’ll just show a very basic example. First of all some more lines are added to our postactivate script.
[cc lang=”bash”]
if [ ! -e “$HOME/projects/$envname/buildout.cfg” ]; then
cp ~/.virtualenvs/buildout.cfg .
hg add ./buildout.cfg
hg commit -m “Adding initial buildout.cfg”
echo
echo Now run pip install zc.buildout
echo
fi
[/cc]
This time we are checking for the presence of a file called buildout.cfg (unsurprisingly, the config file used by buildouts) and once again we copy over a template version of that file from our ~/.virtualenvs directory if it does not exist. We then add the file to mercurial, commit it and then tell ourselves we need to run a pip install command. The contents of the buildout.cfg file will be something like this:

[buildout]
parts = django
eggs = ipython
       ipdb
eggs-directory = ../.buildout-eggs
download-cache = ../.buildout-downloads
unzip = true

[versions]
django=1.3

[django]
recipe = djangorecipe
settings = development
eggs = ${buildout:eggs}
extra-paths =

project = project

I don’t want to go into too much detail about this for the moment but just to say you’ll need to create a .buildout-eggs and .buildout-downloads directory in the parent directory of the project directory (in my case ~/projects) if you want to use that part of the recipe. It means that if you have multiple projects you can share the eggs and downloads without downloading them every time.

Now we have all our pieces together I can try a test run from start to finish. Note that if you’ve already created a dropbox-sync project you should get rid of it. That’s easy – something like:

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ deactivate
ian@vm:~/projects$ rmvirtualenv dropbox-sync
ian@vm:~/projects$ rm -rf dropbox-sync/
ian@vm:~/projects$ 

I have also decided I want to store my code in a bitbucket repository so I create one there called dropbox-sync. Now I run the command

mkvirtualenv dropbox-sync

Some work is done, the screen is cleared and I see this

********************** IMPORTANT ***************************
if this is a long term project, create a repository on bitbucket
and then hg push. Check the value of default in paths in
.hg/hgrc - may need to switch from zobbo to bfs
********************** IMPORTANT ***************************
adding .hgignore
.hgignore
committed changeset 0:d3e247f59565
adding buildout.cfg
buildout.cfg
committed changeset 1:c270ecce5c5c

Now run pip install zc.buildout

In this case I want to push the repository to bitbbucket. So I do

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ hg push
running ssh hg@bitbucket.org "hg -R zobbo/dropbox-sync serve --stdio"
pushing to ssh://hg@bitbucket.org/zobbo/dropbox-sync
searching for changes
2 changesets found
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 2 changesets with 2 changes to 2 files
remote: bb/acl: zobbo is allowed. accepted payload.

The files are now on bitbucket. Now time to run the buildout. First do the pip install as our message says

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ pip install zc.buildout
Downloading/unpacking zc.buildout
  Downloading zc.buildout-1.5.2.tar.gz (331Kb): 331Kb downloaded
  Running setup.py egg_info for package zc.buildout
    
Requirement already satisfied (use --upgrade to upgrade): setuptools in /home/ian/.virtualenvs/dropbox-sync/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg (from zc.buildout)
Installing collected packages: zc.buildout
  Running setup.py install for zc.buildout
    
    Skipping installation of /home/ian/.virtualenvs/dropbox-sync/lib/python2.7/site-packages/zc/__init__.py (namespace package)
    Installing /home/ian/.virtualenvs/dropbox-sync/lib/python2.7/site-packages/zc.buildout-1.5.2-py2.7-nspkg.pth
    Installing buildout script to /home/ian/.virtualenvs/dropbox-sync/bin
Successfully installed zc.buildout
Cleaning up...

And now run our actual buildout which will give us a working django installation

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ buildout
Creating directory '/home/ian/projects/dropbox-sync/bin'.
Creating directory '/home/ian/projects/dropbox-sync/parts'.
Creating directory '/home/ian/projects/dropbox-sync/develop-eggs'.
Not upgrading because not running a local buildout command.
Installing django.
Generated script '/home/ian/projects/dropbox-sync/bin/django'.
(dropbox-sync)ian@vm:~/projects/dropbox-sync$ 

We now have within our project bin directory a django command, which we can use as per the django-admin.py command. We will call our Django app without our project just syncer.

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ bin/django startapp syncer

We now have a django environment created automatically with the following structure

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ tree . 
.
├── bin
│   └── django
├── buildout.cfg
├── develop-eggs
├── parts
└── project
    ├── development.py
    ├── development.pyc
    ├── __init__.py
    ├── __init__.pyc
    ├── media
    ├── production.py
    ├── settings.py
    ├── settings.pyc
    ├── syncer
    │   ├── __init__.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── templates
    └── urls.py

You can see our created django app directory and (if you know Django) all the other usual Django components. We should probably commit this to our repository and push it remotely.

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ hg stat 
? project/__init__.py
? project/development.py
? project/production.py
? project/settings.py
? project/syncer/__init__.py
? project/syncer/models.py
? project/syncer/tests.py
? project/syncer/views.py
? project/urls.py

You can see the only items which are flagged as being missing from our repository are the actual items of code we want. So finally.

(dropbox-sync)ian@vm:~/projects/dropbox-sync$ hg add project
adding project/__init__.py
adding project/development.py
adding project/production.py
adding project/settings.py
adding project/syncer/__init__.py
adding project/syncer/models.py
adding project/syncer/tests.py
adding project/syncer/views.py
adding project/urls.py
(dropbox-sync)ian@vm:~/projects/dropbox-sync$ hg commit -m "Start of our new app"
project/__init__.py
project/development.py
project/production.py
project/settings.py
project/syncer/__init__.py
project/syncer/models.py
project/syncer/tests.py
project/syncer/views.py
project/urls.py
committed changeset 2:e8bd66e10c1a
(dropbox-sync)ian@vm:~/projects/dropbox-sync$ hg push
running ssh hg@bitbucket.org "hg -R zobbo/dropbox-sync serve --stdio"
pushing to ssh://hg@bitbucket.org/zobbo/dropbox-sync
searching for changes
1 changesets found
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 9 changes to 9 files
remote: bb/acl: zobbo is allowed. accepted payload.
(dropbox-sync)ian@vm:~/projects/dropbox-sync$ 

You can see the results of this on bit bucket here. I now have a fully working Django install (although Django config needs to be done). If you try and run bin/django syncdb you’d be told you haven’t setup your db settings but that’s a standard Django setup item.

This environment is self contained. Assuming you didn’t install zc.buildout outside of the environment, once you deactivate you’ll see the ‘buildout’ command no longer exists. Switch back again (“workon dropbox-sync”) and voila, it’s back again.

Now I have the environment setup I can create new projects very easily and consistently. However, there’s a lot more that can be done and having written this up I can see a lot of holes and places where things could be sweeter.

Note I have added the two virtualenv files we created into the repository at dropbox-sync, they can be found in the virtualenv-files directory

Leave a Reply

Your email address will not be published. Required fields are marked *