blob: 521004c41495525ffe08b2b663bea36aae181fa1 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Verion string utilities.
"""
import re
_INF = float('inf')
_NULL = (), _INF
_DIGITS_RE = re.compile(r'^\d+$')
_PREFIXES = {
'dev': 0.0001,
'alpha': 0.001,
'beta': 0.01,
'rc': 0.1
}
class VersionString(unicode):
"""
Version string that can be compared, sorted, made unique in a set, and used as a unique dict
key.
The primary part of the string is one or more dot-separated natural numbers. Trailing zeroes
are treated as redundant, e.g. "1.0.0" == "1.0" == "1".
An optional qualifier can be added after a "-". The qualifier can be a natural number or a
specially treated prefixed natural number, e.g. "1.1-beta1" > "1.1-alpha2". The case of the
prefix is ignored.
Numeric qualifiers will always be greater than prefixed integer qualifiers, e.g. "1.1-1" >
"1.1-beta1".
Versions without a qualifier will always be greater than their equivalents with a qualifier,
e.g. e.g. "1.1" > "1.1-1".
Any value that does not conform to this format will be treated as a zero version, which would
be lesser than any non-zero version.
For efficient list sorts use the ``key`` property, e.g.::
sorted(versions, key=lambda x: x.key)
"""
NULL = None # initialized below
def __init__(self, value=None):
if value is not None:
super(VersionString, self).__init__(value)
self.key = parse_version_string(self)
def __eq__(self, version):
if not isinstance(version, VersionString):
version = VersionString(version)
return self.key == version.key
def __lt__(self, version):
if not isinstance(version, VersionString):
version = VersionString(version)
return self.key < version.key
def __hash__(self):
return self.key.__hash__()
def parse_version_string(version): # pylint: disable=too-many-branches
"""
Parses a version string.
:param version: version string
:returns: primary tuple and qualifier float
:rtype: ((:obj:`int`), :obj:`float`)
"""
if version is None:
return _NULL
version = unicode(version)
# Split to primary and qualifier on '-'
split = version.split('-', 1)
if len(split) == 2:
primary, qualifier = split
else:
primary = split[0]
qualifier = None
# Parse primary
split = primary.split('.')
primary = []
for element in split:
if _DIGITS_RE.match(element) is None:
# Invalid version string
return _NULL
try:
element = int(element)
except ValueError:
# Invalid version string
return _NULL
primary.append(element)
# Remove redundant zeros
for element in reversed(primary):
if element == 0:
primary.pop()
else:
break
primary = tuple(primary)
# Parse qualifier
if qualifier is not None:
if _DIGITS_RE.match(qualifier) is not None:
# Integer qualifier
try:
qualifier = float(int(qualifier))
except ValueError:
# Invalid version string
return _NULL
else:
# Prefixed integer qualifier
value = None
qualifier = qualifier.lower()
for prefix, factor in _PREFIXES.iteritems():
if qualifier.startswith(prefix):
value = qualifier[len(prefix):]
if _DIGITS_RE.match(value) is None:
# Invalid version string
return _NULL
try:
value = float(int(value)) * factor
except ValueError:
# Invalid version string
return _NULL
break
if value is None:
# Invalid version string
return _NULL
qualifier = value
else:
# Version strings with no qualifiers are higher
qualifier = _INF
return primary, qualifier
VersionString.NULL = VersionString()