How can I rewrite python __version__ with git?

Question:

I would like to define a __version__ variable in my module which should be automatically updated on git commit similarly to what SVN keywords do. Is there a way to achieve it in Git? Does anyone have a working example?

I considered using GitPython, but I would not like to introduce another dependency and I want users who download the module from SVN repo or as a zip package to have the same versioning scheme (I do not care that is some illegible hash).

Edit: My particular problem is that I have to run simulations whose result depend on the exact version of the simulation script. Therefore each time I have to store version number together with the simulation results. If both run out of sync, it may have very dire outcomes.

Asked By: btel

||

Answers:

It might be better to do this as part of your packaging, rather than after every commit.

There are two primary options:

  • Use git-archive to package, and use the export-subst attribute. Unfortunately, the things you can substitute in are limited to the placeholders from git log --format=.... For example, you could write __version__ = $Format:%H$ in your file, put <filename> export-subst in your .gitattributes, and when you run git archive, that’d be changed to the full hash of the commit you’re archiving with. This is just about what you’re asking for, but I do prefer the next option.

  • Do it yourself as part of a packaging process (often a build process for compiled packages), and use git describe. That will get you a nice pretty string like v1.7.4.1-59-ge3d3f7d, meaning “59 commits past the tag v1.7.4.1, at commit ge3d3f7d” which you can then insert somehow into the right place in your code as you package/build. This is what Git itself does; the result is dumped to a file, whose contents are read into the makefile and then passed into the build via a -D preprocessor option, and placed into various filenames (e.g. the release tarball) directly.

If you really, really want to do this after every commit, you could, with a post-commit hook, but then only you (and those you give the hook to) will have it, and it’s very very possible to get out of sync – you’ll also have to have a post-checkout hook, and so on and so on. It’s really better for whatever processes that create something needing this version number to get it themselves.

You could also use a smudge/clean filter, which would be more like what you actually want (rather than simply after every commit).

Answered By: Cascabel

For those finding this question in 2018 you can also use Versioneer. Once enabled, it automatically sets version in setup.py and __version__ in your module based on the latest Git tag at time of build.

For example, if you build your project at tag 1.0.0, Versioneer will set the project’s version to 1.0.0. If you do two more commits, edit some without checking in, and build, Versioneer will set the version to something like 1.0.0+2.g1076c97.dirty.

Of course you can customize which tags Versioneer recognizes as version tags.

This is also how larger projects like pandas and matplotlib handle their versioning.

Answered By: jpyams

Another possibility other than Versioneer is setuptools_scm.

I have successfully implemented something very similar to the OP by adding the following to setup.py (or by modifying it accordingly):

from setuptools import setup
setup(
    ...,
    use_scm_version=True,
    setup_requires=['setuptools_scm'],
    ...,
)

and, in order to have __version__ updated automatically, added this to __init__.py of my package:

from pkg_resources import get_distribution, DistributionNotFound
try:
    __version__ = get_distribution(__name__).version
except DistributionNotFound:
    # package is not installed
    pass
Answered By: Pincopallino

You can use the following code to retrieve your current git commit version (reported as either a commit id on a branch or as a tag if a tag has been added:

from git import Repo

def GetGitVersion():
    '''report the git commit/branch/tag on which we are '''
    repo = Repo(".", search_parent_directories=True)
    git = repo.git    

    branchOrTag=git.rev_parse('--abbrev-ref', 'HEAD')

    if branchOrTag == 'HEAD':
        # get tag name
        # since several tags are sometime put on the same commit we want to retrieve all of them
        # and use the last one as reference
        # Note:
        # branchOrTag=`git describe --tags --exact-match` does not provided the latest created tag in case several point to the same place
        currentSha=git.rev_parse('--verify','HEAD')

        # list all tags on the current sha using most recent first:
        allTags=git.tag('--points-at',currentSha,'--sort=-creatordate')
        print (allTags)

        allTagsArray=allTags.split(' ') #create an array (assuming tags are separated by space)

        # if we checkouted a commit with no tag associated, the allTagsArray is empty we can use directly the sha value
        if len(allTagsArray) == 0:
            branchOrTag=git.rev-rev_parse('--short','HEAD') # take the short sha
        else:

            branchOrTag=allTagsArray[0] #first from the list
    else:
        #add the head commit id on the current branch
        branchOrTag="{}[{}]".format(branchOrTag,git.rev_parse('--short', 'HEAD'))

    return branchOrTag
if __name__ == "__main__":
    print (GetGitVersion())
Answered By: Jean-Marc Volle

(1) One option is to get the version number in real-time from the release version written into a published package. To do this, you would add a dependency to __init__.py, release the product with something like setup.py and at runtime execute python3 setup.py --version. This approach uses the lightweight importlib_metadata module [importlib_metadata (for pre Python 3.8) and importlib.metadata (for Python 3.8+)]:

from importlib.metadata import version, PackageNotFoundError
# pre-3.8 import statement
# from importlib_metadata import version, PackageNotFoundError

VERSION_FALLBACK = "0.0.0"
try:
    __version__ = version(__name__)
except PackageNotFoundError:
    # package is not installed
    # assign signal or sane value as a default
    __version__ = VERSION_FALLBACK
    pass

This implements metadata recommendations from PEP 566. If you release with setuptools>=42.0.0 this works great, and it may work with packages released by other tooling, also.

(2) A second option is to do something with Git to collect the last tag value (assuming you are tagging the application). Then increment the point version number. Then replace the value in the init file with the new value and tag with the new value.

# get and increment semantic version
version=$( git tag --list | sort -rV | head -1 );  # v0.1.1
version_point=${version##*.}  # 1
version_point=$((${version_point} + 1))  # 2
version="${version%.*}.${version_point}"  # v0.1.2
# replace in __init__.py (NOTE: THIS OVERWRITES!)
cat __init.py__ | sed -e "s/VERSION=.*/VERSION=${version}/i" > __init__.py
git add __init__.py && git commit -m "Updated version in __init__.py"
git tag -a ${version} -m "Latest tagged version"
Answered By: ingyhere

My approach has been to package a file ‘version.txt’ in a directory (say "metadata") which contains the output from the command

git describe --long --tags --dirty --always >metadata/version.txt

together with the Python source files.

In the script that runs the Python application, there is then a command to

export GIT_VERSION=`cat metadata/version.txt`

running just before the application is started.

The code can then on startup run

git_version = os.getenv('GIT_VERSION', None)

to read

Answered By: MikeW
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.