Bootstrapping a Python project

2010-04-01T09:54:44Z in python, programming

Here are my notes on starting a brand new, versioned, readily distributed Python project. Examples show a bash session, but Python, virtualenv, pip, distribute, paster, and hg all work on Windows (from whence more and more Python GIS programmers come) as well.

1. Create a fresh virtual environment. Why? So you don't clutter your system Python with in-development code, and to keep possibly conflicting versions of dependencies out of your development environment. It's probably even more useful for working on code that we may clone from another repository than it is for starting from scratch.

$ virtualenv --distribute foo
New python executable in /tmp/foo/bin/python2.6
Also creating executable in /tmp/foo/bin/python
Installing distribute....done.

2. Install paster if it wasn't already installed under our original Python (or if you used the --no-site-packages option). It's a script that creates a basic, normal source layout for a new package, prompts us for essential project metadata, and writes a working setup.py.

$ cd foo
$ ./bin/pip install PasteScript
Downloading/unpacking PasteScript
  Downloading PasteScript-1.7.3.tar.gz (127Kb): 127Kb downloaded
  Running setup.py egg_info for package PasteScript
Downloading/unpacking Paste>=1.3 (from PasteScript)
  Downloading Paste-1.7.2.tar.gz (373Kb): 373Kb downloaded
  Running setup.py egg_info for package Paste
Downloading/unpacking PasteDeploy (from PasteScript)
  Downloading PasteDeploy-1.3.3.tar.gz
  Running setup.py egg_info for package PasteDeploy
    warning: no files found matching 'docs/*.html'
    warning: no previously-included files found matching 'docs/rebuild'
Installing collected packages: Paste, PasteDeploy, PasteScript
...
Successfully installed Paste PasteDeploy PasteScript

3. Create the new project.

$ ./bin/paster create -t basic_package foogis
Selected and implied templates:
PasteScript#basic_package  A basic setuptools-enabled package

Variables:
  egg:      foogis
  package:  foogis
  project:  foogis
Enter version (Version (like 0.1)) ['']: 0.1
Enter description (One-line description of the package) ['']: FooGIS
Enter long_description (Multi-line description (in reST)) ['']:
Enter keywords (Space-separated keywords/tags) ['']: gis
Enter author (Author name) ['']: Sean Gillies
Enter author_email (Author email) ['']: sean@example.com
Enter url (URL of homepage) ['']: http://example.com/foogis
Enter license_name (License name) ['']: DWTFYWWI
Enter zip_safe (True/False: if the package can be distributed as a .zip file) [False]:
Creating template basic_package
Creating directory ./foogis
  Recursing into +package+
    Creating ./foogis/foogis/
    Copying __init__.py to ./foogis/foogis/__init__.py
  Copying setup.cfg to ./foogis/setup.cfg
  Copying setup.py_tmpl to ./foogis/setup.py
Running /tmp/foo/bin/python2.6 setup.py egg_info

What we get is

$ find foogis
foogis
foogis/foogis
foogis/foogis/__init__.py
foogis/foogis.egg-info
foogis/foogis.egg-info/dependency_links.txt
foogis/foogis.egg-info/entry_points.txt
foogis/foogis.egg-info/not-zip-safe
foogis/foogis.egg-info/PKG-INFO
foogis/foogis.egg-info/SOURCES.txt
foogis/foogis.egg-info/top_level.txt
foogis/setup.cfg
foogis/setup.py

The package code iself is in foogis/foogis. The foogis directory holds distribution files. Metadata, README, etc.

4. This is a good time to get everything under revision control (except the egg-info, as Tarek points out).

$ cd foogis
$ hg init
$ hg add --exclude *egg-info
adding foogis/__init__.py
adding setup.cfg
adding setup.py
$ hg commit -m "Start of the FooGIS project"

5. Install nose and coverage. Nose flattens the testing learning curve and coverage tells us how comprehensive our tests are.

$ cd ..
$ ./bin/pip install nose
Downloading/unpacking nose
...
Successfully installed nose
$ ./bin/pip install coverage
Downloading/unpacking coverage
...
Successfully installed coverage

6. Write some tests. The sooner we start testing, the better. Few things are more painful than writing tests a few hundred lines of code down the road.

$ cd foogis
$ vim foogis/tests.py

Here's the first test, taking advantage of nose's conventions for finding tests.

from foogis import Point

def test_foogis():
    assert Point(0.0, 0.0).x == 0.0

Nose lets you start testing immediately, avoiding the intricacies of unittest until you need them. Before we run the tests, we'll fully activate the virtual environment, adjusting executable paths so that we don't have to be explicit about them (It's true, as pointed out in comments, that we could have done this at the outset).

$ source ../bin/activate
$ which nosetests
/private/tmp/foo/bin/nosetests

Without any code, the tests fail, of course.

$ nosetests foogis
E
======================================================================
ERROR: Failure: ImportError (cannot import name Point)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/private/tmp/foo/lib/python2.6/site-packages/nose/loader.py", line 382, in loadTestsFromName
    addr.filename, addr.module)
  File "/private/tmp/foo/lib/python2.6/site-packages/nose/importer.py", line 39, in importFromPath
    return self.importFromDir(dir_path, fqname)
  File "/private/tmp/foo/lib/python2.6/site-packages/nose/importer.py", line 86, in importFromDir
    mod = load_module(part_fqname, fh, filename, desc)
  File "/private/tmp/foo/foogis/foogis/tests.py", line 1, in <module>
    from foogis import Point
ImportError: cannot import name Point

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

7. Write code and test.

$ vim foogis/__init__.py
class Point(object):
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
    def __repr__(self):
        return 'Point (%s %s)' % (self.x, self.y)

Now, we run nosetests again with the coverage module:

$ nosetests --with-coverage foogis
.
Name     Stmts   Exec  Cover   Missing
--------------------------------------
foogis       6      5    83%   6
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

The tests pass, but we're missing a test of the __repr__ method on line 6. Let's add one.

$ vim foogis/tests.py
from foogis import Point

def test_foogis():
    assert Point(0.0, 0.0).x == 0.0
    assert repr(Point(0.0, 0.0)) == 'Point (0.0 0.0)'

and re-run the tests.

$ nosetests --with-coverage foogis
.
Name     Stmts   Exec  Cover   Missing
--------------------------------------
foogis       6      6   100%
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

8. Commit the changes and make a distribution.

$ hg add foogis/tests.py
$ hg commit -m "Added a Point class, with tests"
$ python setup.py sdist
running sdist
running egg_info
writing foogis.egg-info/PKG-INFO
writing top-level names to foogis.egg-info/top_level.txt
writing dependency_links to foogis.egg-info/dependency_links.txt
writing entry points to foogis.egg-info/entry_points.txt
reading manifest file 'foogis.egg-info/SOURCES.txt'
writing manifest file 'foogis.egg-info/SOURCES.txt'
creating foogis-0.1dev
creating foogis-0.1dev/foogis
creating foogis-0.1dev/foogis.egg-info
making hard links in foogis-0.1dev...
hard linking setup.cfg -> foogis-0.1dev
hard linking setup.py -> foogis-0.1dev
hard linking foogis/__init__.py -> foogis-0.1dev/foogis
hard linking foogis/tests.py -> foogis-0.1dev/foogis
hard linking foogis.egg-info/PKG-INFO -> foogis-0.1dev/foogis.egg-info
hard linking foogis.egg-info/SOURCES.txt -> foogis-0.1dev/foogis.egg-info
hard linking foogis.egg-info/dependency_links.txt -> foogis-0.1dev/foogis.egg-info
hard linking foogis.egg-info/entry_points.txt -> foogis-0.1dev/foogis.egg-info
hard linking foogis.egg-info/not-zip-safe -> foogis-0.1dev/foogis.egg-info
hard linking foogis.egg-info/top_level.txt -> foogis-0.1dev/foogis.egg-info
copying setup.cfg -> foogis-0.1dev
Writing foogis-0.1dev/setup.cfg
creating dist
tar -cf dist/foogis-0.1dev.tar foogis-0.1dev
gzip -f9 dist/foogis-0.1dev.tar
removing 'foogis-0.1dev' (and everything under it)

The file at dist/foogis-0.1dev.tar.gz is ready to be distributed to users of our package. Let's get them to install it using pip.

$ pip install dist/foogis-0.1dev.tar.gz
Unpacking ./dist/foogis-0.1dev.tar.gz
  Running setup.py egg_info for package from file:///private/tmp/foo/foogis/dist/foogis-0.1dev.tar.gz
Installing collected packages: foogis
  Running setup.py install for foogis
Successfully installed foogis

Do read the comment below about disabling (in setup.cfg) the "dev" tag in the distribution version string. Paste's "basic_package" template isn't the optimal template for every developer community. I'm familiar with the many additional features of the "ZopeSkel" template from Zope and Plone. I can imagine that commercial or semi-commercial efforts to grow Python developer communities (particularly thinking of ESRI here) might also be served well by specialized project templates.

Comments

1see also

Jonathan Hartley, 2010-04-01T11:45:39Z

Brilliant, many thanks. In particular I didn't realise coverage was so easy to use, and I've never got to grips with paster - I'll give it a try now.

Also, for people looking for more of the same, this nicely complements the following, which goes into more detail on some aspects:

http://infinitemonkeycorps.net/docs/pph/

2Re: Bootstrapping a Python project

Sean, 2010-04-01T12:05:48Z

Thanks, Jonathan. John Kleint's howto looks excellent, and does indeed cover important stuff that i skipped, such as how to write a readme and documentation.

3Re: Bootstrapping a Python project

Tarek Ziadé, 2010-04-01T15:06:32Z

Nice article !

One minor point: I would not put the *.egg-info dir under revision control. It's a generated content that will change all the time, and that is not required when people get the source from the repository.

Btw: Would you be interested to include this document in the HitchHicker's guide to packaging ? (which is planned to be included in the official docs.python.org at some point) I think it's a great help for people to get started.

4Re: Bootstrapping a Python project

Sean, 2010-04-01T16:11:25Z

Sure, Tarek, after a little more feedback it might be worth including. I always try to avoid committing the egg-info, or remove it soon after accidentally committing it. The --exclude option (now used above) is handy.

5dev releases

Kevin Teague, 2010-04-01T17:47:51Z

It's also worth mentioning that setup.cfg needs to be edited before a release is made so that there is no "tag_build = dev" in the [egg_info] section (and you can get rid of the "tag_svn_revision = true" if you aren't using SVN). User's generally shouldn't be installing dev releases since they are unversioned.

Personally, I get rid of the setup.cfg and put append dev inside the setup.py file, so that there is one less file to have to think about. Opinions on this file go both ways though ...

http://philikon.wordpress.com/2008/06/05/setupcfg-considered-harmful/

6Re: Bootstrapping a Python project

Brian, 2010-04-01T19:44:50Z
Install paster if it wasn't already installed under our original Python (or if you used the --no-site-packages option)

Does pip respect http_proxy? I'm pretty sure urllib2 supports the http_proxy variable, so I imagine it probably does...right?

7Re: Bootstrapping a Python project

Marius Gedminas, 2010-04-01T19:57:11Z

Good article!

Personally I use zc.buildout instead of virtualenv: so it's one less directory level to handle, and the sandbox creation can be automated (which is useful for other people who want to check out your source tree and start working on it). It has downsides (another config file in your tree; poor documentation) and upsides (common 3rd packages are can be shared between many projects, which speeds up downloading/installing of new environments and is a killer feature in my book).

One important point that bit me recently: nose --with-coverage remembers coverage results from previous runs, so if you change something and re-run, you won't know your actual coverage. *Always* use nosetests --with-coverage --cover-erase.

8Re: Bootstrapping a Python project

Benjamin Sergeant, 2010-04-02T05:22:33Z

This foogis package looks very cool, can I download it somewhere ?

;) ... I'm in the April's fool mood ... didn't know about repr and pastescript, good stuff.

9Re: Bootstrapping a Python project

srid, 2010-04-02T13:21:09Z

Do take a look at modern-package-template as an alternative for basic_package: http://pypi.python.org/pypi/modern-package-template

Comments are closed after 13 days.

about archive feed find

Some rights reserved by Sean Gillies.