| # 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+$', flags=re.UNICODE) |
| |
| _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() |