#! /usr/bin/python

"""versioning.py

This file helps distutils-based projects manage their version number by just
creating version-control tags.

For developers who work from a VCS-generated tree (e.g. 'git clone' etc),
each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a
version number by asking your version-control tool about the current
checkout. The version number will be written into a generated _version.py
file of your choosing, where it can be included by your __init__.py

For users who work from a VCS-generated tarball (e.g. 'git archive'), it will
compute a version number by looking at the name of the directory created when
te tarball is unpacked. This conventionally includes both the name of the
project and a version number.

For users who work from a tarball built by 'setup.py sdist', it will get a
version number from a previously-generated _version.py file.

As a result, loading code directly from the source tree will not result in a
real version. If you want real versions from VCS trees (where you frequently
update from the upstream repository, or do new development), you will need to
do a 'setup.py version' after each update, and load code from the build/
directory.

You need to provide this code with a few configuration values:

 versionfile: a project-relative pathname into which the generated version
              strings should be written. This is usually a _version.py next
              to your project's main __init__.py file. If your project uses
              src/myproject/__init__.py, this should be
              'src/myproject/_version.py'. This file should *not* be checked
              in to your VCS: it will be generated during 'build' and 'sdist'
              commands (in fact this file should be added to your .gitignore
              or equivalent). Your __init__.py should then do:

                from _version import __version__

 tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all
             VCS tags. If your tags look like 'myproject-1.2.0', then you
             should use tag_prefix='myproject-'. If you use unprefixed tags
             like '1.2.0', this should be an empty string.

 parentdir_prefix: a string, frequently the same as tag_prefix, which
                   appears at the start of all unpacked tarball filenames. If
                   your tarball unpacks into 'myproject-1.2.0', this should
                   be 'myproject-'.

To use it, include this file in the top level of your project (don't forget
to modify MANIFEST.in to include it in generated source tarballs, as well as
the generated _version.py file), and make the following changes to your
setup.py:

 import versioning
 versioning.versionfile = 'src/myproject/_version.py'
 versioning.tag_prefix = '' # tags are like 1.2.0
 versioning.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0'
 versioning.update_version() # this updates _version.py each time

 # in the setup() call arguments, include the following:
  version=versioning.get_version(),
  cmdclass={'version': versioning.cmd_version,
            'build': versioning.cmd_build,
            'sdist': versioning.cmd_sdist },
"""

import os, sys, subprocess, re
from distutils.core import Command
from distutils.command.sdist import sdist as _sdist
from distutils.command.build import build as _build

versionfile = None
tag_prefix = None
parentdir_prefix = None

VERSION_PY = """
# This file was generated by 'versioning.py' from revision-control system
# data, or from the parent directory name of an unpacked source archive.
# Distribution tarballs contain a pre-generated copy of this file.

__version__ = '%s'
"""

class NoVersionError(Exception):
    pass

def version_from_git_describe(tag_prefix, verbose=False):
    if not os.path.isdir(".git"):
        if verbose:
            print "This does not appear to be a Git repository."
        return None
    try:
        p = subprocess.Popen(["git", "describe",
                              "--tags", "--dirty", "--always"],
                             stdout=subprocess.PIPE)
    except EnvironmentError, e:
        if verbose:
            print "unable to run git"
            print e
        return None
    stdout = p.communicate()[0].strip()
    if p.returncode != 0:
        if verbose:
            print "unable to run git (error)"
        return None
    if not stdout.startswith(tag_prefix):
        if verbose:
            print "tag '%s' doesn't start with prefix '%s'" % \
                  (stdout, tag_prefix)
        return None
    return stdout[len(tag_prefix):]

def version_from_file(filename):
    try:
        f = open(filename)
    except EnvironmentError:
        return None
    for line in f.readlines():
        mo = re.match("__version__ = '([^']+)'", line)
        if mo:
            ver = mo.group(1)
            return ver
    return None

def version_from_parentdir(tag_prefix, parentdir_prefix, verbose):
    # try a couple different things to handle py2exe, bbfreeze, and
    # non-CPython implementations
    try:
        me = __file__
    except NameError:
        me = sys.argv[0]
    me = os.path.abspath(me)
    dirname = os.path.basename(os.path.dirname(me))
    if not dirname.startswith(parentdir_prefix):
        if verbose:
            print "dirname '%s' doesn't start with prefix '%s'" % \
                  (dirname, parentdir_prefix)
        return None
    return dirname[len(parentdir_prefix):]

def write_to_version_file(filename, ver):
    f = open(filename, "w")
    f.write(VERSION_PY % ver)
    f.close()
    print "set %s to '%s'" % (filename, ver)

def get_best_version(versionfile, tag_prefix, parentdir_prefix,
                     default=None, verbose=False):
    # extract version from first of: 'git describe', _version.py, parentdir.
    # This is meant to work for developers, for users of a tarball created by
    # 'setup.py sdist', and for users of a tarball/zipball created by 'git
    # archive' or github's download-from-tag feature.

    ver = version_from_git_describe(tag_prefix, verbose)
    if ver is not None:
        if verbose: print "got version from git"
        return ver

    ver = version_from_file(versionfile)
    if ver is not None:
        if verbose: print "got version from file %s" % versionfile
        return ver

    ver = version_from_parentdir(tag_prefix, parentdir_prefix, verbose)
    if ver is not None:
        if verbose: print "got version from parentdir"
        return ver

    ver = default
    if ver is not None:
        if verbose: print "got version from default"
        return ver

    raise NoVersionError("Unable to compute version at all")

def get_version(verbose=False):
    assert versionfile is not None, "please set versioning.versionfile"
    assert tag_prefix is not None, "please set versioning.tag_prefix"
    assert parentdir_prefix is not None, "please set versioning.parentdir_prefix"
    return get_best_version(versionfile, tag_prefix, parentdir_prefix,
                            verbose=verbose)

def update_version(verbose=False):
    ver = get_version(verbose=verbose)
    write_to_version_file(versionfile, ver)
    return ver

class cmd_version(Command):
    description = "report generated version string"
    user_options = []
    boolean_options = []
    def initialize_options(self):
        pass
    def finalize_options(self):
        pass
    def run(self):
        ver = get_version(verbose=True)
        print "Version is currently:", ver

class cmd_build(_build):
    def run(self):
        ver = update_version() # just in case
        # unless we update this, the command will keep using the old version
        self.distribution.metadata.version = ver
        return _build.run(self)

class cmd_sdist(_sdist):
    def run(self):
        ver = update_version()
        self.distribution.metadata.version = ver
        return _sdist.run(self)
