1# Copyright (c) 2020 Matt Martz <matt@sivel.net> 2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 4# Make coding more python3-ish 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8import re 9 10from distutils.version import LooseVersion, Version 11 12from ansible.module_utils.six import text_type 13 14 15# Regular expression taken from 16# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 17SEMVER_RE = re.compile( 18 r''' 19 ^ 20 (?P<major>0|[1-9]\d*) 21 \. 22 (?P<minor>0|[1-9]\d*) 23 \. 24 (?P<patch>0|[1-9]\d*) 25 (?: 26 - 27 (?P<prerelease> 28 (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) 29 (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* 30 ) 31 )? 32 (?: 33 \+ 34 (?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*) 35 )? 36 $ 37 ''', 38 flags=re.X 39) 40 41 42class _Alpha: 43 """Class to easily allow comparing strings 44 45 Largely this exists to make comparing an integer and a string on py3 46 so that it works like py2. 47 """ 48 def __init__(self, specifier): 49 self.specifier = specifier 50 51 def __repr__(self): 52 return repr(self.specifier) 53 54 def __eq__(self, other): 55 if isinstance(other, _Alpha): 56 return self.specifier == other.specifier 57 elif isinstance(other, str): 58 return self.specifier == other 59 60 return False 61 62 def __ne__(self, other): 63 return not self.__eq__(other) 64 65 def __lt__(self, other): 66 if isinstance(other, _Alpha): 67 return self.specifier < other.specifier 68 elif isinstance(other, str): 69 return self.specifier < other 70 elif isinstance(other, _Numeric): 71 return False 72 73 raise ValueError 74 75 def __le__(self, other): 76 return self.__lt__(other) or self.__eq__(other) 77 78 def __gt__(self, other): 79 return not self.__le__(other) 80 81 def __ge__(self, other): 82 return not self.__lt__(other) 83 84 85class _Numeric: 86 """Class to easily allow comparing numbers 87 88 Largely this exists to make comparing an integer and a string on py3 89 so that it works like py2. 90 """ 91 def __init__(self, specifier): 92 self.specifier = int(specifier) 93 94 def __repr__(self): 95 return repr(self.specifier) 96 97 def __eq__(self, other): 98 if isinstance(other, _Numeric): 99 return self.specifier == other.specifier 100 elif isinstance(other, int): 101 return self.specifier == other 102 103 return False 104 105 def __ne__(self, other): 106 return not self.__eq__(other) 107 108 def __lt__(self, other): 109 if isinstance(other, _Numeric): 110 return self.specifier < other.specifier 111 elif isinstance(other, int): 112 return self.specifier < other 113 elif isinstance(other, _Alpha): 114 return True 115 116 raise ValueError 117 118 def __le__(self, other): 119 return self.__lt__(other) or self.__eq__(other) 120 121 def __gt__(self, other): 122 return not self.__le__(other) 123 124 def __ge__(self, other): 125 return not self.__lt__(other) 126 127 128class SemanticVersion(Version): 129 """Version comparison class that implements Semantic Versioning 2.0.0 130 131 Based off of ``distutils.version.Version`` 132 """ 133 134 version_re = SEMVER_RE 135 136 def __init__(self, vstring=None): 137 self.vstring = vstring 138 self.major = None 139 self.minor = None 140 self.patch = None 141 self.prerelease = () 142 self.buildmetadata = () 143 144 if vstring: 145 self.parse(vstring) 146 147 def __repr__(self): 148 return 'SemanticVersion(%r)' % self.vstring 149 150 @staticmethod 151 def from_loose_version(loose_version): 152 """This method is designed to take a ``LooseVersion`` 153 and attempt to construct a ``SemanticVersion`` from it 154 155 This is useful where you want to do simple version math 156 without requiring users to provide a compliant semver. 157 """ 158 if not isinstance(loose_version, LooseVersion): 159 raise ValueError("%r is not a LooseVersion" % loose_version) 160 161 try: 162 version = loose_version.version[:] 163 except AttributeError: 164 raise ValueError("%r is not a LooseVersion" % loose_version) 165 166 extra_idx = 3 167 for marker in ('-', '+'): 168 try: 169 idx = version.index(marker) 170 except ValueError: 171 continue 172 else: 173 if idx < extra_idx: 174 extra_idx = idx 175 version = version[:extra_idx] 176 177 if version and set(type(v) for v in version) != set((int,)): 178 raise ValueError("Non integer values in %r" % loose_version) 179 180 # Extra is everything to the right of the core version 181 extra = re.search('[+-].+$', loose_version.vstring) 182 183 version = version + [0] * (3 - len(version)) 184 return SemanticVersion( 185 '%s%s' % ( 186 '.'.join(str(v) for v in version), 187 extra.group(0) if extra else '' 188 ) 189 ) 190 191 def parse(self, vstring): 192 match = SEMVER_RE.match(vstring) 193 if not match: 194 raise ValueError("invalid semantic version '%s'" % vstring) 195 196 (major, minor, patch, prerelease, buildmetadata) = match.group(1, 2, 3, 4, 5) 197 self.major = int(major) 198 self.minor = int(minor) 199 self.patch = int(patch) 200 201 if prerelease: 202 self.prerelease = tuple(_Numeric(x) if x.isdigit() else _Alpha(x) for x in prerelease.split('.')) 203 if buildmetadata: 204 self.buildmetadata = tuple(_Numeric(x) if x.isdigit() else _Alpha(x) for x in buildmetadata.split('.')) 205 206 @property 207 def core(self): 208 return self.major, self.minor, self.patch 209 210 @property 211 def is_prerelease(self): 212 return bool(self.prerelease) 213 214 @property 215 def is_stable(self): 216 # Major version zero (0.y.z) is for initial development. Anything MAY change at any time. 217 # The public API SHOULD NOT be considered stable. 218 # https://semver.org/#spec-item-4 219 return not (self.major == 0 or self.is_prerelease) 220 221 def _cmp(self, other): 222 if isinstance(other, str): 223 other = SemanticVersion(other) 224 225 if self.core != other.core: 226 # if the core version doesn't match 227 # prerelease and buildmetadata doesn't matter 228 if self.core < other.core: 229 return -1 230 else: 231 return 1 232 233 if not any((self.prerelease, other.prerelease)): 234 return 0 235 236 if self.prerelease and not other.prerelease: 237 return -1 238 elif not self.prerelease and other.prerelease: 239 return 1 240 else: 241 if self.prerelease < other.prerelease: 242 return -1 243 elif self.prerelease > other.prerelease: 244 return 1 245 246 # Build metadata MUST be ignored when determining version precedence 247 # https://semver.org/#spec-item-10 248 # With the above in mind it is ignored here 249 250 # If we have made it here, things should be equal 251 return 0 252 253 # The Py2 and Py3 implementations of distutils.version.Version 254 # are quite different, this makes the Py2 and Py3 implementations 255 # the same 256 def __eq__(self, other): 257 return self._cmp(other) == 0 258 259 def __ne__(self, other): 260 return not self.__eq__(other) 261 262 def __lt__(self, other): 263 return self._cmp(other) < 0 264 265 def __le__(self, other): 266 return self._cmp(other) <= 0 267 268 def __gt__(self, other): 269 return self._cmp(other) > 0 270 271 def __ge__(self, other): 272 return self._cmp(other) >= 0 273