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.