26

For traditional Python projects with a setup.py, there are various ways of ensuring that the version string does not have to be repeated throughout the code base. See PyPA's guide on "Single-sourcing the package version" for a list of recommendations.

Many are trying to move away from setup.py to setup.cfg (probably under the influence of PEP517 and PEP518; setup.py was mostly used declaratively anyway, and when there was logic in setup.py, it was probably for the worse.) This means that most the suggestions won't work anymore since setup.cfg cannot contain "code".

How can I single-source the package version for Python projects that use setup.cfg?

3
  • I would not call PyPA and their guides, and tutorials official (but yes they're the closest to official there is for Python packaging). setup.cfg has not much to do with PEP 517, PEP 518, it existed long before (but yes, I see your point). And you want version 5 of of the PyPA recommendations. Commented Feb 27, 2020 at 10:01
  • @sinoroc Thanks a lot for your comment. Feel free to edit the post to make it more correct, I'll approve straight away. Commented Feb 27, 2020 at 10:03
  • 1
    Sorry for the nit-pick, definitely wasn't necessary. Anyway, doesn't directly answer your question since it uses code in setup.py, but this is how I do it in my projects: sinoroc.gitlab.io/kb/python/project_version.html Commented Feb 27, 2020 at 10:24

1 Answer 1

37

There are a couple of ways to do this (see below for the project structure used in these examples):

1.

setup.cfg

[metadata]
version = 1.2.3.dev4

src/my_top_level_package/__init__.py

import importlib.metadata
__version__ = importlib.metadata.version('MyProject')

2.

setup.cfg

[metadata]
version = file: VERSION.txt

VERSION.txt

1.2.3.dev4

src/my_top_level_package/__init__.py

import importlib.metadata
__version__ = importlib.metadata.version('MyProject')

3.

setup.cfg

[metadata]
version = attr: my_top_level_package.__version__

src/my_top_level_package/__init__.py

__version__ = '1.2.3.dev4'

And more...

There are probably other ways to do this, by playing with different combinatons.


References:


Structure assumed in the previous examples is as follows...

MyProject
├── setup.cfg
├── setup.py
└── src
    └── my_top_level_package
        └── __init__.py

setup.py

#!/usr/bin/env python3

import setuptools

if __name__ == '__main__':
    setuptools.setup(
        # see 'setup.cfg'
    )

setup.cfg

[metadata]
name = MyProject
# See above for the value of 'version = ...'

[options]
package_dir =
    = src
packages = find:

[options.packages.find]
where = src
$ cd path/to/MyProject
$ python3 setup.py --version
1.2.3.dev4
$ python3 -m pip install .
# ...
$ python3 -c 'import my_top_level_package; print(my_top_level_package.__version__)'
1.2.3.dev4
$ python3 -V
Python 3.6.9
$ python3 -m pip list
Package       Version   
------------- ----------
MyProject     1.2.3.dev4
pip           20.0.2    
pkg-resources 0.0.0     
setuptools    45.2.0    
wheel         0.34.2    
zipp          3.0.0
Sign up to request clarification or add additional context in comments.

19 Comments

The importlib.metadata variant as the disadvantage that it always gives the version of the installed package, even if you use a checkout of devel master, for example.
Option (3.) does not work. The package won't install locally using pip install . with seemingly unrelated ModuleNotFoundErrors. When using version = 1.2.3 in the [metadata], it all works. Might be a pip bug, not sure.
(3) doesn't work for me. When putting my code in src/foobar, pip install . tells me ModuleNotFoundError: No module named 'foobar' -- understandable, too. Maybe I misunderstand my_top_level_package?
@red888 It is out of scope for this question but yes. In setup.py in the setuptools.setup() function call, you could write something like version=os.getenv('MYPROJECT_VERSION').
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.