63

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-...)?

Community
  • 1
  • 1
Sergey Vasilyev
  • 3,919
  • 3
  • 26
  • 37
  • Also see [single-sourcing package version](https://packaging.python.org/guides/single-sourcing-package-version/) in Python's packaging documentation. – djvg Oct 06 '21 at 07:57
  • Related: https://stackoverflow.com/q/60430112 – djvg Oct 06 '21 at 08:25

7 Answers7

43

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?

sorin
  • 161,544
  • 178
  • 535
  • 806
merwok
  • 6,779
  • 1
  • 28
  • 42
  • 1
    You mean `git tag -a v$(setup.py --version) -m 'description of version v$(setup.py --version)'`, right? – Kalle Richter Jan 16 '15 at 19:15
  • 4
    The issue I ran into with this is that `__init__.py` imports your modules, which in turn import your external dependencies which setup.py will install _after_ processing `__init__.py` for your version number. Ergo: this only works if you have no external dependencies. – J0hnG4lt Feb 04 '15 at 14:31
  • 3
    Yes, that’s why I said to parse (i.e. use open, read and string matching operations) that file, not import it. – merwok Feb 04 '15 at 18:56
  • If you get "permission denied" because `setup.py` isn't marked executable (e.g. you cloned from github), just add `python` to the command: `git tag -a v$(python setup.py --version) -m 'description of version'` – hobs Feb 29 '16 at 02:24
  • Is there an example of what `__init__.py` should look like with the version number? How would you parse it from `setup.py`? – Stevoisiak Apr 09 '18 at 17:39
  • You don’t parse it! `python setup.py --version` prints the version number to stdout. – merwok Apr 10 '18 at 16:20
  • @ÉricAraujo What would `__init__.py` and `setup.py` look like? Can you show an example? – Stevoisiak May 09 '18 at 16:26
27

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.

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 2
    So, as I understood, it is better to provide this generated version.py ONLY in packages (tar.gz&K), whilst using some kind of hard-coded "dev" version number when working from *working copy* code. And not to hold this version.py under code control at all, so as in working copy, but only temporarily when generating the package. Right? – Sergey Vasilyev Jul 22 '11 at 07:39
  • 1
    @Sergey: yes, that is the general idea: "not to hold this version.py under code control at all": I confirm. – VonC Jul 22 '11 at 08:00
  • 1
    Can you provide an example of how to accomplish this? – Stevoisiak Apr 09 '18 at 17:36
12

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).

Sergey Vasilyev
  • 3,919
  • 3
  • 26
  • 37
  • 4
    This solution is used by both [pandas](https://github.com/pandas-dev/pandas/blob/7d37ab85c9df9561653c659f29c5d7fca1454c67/pandas/__init__.py#L182-L188) and [matplotlib](https://github.com/matplotlib/matplotlib/blob/2bdf82b7d3103271d62a8ca73d55bec7f494f12d/lib/matplotlib/__init__.py#L114-L118) – joelostblom Feb 21 '20 at 17:22
10

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,
 .....
)
dim_user
  • 969
  • 1
  • 13
  • 24
5

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.

SeF
  • 3,864
  • 2
  • 28
  • 41
4

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,
    ....
Community
  • 1
  • 1
Sven
  • 985
  • 1
  • 11
  • 27
2

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.

Dorian B.
  • 1,101
  • 13
  • 22