Dynamic versioning library and CLI
Dunamai
Dunamai is a Python 3.5+ library and command line tool for producing dynamic, standards-compliant version strings, derived from tags in your version control system. This facilitates uniquely identifying nightly or per-commit builds in continuous integration and releasing new versions of your software simply by creating a tag.Dunamai is also available as a GitHub Action.
Features
- Version control system support:
- Version styles:
- Can be used for projects written in any programming language.
Usage
Installation
pip install dunamai
CLI
# Suppose you are on commit g29045e8, 7 commits after the v0.2.0 tag.
Auto-detect the version control system and generate a version:
$ dunamai from any
0.2.0.post7.dev0+g29045e8
Or use an explicit VCS and style:
$ dunamai from git --no-metadata --style semver
0.2.0-post.7
Custom formats:
$ dunamai from any --format "v{base}+{distance}.{commit}"
v0.2.0+7.g29045e8
If you'd prefer to frame the version in terms of progress toward the next
release rather than distance from the latest one, you can bump it:
$ dunamai from any --bump
0.2.1.dev7+g29045e8
Validation of custom formats:
$ dunamai from any --format "v{base}" --style pep440
Version 'v0.2.0' does not conform to the PEP 440 style
Validate your own freeform versions:
$ dunamai check 0.01.0 --style semver
Version '0.01.0' does not conform to the Semantic Versioning style
More info:
$ dunamai --help
$ dunamai from --help
$ dunamai from git --help
Library
from dunamai import Version, Style
Let's say you're on commit g644252b, which is tagged as v0.1.0.
version = Version.from_git()
assert version.serialize() == "0.1.0"
Let's say there was a v0.1.0rc5 tag 44 commits ago
and you have some uncommitted changes.
version = Version.fromanyvcs()
assert version.serialize() == "0.1.0rc5.post44.dev0+g644252b"
assert version.serialize(metadata=False) == "0.1.0rc5.post44.dev0"
assert version.serialize(dirty=True) == "0.1.0rc5.post44.dev0+g644252b.dirty"
assert version.serialize(style=Style.SemVer) == "0.1.0-rc.5.post.44+g644252b"
The serialize() method gives you an opinionated, PEP 440-compliant default that ensures that versions for untagged commits are compatible with Pip's --pre flag. The individual parts of the version are also available for you to use and inspect as you please:
assert version.base == "0.1.0"
assert version.stage == "rc"
assert version.revision == 5
assert version.distance == 44
assert version.commit == "g644252b"
assert version.dirty is True
Available if the latest tag includes metadata, like v0.1.0+linux:
assert version.tagged_metadata == "linux"
Tips
By default, the "v" prefix on the tag is required, unless you specify a custom tag pattern. You can either write a regular expression:- Console:
$ dunamai from any --pattern "(?P<base>\d+\.\d+\.\d+)"
- Python:
from dunamai import Version
version = Version.fromanyvcs(pattern=r"(?P<base>\d+\.\d+\.\d+)")
...or use a named preset:
- Console:
$ dunamai from any --pattern default-unprefixed
- Python:
from dunamai import Version, Pattern
version = Version.fromanyvcs(pattern=Pattern.DefaultUnprefixed)
You can also keep the default pattern and just specify a prefix. For example, this would match tags like some-package-v1.2.3:
- Console:
$ dunamai from any --pattern-prefix some-package-
- Python:
from dunamai import Version
version = Version.fromanyvcs(pattern_prefix="some-package-")
VCS archives
Sometimes, you may only have access to an archive of a repository (e.g., a zip file) without the full history. Dunamai can still detect a version in some of these cases:- For Git, you can configure
git archiveto produce a file with some metadata for Dunamai.
.git_archival.json file to the root of your repository with this content:
{
"hash-full": "$Format:%H$",
"hash-short": "$Format:%h$",
"timestamp": "$Format:%cI$",
"refs": "$Format:%D$",
"describe": "$Format:%(describe:tags=true,match=v[0-9]*)$"
}
Add this line to your .gitattributes file. If you don't already have this file, add it to the root of your repository:
.git_archival.json export-subst
- For Mercurial, Dunamai will detect and use an
.hg_archival.txtfile created byhg archive.
.hgtags if present.
Custom formats
Here are the available substitutions for custom formats. If you have a tag likev9!0.1.2-beta.3+other, then:
{base}=0.1.2{stage}=beta{revision}=3{distance}is the number of commits since the last{commit}is the commit hash (defaults to short form, unless you use--full-commit){dirty}expands to either "dirty" or "clean" if you have uncommitted modified files{tagged_metadata}=other{epoch}=9{branch}=feature/foo{branch_escaped}=featurefoo(can be customized using--escape-with){timestamp}is in the formatYYYYmmddHHMMSSas UTC{major}=0{minor}=1{patch}=2
distance=$(dunamai from any --format "{distance}")
if [ "$distance" = "0" ]; then
dunamai from any --format "v{base}"
else
dunamai from any --format "v{base}+{distance}.{dirty}"
fi
Comparison to Versioneer
Versioneer is another great library for dynamic versions, but there are some design decisions that prompted the creation of Dunamai as an alternative:- Versioneer requires a setup.py file to exist, or else
versioneer installwill fail,
- Versioneer has a CLI that generates Python code which needs to be committed into your repository,
- Versioneer produces the version as an opaque string,
- Versioneer provides customizability through a config file,
Integration
- Setting a
versionstatically:
$ echo "version = '$(dunamai from any)'" > yourlibrary/version.py
# your_library/init.py
from yourlibrary.version import version
Or dynamically (but Dunamai becomes a runtime dependency):
# your_library/init.py
import dunamai as _dunamai
version = dunamai.getversion("your-library", thirdchoice=dunamai.Version.fromanyvcs).serialize()
- setup.py (no install-time dependency on Dunamai as long as you use wheels):
from setuptools import setup
from dunamai import Version
setup( name="your-library", version=Version.fromanyvcs().serialize(), )
Or you could use a static inclusion approach as in the prior example.
$ poetry version $(dunamai from any)
Or you can use the poetry-dynamic-versioning plugin.
Other notes
- Dunamai needs access to the full version history to find tags and compute distance.
* For GitHub workflows, invoke actions/checkout@v3 with fetch-depth: 0. * For GitLab pipelines, set the GIT_DEPTH variable to 0. * For Docker builds, copy the VCS history (e.g., .git folder) into the container.
For Git, you can also avoid doing a full clone by specifying a remote branch for tags (e.g., --tag-branch remotes/origin/master).
- When using Git, remember that lightweight tags do not store their creation time.
- When using Git, the initial commit should not be both tagged and empty
--allow-empty). This is related to a reporting issue in Git. For more info, click here. Dunamai tries to work around this, but multiple tags on an empty initial commit may not be sorted correctly.