A problem that comes up every time I make a new Pypi package is this: I want software version information to be included inside my python package and inside setup.py in a way that has a DRY and single source of truth'y but also in such a way that I can increment the version automatically from release tools. This post discusses a solution to this problem and is part of a series discussing other tricks that can make release automation tasks easier.

Project layout

You might already have a directory layout for your python package, but if not it's time to choose one. The best project layout for you will obviously depend on various things like whether you are releasing a library, a command-line application, whether your code also requires support-data, etc. There are plenty of full-service project templates out there, most of which are pretty opinionated. If you use cookiecutter, then have a look at the options here. Choose one you like, I'll wait...

DRY versioning

Hopefully you now have a project template that doesn't suck, but you might need some small changes to the skeleton to support DRY version data. First, I'm assuming your new project layout looks something like what you'll find below. If the version.py and fabfile.py files don't exist, create them. I'll describe the contents later.

pkg_root/
|-- setup.py
|-- fabfile.py
|-- py_pkg
    |-- __init__.py
    `-- version.py

Version.py

The version.py file contents should look something like what you see below. Keep this file simple! Don't use any imports because you want setup.py to still be able to import the version number, even when your package and it's dependencies are not yet installed.

# -*- coding: utf-8 -*-
""" py_pkg.version
"""
__version__ = 0.1

Setup.py

Inside setup.py, import the version data and put it in the call to setup() as illustrated below:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" this is pkg_root/setup.py """

# Inside pkg_root/setup.py, make sure we can import the version
# number so that it doesn't have to be changed in two places.  Note
# also that `pkg_root/py_pkg/__init__.py` is also free to import various
# requirements that haven't been installed yet
sys.path.append(os.path.join(os.path.dirname(__file__), 'pkg_name'))
from version import __version__ as release_version  # flake8: noqa
sys.path.pop()

setup(
  # ... stuff ..
  version=release_version,
  # ... more stuff ..
)

Once everything above is configured, you can change everything by hand if you want to.. at least now you only have to change it in once place. However if you're interested in bumping the version from commandline without editing a file, or maybe bumping it from your buildbot after a test job passes, read on.

Warning! The version-bumper discussed here assumes naive incremental decimal versioning and it won't JustWork™ with stuff like semver out of the box. The reason for that is because then the release automation gets somewhat less, well, automatic, in that case because the bump_version command would have parse tuples and accept arguments that described whether your changes were backwards compatible. If you need something more sophisticated and don't feel like tweaking the approach used here, you should consider using zest.releaser instead.

Fabfile.py

I implement lots of simple automation using fabric, and so it is for the bump_version command. To install fabric itself you'll need to run the command below:

pip install fabric

The fabfile with the bump_version command is gisted here and included below. Basically you just need to run "fab bump_version" and it will rewrite the version.py file described earlier in this post.

[gist ac7c3fc1da4e696e58976324c414c72e]