Version.py
Version (Version)
¤
This class abstracts handling of a project's versions. It implements the
scheme defined in PEP 440. A Version
instance is comparison
aware and can be compared and sorted using the standard Python interfaces.
This class is descendant from Version found in packaging.version
,
and implements some additional, "AI"-like normalization during instantiation.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
version |
str |
The string representation of a version which will be parsed and normalized before use. |
required |
Exceptions:
Type | Description |
---|---|
InvalidVersion |
If the |
Source code in lastversion/Version.py
class Version(PackagingVersion):
"""
This class abstracts handling of a project's versions. It implements the
scheme defined in PEP 440. A `Version` instance is comparison
aware and can be compared and sorted using the standard Python interfaces.
This class is descendant from Version found in `packaging.version`,
and implements some additional, "AI"-like normalization during instantiation.
Args:
version (str): The string representation of a version which will be
parsed and normalized before use.
Raises:
InvalidVersion: If the ``version`` does not conform to PEP 440 in
any way then this exception will be raised.
"""
def fix_letter_post_release(self, match):
self.fixed_letter_post_release = True
return match.group(1) + '.post' + str(ord(match.group(2)))
def __init__(self, version, char_fix_required=False):
"""Instantiate the `Version` object.
Args:
version (str): The version-like string
char_fix_required (bool): Should we treat alphanumerics as part of version
"""
self.fixed_letter_post_release = False
# 4.27-chaos-preview-3 -> 4.27-chaos-pre3
version = re.sub('-preview-(\\d+)', '-pre\\1', version, 1)
# 5.0.0-early-access-2 -> 5.0.0-alpha2
version = re.sub('-early-access-(\\d+)', '-alpha\\1', version, 1)
# many times they would tag foo-1.2.3 which would parse to LegacyVersion
# we can avoid this, by reassigning to what comes after the dash:
parts = version.split('-')
# TODO test v5.12-rc1-dontuse -> v5.12.rc1
# go through parts separated by dot, detect beta level, and weed out numberless info:
parts_n = []
for part in parts:
# help devel releases to be correctly identified
# https://www.python.org/dev/peps/pep-0440/#developmental-releases
if part in ['devel', 'test', 'dev']:
part = 'dev0'
elif part in ['beta']:
# "4.3.0-beta"
part = 'b0'
else:
# help post (patch) releases to be correctly identified (e.g. Magento 2.3.4-p2)
# p12 => post12
part = re.sub('^p(\\d+)$', 'post\\1', part, 1)
if not part.isalpha():
parts_n.append(part)
if not parts_n:
raise InvalidVersion("Invalid version: '{0}'".format(version))
# remove *any* non-digits which appear at the beginning of the version string
# e.g. Rhino1_7_13_Release does not even bother to put a delimiter...
# such string at the beginning typically do not convey stability level,
# so we are fine to remove them (unlike the ones in the tail)
parts_n[0] = re.sub('^[^0-9]+', '', parts_n[0], 1)
# go back to full string parse out
version = ".".join(parts_n)
if char_fix_required:
version = re.sub('(\\d)([a-z])$', self.fix_letter_post_release, version, 1)
# release-3_0_2 is often seen on Mercurial holders
# note that above code removes "release-" already, so we are left with "3_0_2"
if re.search(r'^(?:\d+_)+(?:\d+)', version):
version = version.replace('_', '.')
# finally, split by dot "delimiter", see if there are common words which are definitely
# removable
parts = version.split('.')
version = []
for p in parts:
if p.lower() in ['release']:
continue
version.append(p)
version = '.'.join(version)
super(Version, self).__init__(version)
@property
def epoch(self):
# type: () -> int
"""
An integer giving the version epoch of this Version instance
"""
_epoch = self._version.epoch # type: int
return _epoch
@property
def release(self):
"""
A tuple of integers giving the components of the release segment
of this Version instance; that is, the 1.2.3 part of the version
number, including trailing zeroes but not including the epoch or
any prerelease/development/postrelease suffixes
"""
_release = self._version.release
return _release
@property
def pre(self):
_pre = self._version.pre
return _pre
@property
def post(self):
return self._version.post[1] if self._version.post else None
@property
def dev(self):
return self._version.dev[1] if self._version.dev else None
@property
def local(self):
if self._version.local:
return ".".join(str(x) for x in self._version.local)
return None
@property
def major(self):
# type: () -> int
return self.release[0] if len(self.release) >= 1 else 0
@property
def minor(self):
# type: () -> int
return self.release[1] if len(self.release) >= 2 else 0
@property
def micro(self):
# type: () -> int
return self.release[2] if len(self.release) >= 3 else 0
@property
def is_prerelease(self):
# type: () -> bool
if self.major and self.minor and self.micro >= 90:
return True
return self.dev is not None or self.pre is not None
@property
def even(self):
return self.minor and not self.minor % 2
def sem_extract_base(self, level=None):
"""
Return Version with desired semantic version level base
E.g. for 5.9.3 it will return 5.9 (patch is None)
"""
if level == 'major':
# get major
return Version(str(self.major))
if level == 'minor':
return Version("{}.{}".format(self.major, self.minor))
if level == 'patch':
return Version("{}.{}.{}".format(self.major, self.minor, self.micro))
return self
def __str__(self):
# type: () -> str
parts = []
# Epoch
if self.epoch != 0:
parts.append("{0}!".format(self.epoch))
# Release segment
parts.append(".".join(str(x) for x in self.release))
# Pre-release
if self.pre is not None:
parts.append("".join(str(x) for x in self.pre))
# Post-release
if self.post is not None:
if self.fixed_letter_post_release:
parts.append("{0}".format(chr(self.post)))
else:
parts.append(".post{0}".format(self.post))
# Development release
if self.dev is not None:
parts.append(".dev{0}".format(self.dev))
# Local version segment
if self.local is not None:
parts.append("+{0}".format(self.local))
return "".join(parts)
epoch
property
readonly
¤
An integer giving the version epoch of this Version instance
release
property
readonly
¤
A tuple of integers giving the components of the release segment of this Version instance; that is, the 1.2.3 part of the version number, including trailing zeroes but not including the epoch or any prerelease/development/postrelease suffixes
__init__(self, version, char_fix_required=False)
special
¤
Instantiate the Version
object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
version |
str |
The version-like string |
required |
char_fix_required |
bool |
Should we treat alphanumerics as part of version |
False |
Source code in lastversion/Version.py
def __init__(self, version, char_fix_required=False):
"""Instantiate the `Version` object.
Args:
version (str): The version-like string
char_fix_required (bool): Should we treat alphanumerics as part of version
"""
self.fixed_letter_post_release = False
# 4.27-chaos-preview-3 -> 4.27-chaos-pre3
version = re.sub('-preview-(\\d+)', '-pre\\1', version, 1)
# 5.0.0-early-access-2 -> 5.0.0-alpha2
version = re.sub('-early-access-(\\d+)', '-alpha\\1', version, 1)
# many times they would tag foo-1.2.3 which would parse to LegacyVersion
# we can avoid this, by reassigning to what comes after the dash:
parts = version.split('-')
# TODO test v5.12-rc1-dontuse -> v5.12.rc1
# go through parts separated by dot, detect beta level, and weed out numberless info:
parts_n = []
for part in parts:
# help devel releases to be correctly identified
# https://www.python.org/dev/peps/pep-0440/#developmental-releases
if part in ['devel', 'test', 'dev']:
part = 'dev0'
elif part in ['beta']:
# "4.3.0-beta"
part = 'b0'
else:
# help post (patch) releases to be correctly identified (e.g. Magento 2.3.4-p2)
# p12 => post12
part = re.sub('^p(\\d+)$', 'post\\1', part, 1)
if not part.isalpha():
parts_n.append(part)
if not parts_n:
raise InvalidVersion("Invalid version: '{0}'".format(version))
# remove *any* non-digits which appear at the beginning of the version string
# e.g. Rhino1_7_13_Release does not even bother to put a delimiter...
# such string at the beginning typically do not convey stability level,
# so we are fine to remove them (unlike the ones in the tail)
parts_n[0] = re.sub('^[^0-9]+', '', parts_n[0], 1)
# go back to full string parse out
version = ".".join(parts_n)
if char_fix_required:
version = re.sub('(\\d)([a-z])$', self.fix_letter_post_release, version, 1)
# release-3_0_2 is often seen on Mercurial holders
# note that above code removes "release-" already, so we are left with "3_0_2"
if re.search(r'^(?:\d+_)+(?:\d+)', version):
version = version.replace('_', '.')
# finally, split by dot "delimiter", see if there are common words which are definitely
# removable
parts = version.split('.')
version = []
for p in parts:
if p.lower() in ['release']:
continue
version.append(p)
version = '.'.join(version)
super(Version, self).__init__(version)
sem_extract_base(self, level=None)
¤
Return Version with desired semantic version level base E.g. for 5.9.3 it will return 5.9 (patch is None)
Source code in lastversion/Version.py
def sem_extract_base(self, level=None):
"""
Return Version with desired semantic version level base
E.g. for 5.9.3 it will return 5.9 (patch is None)
"""
if level == 'major':
# get major
return Version(str(self.major))
if level == 'minor':
return Version("{}.{}".format(self.major, self.minor))
if level == 'patch':
return Version("{}.{}.{}".format(self.major, self.minor, self.micro))
return self