Giter Club home page Giter Club logo

version-query's Introduction

version-query

Zero-overhead package versioning for Python.

package version from PyPI

build status from GitHub

test coverage from Codecov

grade from Codacy

license

Package versioning toolkit for Python which relies on git tags, git history traversal and status of git repository to generate a very fine-grained version number.

Aren't you tired hardcoding the version numbers when maintaining a Python package?!

And wouldn't you like have fine-grained versioning scheme with version changing with each commit, for easier development, testing and pre-release deployment?!

Search no more!

As long as you mark your releases using git tags, instead of hardcoding:

__version__ = '1.5.0.dev2'

You can do:

from version_query import predict_version_str

__version__ = predict_version_str()

It's 21st century, stop hardcoding version numbers!

This will set the version to release version when you really release a new version, and it will automatically generate a suitable development version at development/pre-release phase.

Overview

At development time, the current version number is automatically generated based on:

  • tags
  • current commit SHA
  • index status

in your git repository. Therefore the package can be built and shipped to PyPI based only on status of the git repository. When there is no git repository (this might be the case at installation time or at runtime) then the script relies on metadata generated at packaging time.

That's why, regardless if package is installed from PyPI (from source or wheel distribution) or cloned from GitHub, the version query will work.

Additionally, version numbers in version-query are mutable objects and they can be conveniently incremented, compared with each other, as well as converted to/from other popular versioning formats.

Versioning scheme

Version scheme used by version-query is a relaxed mixture of:

These two rulesets are mostly compatible. When they are not, a more relaxed approach of the two is used. Details follow.

Version has one of the following forms:

  • <release>
  • <release><pre-release>
  • <release>+<local>
  • <release><pre-release>+<local>

A release version identifier <release> has one of the following forms:

  • <major>
  • <major>.<minor>
  • <major>.<minor>.<patch>

And the pre-release version identifier <pre-release> has one of the following forms:

  • <pre-type>
  • <pre-type><pre-patch>
  • <pre-separator><pre-type>
  • <pre-separator><pre-patch>
  • <pre-separator><pre-type><pre-patch>
  • ... and any of these forms can be repeated arbitrary number of times.

And finally the local version identifier <local> has one of the forms:

  • <local-part>
  • <local-part><local-separator><local-part>
  • <local-part><local-separator><local-part><local-separator><local-part>
  • ... and so on.

Each version component has a meaning and constraints on its contents:

  • <major> - a non-negative integer, increments when backwards-incompatible changes are made
  • <minor> - a non-negative integer, increments when backwards-compatible features are added
  • <patch> - a non-negative integer, increments when backwards-compatible bugfixes are made
  • <pre-separator> - dot or dash, separates release version information from pre-release
  • <pre-type> - a string of lower-case alphabetic characters, type of the pre-release
  • <pre-patch> - a non-negative integer, revision of the pre-release
  • <local-part> - a sequence of alphanumeric characters, stores arbitrary information
  • <local-separator> - a dot or dash, separates parts of local version identifier

How exactly the version number is determined

The version-query package has two modes of operation:

  • query - only currently available explicit information is used to determine the version number
  • prediction - this applies only to determining version number from git repository, and means that in addition to explicit version information, git repository status can be used to get very fine-grained version number which will be unique for every repository snapshot

Version query from package metadata file

The metadata file (PKG-INFO or metadata.json or METADATA) is automatically generated whenever a Python distribution file is built. Which one, depends on your method of building, but in any case, the file is then packaged into distributions, and when uploaded to PyPI that metadata file is used to populate the package page - therefore all Python packages on PyPI should have it.

Additionally, source code folder of any package using setuptools, in which setup.py build was executed, contains metadata file -- even if distribution file was not built.

The version identifier is contained verbatim in the metadata file, therefore version query in this case boils down to simply reading the metadata file.

Information about Python metadata files:

Version query from git repository

The version number is equal to the version contained in the most recent version tag.

Version tags

Any git tag that is a valid version (matching the rules above) is considered a version tag. Version number can be prefixed with v or ver. Other tags are ignored.

Examples of valid version tags:

  • v1.0
  • v0.16.0
  • v1.0.dev3
  • ver0.5.1-4.0.0+gita1de3012
  • 42.0
  • 3.14-15
Most recent version tag

The most recent tag is found based on repository history and version precedence.

Search for version tags starts from current commit, and goes backwards in history (towards initial commit). Therefore, commits after current one as well as not-merged branches are ignored in the version tag search.

If there are several version tags on one commit, then highest version number is used.

If there are version tags on several merged branches, then the highest version number is used.

If there are no version tags in the repository, you'll get an error - so version cannot be queried from git repository without any version tags.

But in such case, version can still be predicted, as described below.

Version prediction from git repository

In version prediction mode, first of all, a most recent version tag is found, as above. If there are no version tags in the repo, then the initial commit is assumed to have tag v0.1.0.dev0.

Then, the new commits since the most recent version tag are counted. Then, the repository index status is queried. All the results are combined to form the predicted version number. Procedure is described below in detail.

Counting new commits

If after the commit with the most recent tag there are any new commits, a suffix .dev# is appended to the version identifier, where # is the number of commits between the current commit and the most recent version tag.

Additionally, the <patch> version component is incremented by 1.

Additionally, a plus (+) character, word git and the first 8 characters of SHA of the latest commit are appended to version identifier, e.g. +gita3014fe0.

Repository index status

Additionally, if there are any uncommitted changes in the repository (i.e. the repo is dirty), the suffix .dirty followed by current date and time in format YYYYMMDDhhmmss are appended to the identifier.

Example of how the final version identifier looks like, depending on various conditions of the repository:

  • Most recent version tag is v0.4.5, there were 2 commits since, latest having SHA starting with 812f12ea. Version identifier will be 0.4.6.dev2+git812f12ea.
  • Most recent version tag is ver6.0, and there was 1 commit since having SHA starting with e10ac365. Version identifier will be 6.0.1.dev1+gite10ac365.
  • Most recent version tag is v9, there were 40 commit since, latest having SHA starting with 1ad22355, the repository has uncommitted changes and version was queried at 19:52.20, 8th June 2017. the result is 9.0.1.dev40+git1ad22355.dirty20170608195220.

How exactly version numbers are compared

The base specification of the comparison scheme is:

With the notable difference to both that all version components are taken into account when establishing version precedence.

When being compared, <major>, <minor> and <patch> are assumed equal to 0 if they are not present. In <pre-release>, the <pre-patch> is assumed to be 0 if not present.

Examples of comparison results:

  • 0.3-4.4-2.9 < 0.3-4.4-2.10
  • 0.3dev < 0.3dev1
  • 0.3rc2 < 0.3
  • 0.3 < 0.3-2
  • 1.0.0 < 1.0.0+blahblah
  • 1.0.0+aa < 1.0.0+aaa
  • 1.0.0 = 1.0.0
  • 1 = 1.0.0
  • 1.0 = 1.0.0.0
  • 1.0.0-0.0.DEV42 = 1.0.0.0.0.dev42

How exactly version number is incremented

Some version components have assumed value 0 if they are not present, please see section above for details.

Incrementing any version component clears all existing following components.

Examples of how version is incremented:

  • for 1.5, incrementing <major> results in 2.0;
  • for 1.5.1-2.4, <minor>++ results in 1.6;
  • 1.5.1-2.4, <patch>++, 1.5.2;
  • 1.5.1, <major>+=3, 4.0.0.

API details

All functionality mentioned below is considered as the public API. Other functionality may change without notice.

Main API

import version_query

version_str = version_query.query_version_str()

The version-query package will query the version string while operating in query mode.

version_str = version_query.predict_version_str()

The version-query package will infer the version string while operating in prediction mode.

version = version_query.Version(1, 0, 4)
version = version_query.Version(major=1, patch=4)
version = version_query.Version.from_str('1.0.4')

The Version class is used internally by version-query, but it can be also used explicitly.

import packaging.version
version = version_query.Version.from_py_version(packaging.version.Version())
version.to_py_version()

import semver
version = version_query.Version.from_sem_version(semver.VersionInfo())
version.to_sem_version()

Also, Version class interoperates with packaging and semver packages as well as selected built-in types.

assert version_query.Version(1, 0, 4).increment(version_query.VersionComponent.Patch, 2) \
    == version_query.Version.from_str('1.0.6')
assert version_query.Version.from_str('1.0.4') < version_query.Version.from_str('2.0.0')

The Version objects are mutable, hashable and comparable.

version = version_query.query_folder(pathlib.Path('/my/project'), search_parent_directories=False)
version = version_query.predict_git_repo(pathlib.Path('/my/git/versioned/project/subdir'), True)
version = version_query.query_caller(stack_level=1)
version = version_query.predict_caller(2)

Version object can be obtained for any supported path, as well as for any python code currently being executed -- as long as it is located in a supported location.

Command-line interface

python3 -m version_query --help
python3 -m version_query /my/project -p
version_query.__main__.main(args=['--help'])
version_query.__main__.main(args=['/my/project', '-p'])

Version query can be also used as a command-line script, with the entry point also accessible as version_query.__main__.main from within Python.

Utility functions

assert version_query.git_query.preprocess_git_version_tag('v1.0.4') == '1.0.4'
assert version_query.git_query.preprocess_git_version_tag('ver1.0.4') == '1.0.4'
assert version_query.git_query.preprocess_git_version_tag('1.0.4') == '1.0.4'

Remove v and ver prefix from a given string, and preform very crude checking whether the tag is probably a version tag.

Limitations

Either git repository or metadata file must be present for the script to work. When, for example, zipped version of repository is downloaded from GitHub, the resulting archive contains neither metadata files nor repository data.

It is unclear what happens if the queried repository is bare.

The implementation is not fully compatible with Python versioning. Especially, in current implementation at most one of: alpha a / beta b / release candidate rc / development dev suffixes can be used in a version identifier.

And the format in which alpha a, beta b and release candidate rc suffixes are to be used does not match exactly the conditions defined in PEP 440.

Script might feel a bit slow when attempting to find a version tag in a git repository with a very large history and no version tags. It is designed towards packages with short release cycles -- in long release cycles the overhead of manual versioning is small anyway.

Despite above limitations, version-query itself (as well as growing number of other packages) are using version-query without any issues.

Requirements

Python version 3.8 or later.

Python libraries as specified in requirements.txt.

Building and running tests additionally requires packages listed in requirements_test.txt.

Tested on Linux, macOS and Windows.

version-query's People

Contributors

dependabot[bot] avatar jayvdb avatar mbdevpl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

version-query's Issues

Version prediction fails with assertion error when invoked from within wheel package in Spark job

Calling version_query.predict_version_str() from within a wheel package that was submitted with a Spark job causes the following error:

Traceback (most recent call last):
  File "/project/root/run/run.py", line 4, in <module>
    print(f"Running version {some_module.get_version()}")
  File "/project/root/dist/package-0.1.0.dev1+bfcaa08b-py3-none-any.whl/some_module.py", line 5, in get_version
  File "/project/root/venv/lib/python3.7/site-packages/version_query/query.py", line 96, in predict_version_str
    return predict_caller(2).to_str()
  File "/project/root/venv/lib/python3.7/site-packages/version_query/query.py", line 81, in predict_caller
    here = _caller_folder(stack_level + 1)
  File "/project/root/venv/lib/python3.7/site-packages/version_query/query.py", line 22, in _caller_folder
    assert here.is_file(), here
AssertionError: /project/root/dist/package-0.1.0.dev1+bfcaa08b-py3-none-any.whl/some_module.py

I've created a minimal example to reproduce this bug: https://github.com/mlangc/reproduce-spark-related-version-query-issue

Version prediction from git repository (commit count) broken after merge of branch.

Symptom: version prediction always results in 0.1.0.dev0 for ALL commits past merge commit 835e59c Merge branch 'develop' of gitlab.......-service into develop

git log --oneline --all --graph --decorate

* 1af294c (origin/develop, origin/HEAD) TEST: commit to test version.py
* ad8ab2d ...
* bcacad2 ...
*   835e59c  Merge branch 'develop' of gitlab.......-service into develop
|\  
| *   637e04c Merge branch 'feature/...' into 'develop'
| |\  
| | * b0361e8 ...
| |/  
* / 70b3501 ...
|/  
* 2774dc1 ...
...
(commits both in develop and feature/... branch)
...
| * 5ab611f (feature/...) FIX: ...
| * 5809732 FIX: ...
| * 5403ba6 REFACTORING: ...
| * 130a286 REFACTORING: ...
|/  

There are no GIT tags in directory.
Versioning is used to generate version with commit number only for CI: print(predict_version_str().split('+')[0]), resulting in e.g. 0.1.0.dev88 (from 0.1.0.dev88+2774dc19)

Example of LOGGING_LEVEL=debug on problematic merge commit:

โš“ 835e59c
LOGGING_LEVEL=debug python -m version_query -p .
DEBUG:version_query.version:version_query parsed version string '0.1.0.dev0' into <class 're.Match'>: {'release': '0.1.0', 'prerelease': '.dev0', 'local': None} ('0.1.0', '.dev0', None, None)
DEBUG:version_query.version:parsed pre-release string '.dev0' into ['.dev0']
DEBUG:version_query.version:version_query parsed version string '0.1.0.dev0' into <class 're.Match'>: {'release': '0.1.0', 'prerelease': '.dev0', 'local': None} ('0.1.0', '.dev0', None, None)
DEBUG:version_query.version:parsed pre-release string '.dev0' into ['.dev0']
0.1.0.dev0

Workaround: add tag, e.g. git tag v.0.2.0 on ANY commit in history, before, after merge in develop, or within merged branch itself.

Observation: putting tag before merge results in a commits in branch (reached via git checkout ) having it's separate versioning (instead of 2.1)
In this case with simple loop over git up && python -m version_query -p . I can confirm, that counter within branch change

Need to look into code and GIT structure/behavior to understand, what went wrong. It seems version-query can't traverse history with a merge back, and always assumes commit 0.
Any additional information I can supply to help?

Error running check-manifest

> python3 -m check_manifest -u
['/usr/bin/python3', 'setup.py', 'sdist', '-d', '/tmp/check-manifest-6wigkwia-sdist'] failed (status 1):
Traceback (most recent call last):
  File "setup.py", line 33, in <module>
    Package.setup()
  File "/tmp/check-manifest-ar9ccg4n-sources/setup_boilerplate.py", line 303, in setup
    cls.prepare()
  File "/tmp/check-manifest-ar9ccg4n-sources/setup_boilerplate.py", line 290, in prepare
    cls.version = find_version(cls.name)
  File "/tmp/check-manifest-ar9ccg4n-sources/setup_boilerplate.py", line 57, in find_version
    version_module_vars = runpy.run_path(version_module_path)
  File "/usr/lib64/python3.7/runpy.py", line 263, in run_path
    pkg_name=pkg_name, script_name=fname)
  File "/usr/lib64/python3.7/runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "/usr/lib64/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "version_query/_version.py", line 5, in <module>
    VERSION = predict_version_str()
  File "/tmp/check-manifest-ar9ccg4n-sources/version_query/query.py", line 75, in predict_version_str
    return predict_caller(2).to_str()
  File "/tmp/check-manifest-ar9ccg4n-sources/version_query/query.py", line 71, in predict_caller
    return predict_folder(here, True)
  File "/tmp/check-manifest-ar9ccg4n-sources/version_query/query.py", line 65, in predict_folder
    return query_folder(path, search_parent_directories=search_parent_directories)
  File "/tmp/check-manifest-ar9ccg4n-sources/version_query/query.py", line 37, in query_folder
    return query_package_folder(path, search_parent_directories=search_parent_directories)
  File "/tmp/check-manifest-ar9ccg4n-sources/version_query/py_query.py", line 42, in query_package_folder
    raise ValueError(paths, metadata_json_paths, pkg_info_paths)
ValueError: ([PosixPath('/tmp/check-manifest-ar9ccg4n-sources/version_query'), PosixPath('/tmp/check-manifest-ar9ccg4n-sources'), PosixPath('/tmp'), PosixPath('/')], [], [])

ValueError on Conda in windows environment

I got ValueError, when I tried to import typed_astunpare in Conda environment:

Python 3.7.3 | packaged by conda-forge | (default, Jul  1 2019, 22:01:29) [MSC v.1900 64 bit (AMD64)] :: Anaconda, Inc. on win64
Type "help", "copyright", "credits" or "license" for more information.
>>> from typed_astunparse import unparse
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\typed_astunparse-2.1.4-py3.7.egg\typed_astunparse\__init__.py", line 14, in <module>
    from ._version import VERSION
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\typed_astunparse-2.1.4-py3.7.egg\typed_astunparse\_version.py", line 5, in <module>
    VERSION = predict_version_str()
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\version_query-1.0.5-py3.7.egg\version_query\query.py", line 75, in predict_version_str
    return predict_caller(2).to_str()
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\version_query-1.0.5-py3.7.egg\version_query\query.py", line 71, in predict_caller
    return predict_folder(here, True)
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\version_query-1.0.5-py3.7.egg\version_query\query.py", line 65, in predict_folder
    return query_folder(path, search_parent_directories=search_parent_directories)
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\version_query-1.0.5-py3.7.egg\version_query\query.py", line 37, in query_folder
    return query_package_folder(path, search_parent_directories=search_parent_directories)
  File "C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\version_query-1.0.5-py3.7.egg\version_query\py_query.py", line 42, in query_package_folder
    raise ValueError(paths, metadata_json_paths, pkg_info_paths)
ValueError: ([WindowsPath('C:/ProgramData/Anaconda3/envs/greencarbon/lib/site-packages/typed_astunparse-2.1.4-py3.7.egg/typed_astunparse'), WindowsPath('C:/ProgramData/Anaconda3/envs/greencarbon/lib/site-packages/typed_astunparse-2.1.4-py3.7.egg'), WindowsPath('C:/ProgramData/Anaconda3/envs/greencarbon/lib/site-packages'), WindowsPath('C:/ProgramData/Anaconda3/envs/greencarbon/lib'), WindowsPath('C:/ProgramData/Anaconda3/envs/greencarbon'), WindowsPath('C:/ProgramData/Anaconda3/envs'), WindowsPath('C:/ProgramData/Anaconda3'), WindowsPath('C:/ProgramData'), WindowsPath('C:/')], [], [])

In my case 'PNG-INFO' is located on 'C:\ProgramData\Anaconda3\envs\greencarbon\lib\site-packages\typed_astunparse-2.1.4-py3.7.egg\EGG-INFO' directory.
Method 'query_package_folder' in 'py_query.py' tries to find it in 'EGG-INFO' directory, but only if directory name is in lowercase 'egg-info'.

Option to specify version tag prefix

In order to bring this tool to bear in repos that might have been tagged in a different way, it'd be nice to be able to specify a prefix or even a list of prefixes for tags to include in the processing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.