GitHub | PyPI | Issues | Changelog
versioningit — Versioning It with your Version In Git
versioningit is yet another setuptools plugin for automatically determining
your package's version based on your version control repository's tags. Unlike
others, it allows easy customization of the version format and even lets you
easily override the separate functions used for version extraction &
calculation.
Features:
- Installed & configured through PEP 518's
pyproject.toml - Supports Git, modern Git archives, and Mercurial
- Formatting of the final version uses format template strings, with fields for basic VCS information and separate template strings for distanced vs. dirty vs. distanced-and-dirty repository states
- Can optionally write the final version to a file for loading at runtime
- The individual methods for VCS querying, tag-to-version calculation, version bumping, version formatting, and writing the version to a file can all be customized using either functions defined alongside one's project code or via publicly-distributed entry points
- Can alternatively be used as a library for use in
setup.pyor the like, in case you don't want to or can't configure it viapyproject.toml - The only thing it does is calculate your version and optionally write it to a file; there's no overriding of your sdist contents based on what's in your Git repository, especially not without a way to turn it off, because that would just be rude.
Contents
- Installation & Setup
- Configuration
- Getting Package Version at Runtime
- Command
- Library API
- Writing Your Own Methods
- Restrictions & Caveats
versioningit requires Python 3.6 or higher. Just use pip for Python 3 (You have pip, right?) to install
versioningit and its dependencies:
python3 -m pip install versioningit
However, usually you won't need to install versioningit in your environment
directly. Instead, you specify it in your project's pyproject.toml file in
the build-system.requires key, like so:
[build-system]
requires = [
"setuptools >= 42", # At least v42 of setuptools required!
"versioningit ~= 0.3.0",
"wheel"
]
build-backend = "setuptools.build_meta"Then, you configure versioningit by adding a [tool.versioningit] table
to your pyproject.toml. See "Configuration" below for details, but you
can get up & running with just the minimal configuration, an empty table:
[tool.versioningit]versioningit replaces the need for (and will overwrite) the version
keyword to the setup() function, so you should remove any such keyword from
your setup.py/setup.cfg to reduce confusion.
Once you have a [tool.versioningit] table in your pyproject.toml — and
once your repository has at least one tag — building your project with
setuptools while versioningit is installed (which happens automatically
if you set up your build-system.requires as above and you're using a
PEP 517 frontend like build) will result in your project's version
automatically being set based on the latest tag in your Git repository. You
can test your configuration and see what the resulting version will be using
the versioningit command (see below).
The [tool.versioningit] table in pyproject.toml is divided into five
subtables, each describing how one of the five steps of the version extraction
& calculation should be carried out. Each subtable consists of an optional
method key specifying the method (entry point or function) that should be
used to carry out that step, plus zero or more extra keys that will be passed
as parameters to the method when it's called. If the method key is
omitted, the default method for the step is used.
A method can be specified in two different ways, depending on where it's
implemented. A method that is built in to versioningit or provided by an
installed third-party extension is specified by giving its name as a string,
e.g.:
[tool.versioningit.vcs]
# The method key:
method = "git" # <- The method name
# Parameters to pass to the method:
match = ["v*"]
default-tag = "1.0.0"Alternatively, a method can be implemented as a function in a Python source
file in your project directory (either part of the main Python package or in an
auxiliary file); see "Writing Your Own Methods" below for more information.
To tell versioningit to use such a method, set the method key to a
table with a module key giving the dotted name of the module in which the
method is defined and a value key giving the name of the callable object in
the module that implements the method. For example, if you created a custom
next-version method that's named my_next_version() and is located in
mypackage/mymodule.py, you would write:
[tool.versioningit.next-version]
method = { module = "mypackage.module", value = "my_next_version" }
# Put any parameters hereNote that this assumes that mypackage/ is located at the root of the
project directory (i.e., the directory containing the pyproject.toml file);
if is located inside another directory, like src/, you will need to add a
module-dir key to the method table giving the path to that directory
relative to the project root, like so:
[tool.versioningit.next-version]
method = { module = "mypackage.module", value = "my_next_version", module-dir = "src" }
# Put any parameters hereAs a special case, if there are no parameters for a given step, the respective subtable can be replaced by the method specification, e.g.:
[tool.versioningit]
# Use the "git" method for the vcs step with no parameters:
vcs = "git"
# Use a custom function for the next-version step with no parameters:
next-version = { module = "mypackage.module", value = "my_next_version" }The vcs subtable specifies the version control system used by the project
and how to extract the tag and related information from it. versioningit
provides three vcs methods: "git" (the default), "git-archive", and
"hg".
The "git" method relies on the project directory being located inside a Git
repository with one or more commits.
The "git" method takes the following parameters, all optional:
match: list of strings- A set of fileglob patterns to pass to the
--matchoption ofgit describeto make Git only consider tags matching the given pattern(s). Defaults to an empty list. exclude: list of strings- A set of fileglob patterns to pass to the
--excludeoption ofgit describeto make Git not consider tags matching the given pattern(s). Defaults to an empty list. default-tag: string- If
git describecannot find a tag,versioningitwill raise aversioningit.errors.NoTagErrorunlessdefault-tagis set, in which case it will act as though the initial commit is tagged with the value ofdefault-tag
New in version 0.2.0
This method is experimental and may change in future releases.
The "git-archive" method is an extension of the "git" method that also
supports determining the version when installing from a properly-prepared Git
archive. The method takes the same parameters as "git" plus the following:
describe-subst: stringSet this to
"$Format:%(describe)$"(You will get a warning if you don't) and add the linepyproject.toml export-substto your repository's.gitattributesfile. This will cause any Git archive made from your repository from this point forward to contain the minimum necessary information to determine a version.If you also set the
matchorexcludeparameter, you will need to include those values in this parameter; examples:# Match one pattern: match = ["v*"] describe-subst = "$Format:%(describe:match=v*)$" # Match multiple patterns: match = ["v*", "r*"] describe-subst = "$Format:%(describe:match=v*,match=r*)$" # Match and exclude: match = ["v*"] exclude = ["*-final"] describe-subst = "$Format:%(describe:match=v*,exclude=*-final)$"
Note that, in order to provide a consistent set of information regardless of
whether installing from a repository or an archive, the "git-archive"
method provides the format step with only a subset of the fields that the
"git" method does; see below for more information.
Important: The %(describe)s placeholder was only added to Git in
version 2.32.0, and so only archives made with at least that version can be
installed with this method. More importantly, as of 2021-07-09, GitHub does
not yet support the placeholder in its archives (which include repository ZIP
downloads), and so installing from a URL of the form
<https://github.com/$OWNER/$REPO/archive/$BRANCH.zip> will not work — but it
presumably will work at some unspecified point in the future.
Important: As of Git 2.32.0, the %(describe) placeholder only
recognizes annotated tags; lightweight tags are ignored.
Note: In order to avoid DOS attacks, Git will not expand more than one
%(describe)s placeholder per archive, and so you should not have any other
$Format:%(describe)$ placeholders in your repository.
Note: This method will not work correctly if you have a tag that resembles
git describe output, i.e., that is of the form
<anything>-<number>-g<hex-chars>. So don't do that.
New in version 0.2.0
The "hg" method supports installing from a Mercurial repository or archive.
The "hg" method takes the following parameters, all optional:
pattern: string- A revision pattern (See
hg help revisions.patterns) to pass to thelatesttag()template function. Note that this parameter has no effect when installing from a Mercurial archive. default-tag: string- If there is no latest tag,
versioningitwill raise aversioningit.errors.NoTagErrorunlessdefault-tagis set, in which case it will act as though the initial commit is tagged with the value ofdefault-tag
The tag2version subtable specifies how to extract the version from the tag
found by the vcs step. versioningit provides one tag2version
method, "basic" (the default), which proceeds as follows:
- If the
rmprefixparameter is set to a string and the tag begins with that string, the given string is removed from the tag. - If the
rmsuffixparameter is set to a string and the tag ends with that string, the given string is removed from the tag. - If the
regexparameter is set to a string (a Python regex) and the regex matches the tag (usingre.search()), the tag is replaced with the contents of the capturing group named "version", or the entire matched text if there is no group by that name. If the regex does not match the tag, the behavior depends on therequire-matchparameter: if true, an error is raised; if false or unset, the tag is left as-is. - Finally, any remaining leading
v's are removed from the tag.
A warning is emitted if the resulting version is not PEP 440-compliant.
The next-version subtable specifies how to calculate the next release
version from the version extracted from the VCS tag. versioningit provides
the following next-version methods; none of them take any parameters.
minor- (default) Strips the input version down to just the epoch segment (if
any) and release segment (i.e., the
N(.N)*bit), increments the second component of the release segment, and replaces the following components with a single zero. For example, if the version extracted from the VCS tag is1.2.3.4, the"minor"method will calculate a new version of1.3.0. minor-release- Like
minor, except that if the input version is a prerelease or development release, the base version is returned; e.g.,1.2.3a0becomes1.2.3. This method requires the input version to be PEP 440-compliant. smallest- Like
minor, except that it increments the last component of the release segment. For example, if the version extracted from the VCS tag is1.2.3.4, the"smallest"method will calculate a new version of1.2.3.5. smallest-release- Like
smallest, except that if the input version is a prerelease or development release, the base version is returned; e.g.,1.2.3a0becomes1.2.3. This method requires the input version to be PEP 440-compliant. null- Returns the input version unchanged. Useful if your repo version is something horrible and unparseable.
A warning is emitted if the resulting version is not PEP 440-compliant.
The format subtable specifies how to format the project's final version
based on the information calculated in previous steps. (Note that, if the
repository's current state is an exact tag match, this step will be skipped and
the version returned by the tag2version step will be used as the final
version.) versioningit provides one format method, "basic" (the
default).
The data returned by the vcs step includes a repository state (describing
the relationship of the repository's current contents to the most recent tag)
and a collection of format fields. The "basic" format method takes
the name of that state, looks up the format parameter with the same name
(falling back to a default, given below) to get a format template string,
and formats the template using the given format fields plus {version},
{next_version}, and {branch} fields. A warning is emitted if the
resulting version is not PEP 440-compliant.
For the built-in vcs methods, the repository states are:
distance |
One or more commits have been made on the current branch since the latest tag |
dirty |
No commits have been made on the branch since the latest tag, but the repository has uncommitted changes |
distance-dirty |
One or more commits have been made on the branch since the latest tag, and the repository has uncommitted changes |
For the built-in vcs methods, the available format fields are:
{author_date} |
The author date of the HEAD commit [1] ("git"
only) |
{branch} |
The name of the current branch (with non-alphanumeric
characters converted to periods), or None if the
branch cannot be determined |
{build_date} |
The current date & time, or the date & time specified by
the environment variable SOURCE_DATE_EPOCH if it is
set [1] |
{committer_date} |
The committer date of the HEAD commit [1] ("git"
only) |
{distance} |
The number of commits since the most recent tag |
{next_version} |
The next release version, calculated by the
next-version step |
{rev} |
The abbreviated hash of the HEAD commit |
{revision} |
The full hash of the HEAD commit ("git" and "hg"
only) |
{vcs} |
The first letter of the name of the VCS (i.e., "g" or
"h") |
{vcs_name} |
The name of the VCS (i.e., "git" or "hg") |
{version} |
The version extracted from the most recent tag |
| [1] | (1, 2, 3) These fields are UTC datetime.datetime objects. They are
formatted with strftime() formats by writing {fieldname:format},
e.g., {build_date:%Y%m%d}. |
The default parameters for the format step are:
[tool.versioningit.format]
distance = "{version}.post{distance}+{vcs}{rev}"
dirty = "{version}+d{build_date:%Y%m%d}"
distance-dirty = "{version}.post{distance}+{vcs}{rev}.d{build_date:%Y%m%d}"Other sets of format parameters of interest include:
The default format used by setuptools_scm:
[tool.versioningit.next-version] method = "smallest" [tool.versioningit.format] distance = "{next_version}.dev{distance}+{vcs}{rev}" dirty = "{version}+d{build_date:%Y%m%d}" distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.d{build_date:%Y%m%d}"
The format used by versioneer:
[tool.versioningit.format] distance = "{version}+{distance}.{vcs}{rev}" dirty = "{version}+{distance}.{vcs}{rev}.dirty" distance-dirty = "{version}+{distance}.{vcs}{rev}.dirty"
The format used by vcversioner:
[tool.versioningit.format] distance = "{version}.post{distance}" dirty = "{version}" distance-dirty = "{version}.post{distance}"
The write subtable enables an optional feature, writing the final version
to a file. versioningit provides one write method, "basic" (the
default), which takes the following parameters (all optional):
file: stringThe path to the file to which to write the version. This path should use forward slashes (
/) as the path separator, even on Windows. If this parameter is omitted, nothing is written anywhere.Note: This file should not be committed to version control, but it should be included in your project's built sdists and wheels.
encoding: string- The encoding with which to write the file. Defaults to UTF-8.
template: stringThe content to write to the file (minus the final newline, which
versioningitadds automatically), as a string containing a{version}placeholder. If this parameter is omitted, the default is determined based on thefileparameter's file extension. For.txtfiles and files without an extension, the default is:{version}while for
.pyfiles, the default is:__version__ = "{version}"If
templateis omitted andfilehas any other extension, an error is raised.
The final key in the [tool.versioningit] table is default-version,
which is a string rather than a subtable. When this key is set and an error
occurs during version calculation, versioningit will set your package's
version to the given default version. When this key is not set, any errors
that occur inside versioningit will cause the build/install process to
fail.
Note that default-version is not applied if an error occurs while parsing
the [tool.versioningit] table; however, such errors can be caught ahead of
time by running the versioningit command.
When versioningit is invoked via the setuptools plugin interface, it logs
various information to stderr. By default, only messages at WARNING level
or higher are displayed, but this can be changed by setting the
VERSIONINGIT_LOG_LEVEL environment variable to the name of a Python
logging level (case insensitive) or the equivalent integer value.
Automatically setting your project's version is all well and good, but you
usually also want to expose that version at runtime, usually via a
__version__ variable. There are two options for doing this:
Use the
version()function in importlib.metadata to get your package's version, like so:from importlib.metadata import version __version__ = version("mypackage")
Note that
importlib.metadatawas only added to Python in version 3.8. If you wish to support older Python versions, use the importlib-metadata backport available on PyPI for those versions, e.g.:try: from importlib.metadata import version except ImportError: from importlib_metadata import version __version__ = version("mypackage")
If relying on the backport, don't forget to include
importlib-metadata; python_version < "3.8"in your project'sinstall_requires!Fill out the
[tool.versioningit.write]subtable inpyproject.tomlso that the project version will be written to a file in your Python package which you can then import or read. For example, if your package is namedmypackageand is stored in asrc/directory, you can write the version to a Python filesrc/mypackage/_version.pylike so:[tool.versioningit.write] file = "src/mypackage/_version.py"
Then, within
mypackage/__init__.py, you can import the version like so:from ._version import __version__
Alternatively, you can write the version to a text file, say,
src/mypackage/VERSION:[tool.versioningit.write] file = "src/mypackage/VERSION"
and then read the version in at runtime with:
from pathlib import Path __version__ = Path(__file__).with_name("VERSION").read_text().strip()
versioningit [<options>] [<project-dir>]
When versioningit is installed in the current Python environment, a command
of the same name will be available that prints out the version for a given
versioningit-enabled project (by default, the project rooted in the current
directory). This can be used to test out your versioningit setup before
publishing.
| -n, --next-version | |
(New in version 0.3.0) Instead of printing the
current version of the project, print the value of the
next release version as computed by the
next-version step | |
| --traceback | Normally, any library errors are shown as just the error message. Specify this option to show the complete error traceback. |
| -v, --verbose | Increase the amount of log messages displayed. Specify twice for maximum information. |
| -w, --write | Write the version to the file specified in the
[tool.versioningit.write] subtable, if so
configured |
The versioningit package exports a number of classes & functions for
programmatically determining a VCS-managed project's version and the values at
each step that go into calculating it. For brevity, only the "topmost"
function is documented here; consult the source (primarily
src/versioningit/core.py) for documentation on the other features.
versioningit.get_version(
project_dir: Union[str, pathlib.Path] = os.curdir,
config: Optional[dict] = None,
write: bool = False,
fallback: bool = True,
) -> strReturns the version of the project in project_dir. If config is
None, then project_dir must contain a pyproject.toml file
containing a [tool.versioningit] table; if it does not, a
versioningit.errors.NotVersioningitError is raised.
If config is not None, then any pyproject.toml file in
project_dir will be ignored, and the configuration will be taken from
config instead. config must be a dict whose structure mirrors the
structure of the [tool.versioningit] table in pyproject.toml. For
example, the following TOML configuration:
[tool.versioningit.vcs]
method = "git"
match = ["v*"]
[tool.versioningit.next-version]
method = { module = "setup", value = "my_next_version" }
[tool.versioningit.format]
distance = "{next_version}.dev{distance}+{vcs}{rev}"
dirty = "{version}+dirty"
distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.dirty"corresponds to the following Python config value:
{
"vcs": {
"method": "git",
"match": ["v*"],
},
"next-version": {
"method": {
"module": "setup",
"value": "my_next_version",
},
},
"format": {
"distance": "{next_version}.dev{distance}+{vcs}{rev}",
"dirty": "{version}+dirty",
"distance-dirty": "{next_version}.dev{distance}+{vcs}{rev}.dirty",
},
}When passing versioningit configuration as the config argument, an
alternative way to specify methods becomes available: in place of a method
specification, one can pass a callable object directly.
If write is true, then the file specified in the
[tool.versioningit.write] subtable, if any, will be updated.
If fallback is true, then if project_dir is not under version control
(or if the VCS executable is not installed), versioningit will assume that
the directory is an unpacked sdist and will read the version from the
PKG-INFO file; if there is no PKG-INFO file, a
versioningit.errors.NotSdistError is raised. If fallback is false and
project_dir is not under version control, a
versioningit.errors.NotVCSError is raised.
If you need to customize how a versioningit step is carried out, you can
write a custom function in a Python module in your project directory and point
versioningit to that function as described under "Specifying the
Method".
When a custom function is called, it will be passed a step-specific set of
arguments, as documented below, plus all of the parameters specified in the
step's subtable in pyproject.toml. (The step-specific arguments are passed
as keyword arguments, so custom methods need to give them the same names as
documented here.) For example, given the below configuration:
[tool.versioningit.vcs]
method = { module = "ving_methods", value = "my_vcs", module-dir = "tools" }
tag_dir = "tags"
annotated_only = trueversioningit will carry out the vcs step by calling my_vcs() in
ving_methods.py in the tools/ directory with the arguments
project_dir (set to the directory in which the pyproject.toml file is
located), tag_dir="tags", and annotated_only=True. If a subtable
happens to contain any keys that conflict with the step-specific arguments
(e.g., if a [tool.versioningit.vcs] table contains a project_dir key),
such keys will be discarded when the subtable is parsed.
If a user-supplied parameter to a method is invalid, the method should raise a
versioningit.errors.ConfigError. If a method is passed a parameter that it
does not recognize, it should ignore it.
If you choose to store your custom methods in your setup.py, be sure to
place the call to setup() behind an if __name__ == "__main__": guard so
that the module can be imported without executing setup().
If you store your custom methods in a module other than setup.py that is
not part of the project's Python package (e.g., if the module is stored in a
tools/ directory), you need to ensure that the module is included in your
project's sdists but not in wheels.
If your custom method depends on any third-party libraries, they must be listed
in your project's build-system.requires.
A custom vcs method is a callable with the following signature:
(*, project_dir: Union[str, pathlib.Path], **params: Any) -> versioningit.VCSDescriptionThe callable must take a path to a directory and some number of user-supplied
parameters and return a versioningit.VCSDescription describing the state of
the version control repository at the directory, where VCSDescription is a
dataclass with the following fields:
tag:str- The name of the most recent tag in the repository (possibly after applying
any match or exclusion rules based on the parameters) from which the
current repository state is descended. If a tag cannot be determined, a
versioningit.errors.NoTagErrorshould be raised. state:str- A string describing the relationship of the current repository state to the
tag. If the repository state is exactly the tagged state, this field
should equal
"exact"; otherwise, it should be a custom string that will be used as a key in the[tool.versioningit.format]subtable. Customvcsmethods are advised to adhere closely to the"distance"/"dirty"/"distance-dirty"set of states used by built-in methods. branch:Optional[str]- The name of the repository's current branch, or
Noneif it cannot be determined or does not apply fields:Dict[str, Any]- An arbitrary
dictof fields for use in[tool.versioningit.format]format templates. Customvcsmethods are advised to adhere closely to the set of fields used by the built-in methods.
If project_dir is not under the expected type of version control, a
versioningit.errors.NotVCSError should be raised.
A custom tag2version method is a callable with the following signature:
(*, tag: str, **params: Any) -> strThe callable must take a tag retrieved from version control and some number of
user-supplied parameters and return a version string. If the tag cannot be
parsed, a versioningit.errors.InvalidTagError should be raised.
A custom next-version method is a callable with the following signature:
(*, version: str, branch: Optional[str], **params: Any) -> strThe callable must take a project version (as extracted from a VCS tag), the
name of the VCS repository's current branch (if any), and some number of
user-supplied parameters and return a version string for use as the
{next_version} field in [tool.versioningit.format] format templates.
If version cannot be parsed, a versioningit.errors.InvalidVersionError
should be raised.
A custom format method is a callable with the following signature:
(*, description: versioningit.VCSDescription, version: str, next_version: str, **params: Any) -> strThe callable must take a versioningit.VCSDescription as returned by the
vcs method (see above), a version string extracted from the VCS tag, a
"next version" calculated by the next-version step, and some number of
user-supplied parameters and return the project's final version string.
Note that the format method is not called if description.state is
"exact", in which case the version returned by the tag2version step is
used as the final version.
A custom write method is a callable with the following signature:
(*, project_dir: Union[str, pathlib.Path], version: str, **params: Any) -> NoneThe callable must take the path to a project directory, the project's final
version, and some number of user-supplied parameters and write the version to a
file in project_dir.
If you want to make your custom versioningit methods available for others
to use, you can package them in a Python package and distribute it on PyPI.
Simply create a Python package as normal that contains the method function, and
specify the method function as an entry point of the project. The name of the
entry point group is versioningit.STEP (though, for next-version, the
group is spelled with an underscore instead of a hyphen:
versioningit.next_version). For example, if you have a custom vcs
method implemented as a foobar_vcs() function in mypackage/vcs.py, you
would declare it in setup.cfg as follows:
[options.entry_points]
versioningit.vcs =
foobar = mypackage.vcs:foobar_vcsOnce your package is on PyPI, package developers can use it by including it in
their build-system.requires and specifying the name of the entry point (For
the entry point above, this would be foobar) as the method name in the
appropriate subtable. For example, a user of the foobar method for the
vcs step would specify it as:
[tool.versioningit.vcs]
method = "foobar"
# Parameters go here- When building or installing a project that uses
versioningit, the entire repository history (or at least everything back through the most recent tag) must be available. This means that installing from a shallow clone (the default on most CI systems) will not work. If you are using the"git"or"git-archive"vcsmethod and havedefault-tagset in[tool.versioningit.vcs], then shallow clones will end up assigned the default tag, which may or may not be what you want. - If using the
[tool.versioningit.write]subtable to write the version to a file, this file will only be updated whenever the project is built or installed. If using editable installs, this means that you must re-runpython setup.py developorpip install -e .after each commit if you want the version to be up-to-date. - If you define & use a custom method inside your Python project's package, you
will not be able to retrieve your project version by calling
importlib.metadata.version()inside__init__.py— at least, not without atry: ... except ...wrapper. This is becauseversioningitloads the package containing the custom method before the package is installed, butimportlib.metadata.version()only works after the package is installed.