ARIA-140 Version utils Provided mainly to support version comparisons for plugins in order to select the highest version of available plugins that would match a plugin specification. In the future may be useful for other version comparisons.
Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/eae44d0b Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/eae44d0b Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/eae44d0b Branch: refs/heads/ARIA-208-Missing-back-refrences-for-models Commit: eae44d0b0d051fd50cadf94d734bdf7a3b8d3171 Parents: d0411d3 Author: Tal Liron <tal.li...@gmail.com> Authored: Fri Apr 14 13:39:02 2017 -0500 Committer: Tal Liron <tal.li...@gmail.com> Committed: Tue May 9 15:37:46 2017 -0500 ---------------------------------------------------------------------- aria/modeling/service_template.py | 17 ++-- aria/utils/versions.py | 162 +++++++++++++++++++++++++++++++++ tests/utils/test_versions.py | 85 +++++++++++++++++ 3 files changed, 257 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/eae44d0b/aria/modeling/service_template.py ---------------------------------------------------------------------- diff --git a/aria/modeling/service_template.py b/aria/modeling/service_template.py index f721b64..e3320fa 100644 --- a/aria/modeling/service_template.py +++ b/aria/modeling/service_template.py @@ -33,7 +33,8 @@ from sqlalchemy.ext.associationproxy import association_proxy from ..parser import validation from ..parser.consumption import ConsumptionContext from ..parser.reading import deepcopy_with_locators -from ..utils import collections, formatting, console +from ..utils import (collections, formatting, console) +from ..utils.versions import VersionString from .mixins import TemplateModelMixin from . import ( relationship, @@ -2135,13 +2136,15 @@ class PluginSpecificationBase(TemplateModelMixin): # moved to. plugins = model_storage.plugin.list() matching_plugins = [] - for plugin in plugins: - # TODO: we need to use a version comparator - if (plugin.name == self.name) and \ - ((self.version is None) or (plugin.package_version >= self.version)): - matching_plugins.append(plugin) + if plugins: + for plugin in plugins: + if (plugin.name == self.name) and \ + ((self.version is None) or \ + (VersionString(plugin.package_version) >= self.version)): + matching_plugins.append(plugin) self.plugin = None if matching_plugins: # Return highest version of plugin - self.plugin = sorted(matching_plugins, key=lambda plugin: plugin.package_version)[-1] + key = lambda plugin: VersionString(plugin.package_version).key + self.plugin = sorted(matching_plugins, key=key)[-1] return self.plugin is not None http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/eae44d0b/aria/utils/versions.py ---------------------------------------------------------------------- diff --git a/aria/utils/versions.py b/aria/utils/versions.py new file mode 100644 index 0000000..925f59e --- /dev/null +++ b/aria/utils/versions.py @@ -0,0 +1,162 @@ +# 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. + +""" +General-purpose version string handling +""" + +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: The version string + :returns: The primary tuple and qualifier float + :rtype: ((int), 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() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/eae44d0b/tests/utils/test_versions.py ---------------------------------------------------------------------- diff --git a/tests/utils/test_versions.py b/tests/utils/test_versions.py new file mode 100644 index 0000000..222949c --- /dev/null +++ b/tests/utils/test_versions.py @@ -0,0 +1,85 @@ +# 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. + +from aria.utils.versions import (VersionString, parse_version_string) + + +def test_version_string(): + # No qualifiers + assert VersionString('20') == VersionString('20') + assert VersionString('20') == VersionString('20.0') + assert VersionString('20') == VersionString('20.0.0') + assert VersionString('20') < VersionString('20.0.1') + + # With numeric qualifiers + assert VersionString('20.0.1-1') < VersionString('20.0.1-2') + assert VersionString('20.0.1-0') < VersionString('20.0.1') + assert VersionString('20.0.1-1') < VersionString('20.0.1') + + # With prefixed qualifiers + assert VersionString('20.0.1-beta1') < VersionString('20.0.1-beta2') + assert VersionString('20.0.1-beta1') < VersionString('20.0.1-1') + assert VersionString('20.0.1-beta1') < VersionString('20.0.1') + assert VersionString('20.0.1-beta2') < VersionString('20.0.1-rc2') + assert VersionString('20.0.1-alpha2') < VersionString('20.0.1-beta1') + assert VersionString('20.0.1-dev2') < VersionString('20.0.1-alpha1') + assert VersionString('20.0.1-DEV2') < VersionString('20.0.1-ALPHA1') + + # Coercive comparisons + assert VersionString('20.0.0') == VersionString(10 * 2) + assert VersionString('20.0.0') == VersionString(20.0) + + # Non-VersionString comparisons + assert VersionString('20.0.0') == 20 + assert VersionString('20.0.0') < '20.0.1' + + # Nulls + assert VersionString() == VersionString() + assert VersionString() == VersionString.NULL + assert VersionString(None) == VersionString.NULL + assert VersionString.NULL == None # pylint: disable=singleton-comparison + assert VersionString.NULL == 0 + + # Invalid version strings + assert VersionString('maxim is maxim') == VersionString.NULL + assert VersionString('20.maxim.0') == VersionString.NULL + assert VersionString('20.0.0-maxim1') == VersionString.NULL + assert VersionString('20.0.1-1.1') == VersionString.NULL + + # Sorts + v1 = VersionString('20.0.0') + v2 = VersionString('20.0.1-beta1') + v3 = VersionString('20.0.1') + v4 = VersionString('20.0.2') + assert [v1, v2, v3, v4] == sorted([v4, v3, v2, v1], key=lambda v: v.key) + + # Sets + v1 = VersionString('20.0.0') + v2 = VersionString('20.0') + v3 = VersionString('20') + assert set([v1]) == set([v1, v2, v3]) + + # Dicts + the_dict = {v1: 'test'} + assert the_dict.get(v2) == 'test' + +def test_parse_version_string(): + # One test of each type from the groups above should be enough + assert parse_version_string('20') < parse_version_string('20.0.1') + assert parse_version_string('20.0.1-1') < parse_version_string('20.0.1-2') + assert parse_version_string('20.0.1-beta1') < parse_version_string('20.0.1-beta2') + assert parse_version_string('20.0.0') == parse_version_string(10 * 2) + assert parse_version_string(None) == parse_version_string(0) + assert parse_version_string(None) == parse_version_string('maxim is maxim')