Automatic version number both in setup.py (setuptools) AND source code?

Question:

SITUATION:

I have a python library, which is controlled by git, and bundled with distutils/setuptools. And I want to automatically generate version number based on git tags, both for setup.py sdist and alike commands, and for the library itself.

For the first task I can use git describe or alike solutions (see How can I get the version defined in setup.py (setuptools) in my package?).

And when, for example, I am in a tag ‘0.1’ and call for ‘setup.py sdist’, I get ‘mylib-0.1.tar.gz’; or ‘mylib-0.1-3-abcd.tar.gz’ if I altered the code after tagging. This is fine.

THE PROBLEM IS:

The problem comes when I want to have this version number available for the library itself, so it could send it in User-Agent HTTP header as ‘mylib/0.1-3-adcd’.

If I add setup.py version command as in How can I get the version defined in setup.py (setuptools) in my package?, then this version.py is generated AFTER the tag is made, since it uses the tag as a value. But in this case I need to make one more commit after the version tag is made to make the code consistent. Which, in turns, requires a new tag for further bundling.

THE QUESTION IS:

How to break this circle of dependencies (generate-commit-tag-generate-commit-tag-…)?

Asked By: Sergey Vasilyev

||

Answers:

A classic issue when toying with keyword expansion šŸ˜‰

The key is to realize that your tag is part of the release management process, not part of the development (and its version control) process.

In other word, you cannot include a release management data in a development repository, because of the loop you illustrates in your question.

You need, when generating the package (which is the “release management part”), to write that information in a file that your library will look for and use (if said file exists) for its User-Agent HTTP header.

Answered By: VonC

You could also reverse the dependency: put the version in mylib/__init__.py, parse that file in setup.py to get the version parameter, and use git tag $(setup.py –version) on the command line to create your tag.

git tag -a v$(python setup.py --version) -m 'description of version'

Is there anything more complicated you want to do that I havenā€™t understood?

Answered By: merwok

Following OGHaza’s solution in a similar SO question I keep a file _version.py that I parse in setup.py. With the version string from there, I git tag in setup.py. Then I set the setup version variable to a combination of version string plus the git commit hash. So here is the relevant part of setup.py:

from setuptools import setup, find_packages
from codecs import open
from os import path
import subprocess

here = path.abspath(path.dirname(__file__))

import re, os
VERSIONFILE=os.path.join(here,"_version.py")
verstrline = open(VERSIONFILE, "rt").read()
VSRE = r"^__version__ = ['"]([^'"]*)['"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
    verstr = mo.group(1)
else:
    raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))
if os.path.exists(os.path.join(here, '.git')):
    cmd = 'git rev-parse --verify --short HEAD'
    git_hash = subprocess.check_output(cmd)
    # tag git
    gitverstr = 'v' + verstr
    tags =  subprocess.check_output('git tag')
    if not gitverstr in tags:
        cmd = 'git tag -a %s %s -m "tagged by setup.py to %s"' % (gitverstr, git_hash, verstr)        
        subprocess.check_output(cmd)
    # use the git hash in the setup
    verstr += ', git hash: %s' % git_hash

setup(
    name='a_package',
    version = verstr,
    ....
Answered By: Sven

Since this topic is still alive and sometimes gets to search results, I would like to mention another solution which first appeared in 2012 and now is more or less usable:

https://github.com/warner/python-versioneer

It works in different way than all mentioned solutions: you add git tags manually, and the library (and setup.py) reads the tags, and builds the version string dynamically.

The versionĀ string includes the latest tag, distance from that tag, current commit hash, “dirtiness”, and some other info. It has few different version formats.

But it still has no branch name for so called “custom builds”; and commit distance can be confusing sometimes when two branches are based on the same commit, so it is better to tag & release only one selected branch (master).

Answered By: Sergey Vasilyev

Eric’s idea was the simple way to go, just in case this is useful here is the code I used (Flask’s team did it this way):

import re
import ast

_version_re = re.compile(r'__version__s+=s+(.*)')

with open('app_name/__init__.py', 'rb') as f:
    version = str(ast.literal_eval(_version_re.search(
        f.read().decode('utf-8')).group(1)))

setup(
    name='app-name',
    version=version,
 .....
)
Answered By: dim_user

As was mentioned in another answer, this is related to the release process and not to the development process, as such it is not a git issue in itself, but more how is your release work process.

A very simple variant is to use this:

python setup.py egg_info -b ".`date '+%Y%m%d'`git`git rev-parse --short HEAD`" build sdist

The portion between the quotes is up for customization, however I tried to follow the typical Fedora/RedHat package names.

Of note, even if egg_info implies relation to .egg, actually it’s used through the toolchain, for example for bdist_wheel as well and has to be specified in the beginning.

In general, your pre-release and post-release versions should live outside setup.py or any type of import version.py. The topic about versioning and egg_info is covered in detail here.

Example:

  • v1.3.4dev.20200813gitabcdef0
  • The v1.3.4 is in setup.py or any other variation you would like
  • The dev and 20200813gitabcdef0 is generated during the build process (example above)
  • None of the files generated during build are checked in git (usually in .gitignore they are filtered by default); sometimes there is a separate "deployment" repository, or similar, completely separate from the source one

A more complex way would be to have your release work process encoded in a Makefile which is outside the scope of this question, however a good source of inspiration can be found here and here. You will find good correspondeces between Makefile targets and setup.py commands.

Answered By: Dorian B.

If you found versioneer excessively convoluted, you can try bump2version.

Just add the simple bumpversion configuration file in the root of your library. This file indicates where in your repository there are strings storing the version number. Then, to update the version in all indicated places for a minor release, just type:

bumpversion minor

Use patch or major if you want to release a patch or a major.

This is not all about bumpversion. There are other flag-options, and config options, such as tagging automatically the repository, for which you can check the official documentation.

Answered By: SeF