You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
5.4 KiB
188 lines
5.4 KiB
#!/usr/bin/env python
|
|
from contextlib import contextmanager
|
|
import errno
|
|
import inspect
|
|
import os
|
|
import pkg_resources
|
|
from subprocess import check_output, CalledProcessError
|
|
|
|
|
|
class Version(object):
|
|
def __init__(self, default='0', version_file='VERSION'):
|
|
self._version = None
|
|
self.default = default
|
|
self.version_file = version_file
|
|
|
|
@property
|
|
def version(self):
|
|
if self._version is not None:
|
|
return self._version.strip()
|
|
|
|
def imprint(self, path=None):
|
|
"""Write the determined version, if any, to ``self.version_file`` or
|
|
the path passed as an argument.
|
|
"""
|
|
if self.version is not None:
|
|
with open(path or self.version_file, 'w') as h:
|
|
h.write(self.version + '\n')
|
|
else:
|
|
raise ValueError('Can not write null version to file.')
|
|
return self
|
|
|
|
def from_file(self, path=None):
|
|
"""Look for a version in ``self.version_file``, or in the specified
|
|
path if supplied.
|
|
"""
|
|
if self._version is None:
|
|
self._version = file_version(path or self.version_file)
|
|
return self
|
|
|
|
def from_default(self):
|
|
if self._version is None:
|
|
self._version = self.default
|
|
return self
|
|
|
|
def from_git(self, path=None, prefer_daily=False):
|
|
"""Use Git to determine the package version.
|
|
|
|
This routine uses the __file__ value of the caller to determine
|
|
which Git repository root to use.
|
|
"""
|
|
if self._version is None:
|
|
frame = caller(1)
|
|
path = frame.f_globals.get('__file__') or '.'
|
|
providers = ([git_day, git_version] if prefer_daily
|
|
else [git_version, git_day])
|
|
for provider in providers:
|
|
if self._version is not None:
|
|
break
|
|
try:
|
|
with cd(path):
|
|
self._version = provider()
|
|
except CalledProcessError:
|
|
pass
|
|
except OSError as e:
|
|
if e.errno != errno.ENOENT:
|
|
raise
|
|
return self
|
|
|
|
def from_pkg(self):
|
|
"""Use pkg_resources to determine the installed package version.
|
|
"""
|
|
if self._version is None:
|
|
frame = caller(1)
|
|
pkg = frame.f_globals.get('__package__')
|
|
if pkg is not None:
|
|
self._version = pkg_version(pkg)
|
|
return self
|
|
|
|
def from_fn(self, fn):
|
|
if self._version is None:
|
|
self._version = fn()
|
|
return self
|
|
|
|
|
|
v2 = Version()
|
|
|
|
|
|
def file_version(name='VERSION'):
|
|
if os.path.exists(name):
|
|
with open(name) as h:
|
|
txt = h.read().strip()
|
|
if len(txt) != 0:
|
|
return s(txt)
|
|
|
|
|
|
def pkg_version(package=None):
|
|
try:
|
|
return s(pkg_resources.get_distribution(package).version)
|
|
except:
|
|
pass
|
|
|
|
|
|
def git_day():
|
|
"""Constructs a version string of the form:
|
|
|
|
day[.<commit-number-in-day>][+<branch-name-if-not-master>]
|
|
|
|
Master is understood to be always buildable and thus untagged
|
|
versions are treated as patch levels. Branches not master are treated
|
|
as PEP-440 "local version identifiers".
|
|
"""
|
|
vec = ['env', 'TZ=UTC', 'git', 'log', '--date=iso-local', '--pretty=%ad']
|
|
day = cmd(*(vec + ['-n', '1'])).split()[0]
|
|
commits = cmd(*(vec + ['--since', day + 'T00:00Z'])).strip()
|
|
n = len(commits.split('\n'))
|
|
day = day.replace('-', '')
|
|
if n > 1:
|
|
day += '.%s' % n
|
|
# Branches that are not master are treated as local:
|
|
# https://www.python.org/dev/peps/pep-0440/#local-version-identifiers
|
|
branch = get_git_branch()
|
|
if branch != 'master':
|
|
day += '+' + s(branch)
|
|
return day
|
|
|
|
|
|
def git_version():
|
|
"""Constructs a version string of the form:
|
|
|
|
<tag>[.<distance-from-tag>[+<branch-name-if-not-master>]]
|
|
|
|
Master is understood to be always buildable and thus untagged
|
|
versions are treated as patch levels. Branches not master are treated
|
|
as PEP-440 "local version identifiers".
|
|
"""
|
|
tag = cmd('git', 'describe').strip()
|
|
pieces = s(tag).split('-')
|
|
dotted = pieces[0]
|
|
if len(pieces) < 2:
|
|
distance = None
|
|
else:
|
|
# Distance from the latest tag is treated as a patch level.
|
|
distance = pieces[1]
|
|
dotted += '.' + s(distance)
|
|
# Branches that are not master are treated as local:
|
|
# https://www.python.org/dev/peps/pep-0440/#local-version-identifiers
|
|
if distance is not None:
|
|
branch = get_git_branch()
|
|
if branch != 'master':
|
|
dotted += '+' + s(branch)
|
|
return dotted
|
|
|
|
|
|
def get_git_branch():
|
|
return cmd('git', 'rev-parse', '--abbrev-ref', 'HEAD').strip()
|
|
|
|
|
|
@contextmanager
|
|
def cd(path='.'):
|
|
cwd = os.path.abspath(os.getcwd())
|
|
try:
|
|
try:
|
|
os.chdir(path)
|
|
except OSError as e:
|
|
if e.errno != errno.ENOTDIR:
|
|
raise
|
|
d = os.path.dirname(path)
|
|
os.chdir('.' if d == '' else d)
|
|
yield
|
|
finally:
|
|
os.chdir(cwd)
|
|
|
|
|
|
def caller(height=0):
|
|
caller = inspect.stack()[height+1]
|
|
return caller[0]
|
|
|
|
|
|
def s(something):
|
|
if isinstance(something, str):
|
|
return something
|
|
return something.decode()
|
|
|
|
|
|
def cmd(*args):
|
|
with open(os.devnull, 'w') as err:
|
|
return check_output(args, stderr=err)
|