Bootstrapping a Python project
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.
Here's the first test, taking advantage of nose's conventions for finding tests.
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).
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.
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.
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
see also
Author: Jonathan Hartley
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/
Re: Bootstrapping a Python project
Author: Sean
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.
Re: Bootstrapping a Python project
Author: Tarek Ziadé
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.
Re: Bootstrapping a Python project
Author: Sean
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.
dev releases
Author: Kevin Teague
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/
Re: Bootstrapping a Python project
Author: Brian
Does pip respect http_proxy? I'm pretty sure urllib2 supports the http_proxy variable, so I imagine it probably does...right?
Re: Bootstrapping a Python project
Author: Marius Gedminas
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.
Re: Bootstrapping a Python project
Author: Benjamin Sergeant
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.
Re: Bootstrapping a Python project
Author: srid
Do take a look at modern-package-template as an alternative for basic_package: http://pypi.python.org/pypi/modern-package-template