In the course of formulating the above question, I incrementally solved it to find three methods that work:
- Two methods use the
attr: special directive in setup.cfg. Of these:
- One puts the version number directly in the package’s
__init__.py file.
- The other puts the version number in a separate file (
__version__.py) and then __init__.py imports the version string from that separate file.
- The third method uses instead the
file: special directive in setup.cfg.
- This reads the separate version-specifying file (
VERSION) directly and doesn’t involve the package’s __init__.py file at all.
In light of these three possibilities, I re-present the directory/file structure with the addition of the two version-specifying files __version__.py and VERSION:
my-project/
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.cfg
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── __version__.py
│ ├── VERSION
│ └── my_module.py
└── tests/
Of course, you’d have at most one of those two files, depending on which of the three solutions you chose to implement.
The two attr: special-directive solutions
In both of the solutions using the setup.cfg’s attr: special-directive, setup.cfg obtains the import package’s version from the import package’s __version__ attribute. (When you import some_package that has a version, use dir(some_package) and you’ll see that it has a __version__ attribute.) Now you see the connection here between the attr: name of the special directive and our goal.
The key task: how to assign the __version__ attribute to my_package?
We assign the __version__ attribute, either directly or indirectly, using the package’s __init__.py file, which already exists (assuming you have a traditional package rather than a namespace package, which is outside the scope of this answer).
The snippet in setup.cfg that is common in both Method A and Method B
In both of these attr: special-directive solutions, the configuration of the setup.cfg file is the same, with the following snippet:
[metadata]
name = my-project
version = attr: my_package.__version__
To be clear, here the .__version__ references an attribute, not a file, subpackage, or anything else.
Now we branch depending on whether the version information goes directly into __init__.py or instead into its own file.
Method A: Put the assignment into the package’s __init__.py file
This method doesn’t use a separate file for specifying the version number, but rather inserts it into the package’s __init__.py file:
# path-to/my-project/src/my_package/__init__.py
__version__ = '0.0.2'
Note two elements of the assignment:
- the left-hand side (
__version__) corresponds to the attr: line in setup.cfg (version = attr: my_package.__version__)
- the legitimate version string on the right-hand side is a string enclosed by quotes.
We’re done with Method A.
Method B: Put the assignment into a __version__.py file and import it in __init__.py
Create __version__.py and put the version string in it
We construct a new Python file and locate it at the same level as the import package’s __init__.py file.
We insert the exact same __version__ directive that we inserted in __init__.py in Method A:
# my-project/src/my_package/__version__.py
__version__ = '0.0.2'
From within __init__.py, import __version__ from __version__.py
In __init__.py, we do a relative import to access the __version__ that was assigned in the separate file:
# path-to/my-project/src/my_package/__init__.py
from . __version__ import __version__
To unpack this a little…
We’re done with Method B.
Method C: Using the file: special directive
The separate file is populated differently
In this method, we use a separate file for the version number, as in Method B. Unlike Method B, we read the contents of this file directly, rather than importing it.
To prevent confusion, I’ll call this file simply VERSION. Like __init__.py and Method B’s __version__.py, VERSION is at the root level of import package. (See the directory/file diagram.) (Of course, in this method, you won’t have __version__.py.)
However, the contents of this VERSION file are much different than the contents of Method B’s __version__.py.
Here’s the contents of my-project/src/my_package/VERSION:
0.0.2
Note that:
- This file contains nothing but the contents of the version string itself. In particular, do not enclose this string in quotation marks!
- There’s also no assignment syntax going on; there’s no “
__version__ = ” preamble to the assignment string.
- This isn’t even a Python file, so I didn’t include a comment string with the path to the file, because that would be enough to give the error
VERSION does not comply with PEP 440: # comment line.
setup.cfg is different than before
There are two points of note that distinguish setup.cfg in Method C from the setup.cfg that was common to both Methods A and B.
setup.cfg uses file: rather than attr:
In Method C, we use a different formulation in setup.cfg, swapping out the attr: special directive and replacing it with the file: special directive. The new snippet is:
[metadata]
name = my-project
version = file: src/my_package/VERSION
The file path to VERSION is relative to the project directory
Note the path to VERSION in the assignment statement: src/my_package/VERSION.
The relative file path to the VERSIONS file is relative to the root of the project directory my-project. This differs from Method B, where the relative import was relative to the import-package root, i.e., my_package.
We’re done with Method C.
Pros and cons
Method A might be seen to have a virtue of needing no additional file to set the version (because, in addition to setup.cfg, which is needed in any case, Method A uses only __init__.py, which likewise already exists). However, having a separate file for the version number has its own virtue of being obvious where the version number is set. In Method A, sending someone to change the version number who didn’t already know where it was stored might take a while; it wouldn’t be obvious to look in __init__.py.
Method C might seem to have the advantage over Method B, because Method B requires modification to two files (__init__.py and __version__.py) rather than only one for Method C (VERSION). The only perhaps countervailing advantage of Method B is that its __version__.py is a Python file that allows embedded comments, which Method C’s VERSION does not.