00001
00002
00003
00004
00005 from __future__ import unicode_literals
00006
00007 import functools
00008 import re
00009
00010
00011 from .compat import base_cmp
00012
00013 def _to_int(value):
00014 try:
00015 return int(value), True
00016 except ValueError:
00017 return value, False
00018
00019 def _has_leading_zero(value):
00020 return (value
00021 and value[0] == '0'
00022 and value.isdigit()
00023 and value != '0')
00024
00025
00026 def identifier_cmp(a, b):
00027 """Compare two identifier (for pre-release/build components)."""
00028
00029 a_cmp, a_is_int = _to_int(a)
00030 b_cmp, b_is_int = _to_int(b)
00031
00032 if a_is_int and b_is_int:
00033
00034 return base_cmp(a_cmp, b_cmp)
00035 elif a_is_int:
00036
00037 return -1
00038 elif b_is_int:
00039 return 1
00040 else:
00041
00042 return base_cmp(a_cmp, b_cmp)
00043
00044
00045 def identifier_list_cmp(a, b):
00046 """Compare two identifier list (pre-release/build components).
00047
00048 The rule is:
00049 - Identifiers are paired between lists
00050 - They are compared from left to right
00051 - If all first identifiers match, the longest list is greater.
00052
00053 >>> identifier_list_cmp(['1', '2'], ['1', '2'])
00054 0
00055 >>> identifier_list_cmp(['1', '2a'], ['1', '2b'])
00056 -1
00057 >>> identifier_list_cmp(['1'], ['1', '2'])
00058 -1
00059 """
00060 identifier_pairs = zip(a, b)
00061 for id_a, id_b in identifier_pairs:
00062 cmp_res = identifier_cmp(id_a, id_b)
00063 if cmp_res != 0:
00064 return cmp_res
00065
00066 return base_cmp(len(a), len(b))
00067
00068
00069 class Version(object):
00070
00071 version_re = re.compile('^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$')
00072 partial_version_re = re.compile('^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-([0-9a-zA-Z.-]*))?(?:\+([0-9a-zA-Z.-]*))?$')
00073
00074 def __init__(self, version_string, partial=False):
00075 major, minor, patch, prerelease, build = self.parse(version_string, partial)
00076
00077 self.major = major
00078 self.minor = minor
00079 self.patch = patch
00080 self.prerelease = prerelease
00081 self.build = build
00082
00083 self.partial = partial
00084
00085 @classmethod
00086 def _coerce(cls, value, allow_none=False):
00087 if value is None and allow_none:
00088 return value
00089 return int(value)
00090
00091 @classmethod
00092 def coerce(cls, version_string, partial=False):
00093 """Coerce an arbitrary version string into a semver-compatible one.
00094
00095 The rule is:
00096 - If not enough components, fill minor/patch with zeroes; unless
00097 partial=True
00098 - If more than 3 dot-separated components, extra components are "build"
00099 data. If some "build" data already appeared, append it to the
00100 extra components
00101
00102 Examples:
00103 >>> Version.coerce('0.1')
00104 Version(0, 1, 0)
00105 >>> Version.coerce('0.1.2.3')
00106 Version(0, 1, 2, (), ('3',))
00107 >>> Version.coerce('0.1.2.3+4')
00108 Version(0, 1, 2, (), ('3', '4'))
00109 >>> Version.coerce('0.1+2-3+4_5')
00110 Version(0, 1, 0, (), ('2-3', '4-5'))
00111 """
00112 base_re = re.compile(r'^\d+(?:\.\d+(?:\.\d+)?)?')
00113
00114 match = base_re.match(version_string)
00115 if not match:
00116 raise ValueError("Version string lacks a numerical component: %r"
00117 % version_string)
00118
00119 version = version_string[:match.end()]
00120 if not partial:
00121
00122 while version.count('.') < 2:
00123 version += '.0'
00124
00125 if match.end() == len(version_string):
00126 return Version(version, partial=partial)
00127
00128 rest = version_string[match.end():]
00129
00130
00131 rest = re.sub(r'[^a-zA-Z0-9+.-]', '-', rest)
00132
00133 if rest[0] == '+':
00134
00135 prerelease = ''
00136 build = rest[1:]
00137 elif rest[0] == '.':
00138
00139 prerelease = ''
00140 build = rest[1:]
00141 elif rest[0] == '-':
00142 rest = rest[1:]
00143 if '+' in rest:
00144 prerelease, build = rest.split('+', 1)
00145 else:
00146 prerelease, build = rest, ''
00147 elif '+' in rest:
00148 prerelease, build = rest.split('+', 1)
00149 else:
00150 prerelease, build = rest, ''
00151
00152 build = build.replace('+', '.')
00153
00154 if prerelease:
00155 version = '%s-%s' % (version, prerelease)
00156 if build:
00157 version = '%s+%s' % (version, build)
00158
00159 return cls(version, partial=partial)
00160
00161 @classmethod
00162 def parse(cls, version_string, partial=False, coerce=False):
00163 """Parse a version string into a Version() object.
00164
00165 Args:
00166 version_string (str), the version string to parse
00167 partial (bool), whether to accept incomplete input
00168 coerce (bool), whether to try to map the passed in string into a
00169 valid Version.
00170 """
00171 if not version_string:
00172 raise ValueError('Invalid empty version string: %r' % version_string)
00173
00174 if partial:
00175 version_re = cls.partial_version_re
00176 else:
00177 version_re = cls.version_re
00178
00179 match = version_re.match(version_string)
00180 if not match:
00181 raise ValueError('Invalid version string: %r' % version_string)
00182
00183 major, minor, patch, prerelease, build = match.groups()
00184
00185 if _has_leading_zero(major):
00186 raise ValueError("Invalid leading zero in major: %r" % version_string)
00187 if _has_leading_zero(minor):
00188 raise ValueError("Invalid leading zero in minor: %r" % version_string)
00189 if _has_leading_zero(patch):
00190 raise ValueError("Invalid leading zero in patch: %r" % version_string)
00191
00192 major = int(major)
00193 minor = cls._coerce(minor, partial)
00194 patch = cls._coerce(patch, partial)
00195
00196 if prerelease is None:
00197 if partial and (build is None):
00198
00199 return (major, minor, patch, None, None)
00200 else:
00201 prerelease = ()
00202 elif prerelease == '':
00203 prerelease = ()
00204 else:
00205 prerelease = tuple(prerelease.split('.'))
00206 cls._validate_identifiers(prerelease, allow_leading_zeroes=False)
00207
00208 if build is None:
00209 if partial:
00210 build = None
00211 else:
00212 build = ()
00213 elif build == '':
00214 build = ()
00215 else:
00216 build = tuple(build.split('.'))
00217 cls._validate_identifiers(build, allow_leading_zeroes=True)
00218
00219 return (major, minor, patch, prerelease, build)
00220
00221 @classmethod
00222 def _validate_identifiers(cls, identifiers, allow_leading_zeroes=False):
00223 for item in identifiers:
00224 if not item:
00225 raise ValueError("Invalid empty identifier %r in %r"
00226 % (item, '.'.join(identifiers)))
00227 if item[0] == '0' and item.isdigit() and item != '0' and not allow_leading_zeroes:
00228 raise ValueError("Invalid leading zero in identifier %r" % item)
00229
00230 def __iter__(self):
00231 return iter((self.major, self.minor, self.patch, self.prerelease, self.build))
00232
00233 def __str__(self):
00234 version = '%d' % self.major
00235 if self.minor is not None:
00236 version = '%s.%d' % (version, self.minor)
00237 if self.patch is not None:
00238 version = '%s.%d' % (version, self.patch)
00239
00240 if self.prerelease or (self.partial and self.prerelease == () and self.build is None):
00241 version = '%s-%s' % (version, '.'.join(self.prerelease))
00242 if self.build or (self.partial and self.build == ()):
00243 version = '%s+%s' % (version, '.'.join(self.build))
00244 return version
00245
00246 def __repr__(self):
00247 return 'Version(%r%s)' % (
00248 str(self),
00249 ', partial=True' if self.partial else '',
00250 )
00251
00252 @classmethod
00253 def _comparison_functions(cls, partial=False):
00254 """Retrieve comparison methods to apply on version components.
00255
00256 This is a private API.
00257
00258 Args:
00259 partial (bool): whether to provide 'partial' or 'strict' matching.
00260
00261 Returns:
00262 5-tuple of cmp-like functions.
00263 """
00264
00265 def prerelease_cmp(a, b):
00266 """Compare prerelease components.
00267
00268 Special rule: a version without prerelease component has higher
00269 precedence than one with a prerelease component.
00270 """
00271 if a and b:
00272 return identifier_list_cmp(a, b)
00273 elif a:
00274
00275 return -1
00276 elif b:
00277 return 1
00278 else:
00279 return 0
00280
00281 def build_cmp(a, b):
00282 """Compare build components.
00283
00284 Special rule: a version without build component has lower
00285 precedence than one with a build component.
00286 """
00287 if a and b:
00288 return identifier_list_cmp(a, b)
00289 elif a:
00290
00291 return 1
00292 elif b:
00293 return -1
00294 else:
00295 return 0
00296
00297 def make_optional(orig_cmp_fun):
00298 """Convert a cmp-like function to consider 'None == *'."""
00299 @functools.wraps(orig_cmp_fun)
00300 def alt_cmp_fun(a, b):
00301 if a is None or b is None:
00302 return 0
00303 return orig_cmp_fun(a, b)
00304
00305 return alt_cmp_fun
00306
00307 if partial:
00308 return [
00309 base_cmp,
00310 make_optional(base_cmp),
00311 make_optional(base_cmp),
00312 make_optional(prerelease_cmp),
00313 make_optional(build_cmp),
00314 ]
00315 else:
00316 return [
00317 base_cmp,
00318 base_cmp,
00319 base_cmp,
00320 prerelease_cmp,
00321 build_cmp,
00322 ]
00323
00324 def __cmp__(self, other):
00325 if not isinstance(other, self.__class__):
00326 return NotImplemented
00327
00328 field_pairs = zip(self, other)
00329 comparison_functions = self._comparison_functions(partial=self.partial or other.partial)
00330 comparisons = zip(comparison_functions, self, other)
00331
00332 for cmp_fun, self_field, other_field in comparisons:
00333 cmp_res = cmp_fun(self_field, other_field)
00334 if cmp_res != 0:
00335 return cmp_res
00336
00337 return 0
00338
00339 def __eq__(self, other):
00340 if not isinstance(other, self.__class__):
00341 return NotImplemented
00342
00343 return self.__cmp__(other) == 0
00344
00345 def __hash__(self):
00346 return hash((self.major, self.minor, self.patch, self.prerelease, self.build))
00347
00348 def __ne__(self, other):
00349 if not isinstance(other, self.__class__):
00350 return NotImplemented
00351
00352 return self.__cmp__(other) != 0
00353
00354 def __lt__(self, other):
00355 if not isinstance(other, self.__class__):
00356 return NotImplemented
00357
00358 return self.__cmp__(other) < 0
00359
00360 def __le__(self, other):
00361 if not isinstance(other, self.__class__):
00362 return NotImplemented
00363
00364 return self.__cmp__(other) <= 0
00365
00366 def __gt__(self, other):
00367 if not isinstance(other, self.__class__):
00368 return NotImplemented
00369
00370 return self.__cmp__(other) > 0
00371
00372 def __ge__(self, other):
00373 if not isinstance(other, self.__class__):
00374 return NotImplemented
00375
00376 return self.__cmp__(other) >= 0
00377
00378
00379 class SpecItem(object):
00380 """A requirement specification."""
00381
00382 KIND_LT = '<'
00383 KIND_LTE = '<='
00384 KIND_EQUAL = '=='
00385 KIND_GTE = '>='
00386 KIND_GT = '>'
00387 KIND_NEQ = '!='
00388
00389 STRICT_KINDS = (
00390 KIND_LT,
00391 KIND_LTE,
00392 KIND_EQUAL,
00393 KIND_GTE,
00394 KIND_GT,
00395 KIND_NEQ,
00396 )
00397
00398 re_spec = re.compile(r'^(<|<=|==|>=|>|!=)(\d.*)$')
00399
00400 def __init__(self, requirement_string):
00401 kind, spec = self.parse(requirement_string)
00402 self.kind = kind
00403 self.spec = spec
00404
00405 @classmethod
00406 def parse(cls, requirement_string):
00407 if not requirement_string:
00408 raise ValueError("Invalid empty requirement specification: %r" % requirement_string)
00409
00410 match = cls.re_spec.match(requirement_string)
00411 if not match:
00412 raise ValueError("Invalid requirement specification: %r" % requirement_string)
00413
00414 kind, version = match.groups()
00415 spec = Version(version, partial=True)
00416 return (kind, spec)
00417
00418 def match(self, version):
00419 if self.kind == self.KIND_LT:
00420 return version < self.spec
00421 elif self.kind == self.KIND_LTE:
00422 return version <= self.spec
00423 elif self.kind == self.KIND_EQUAL:
00424 return version == self.spec
00425 elif self.kind == self.KIND_GTE:
00426 return version >= self.spec
00427 elif self.kind == self.KIND_GT:
00428 return version > self.spec
00429 elif self.kind == self.KIND_NEQ:
00430 return version != self.spec
00431 else:
00432 raise ValueError('Unexpected match kind: %r' % self.kind)
00433
00434 def __str__(self):
00435 return '%s%s' % (self.kind, self.spec)
00436
00437 def __repr__(self):
00438 return '<SpecItem: %s %r>' % (self.kind, self.spec)
00439
00440 def __eq__(self, other):
00441 if not isinstance(other, SpecItem):
00442 return NotImplemented
00443 return self.kind == other.kind and self.spec == other.spec
00444
00445 def __hash__(self):
00446 return hash((self.kind, self.spec))
00447
00448
00449 class Spec(object):
00450 def __init__(self, *specs_strings):
00451 subspecs = [self.parse(spec) for spec in specs_strings]
00452 self.specs = sum(subspecs, ())
00453
00454 @classmethod
00455 def parse(self, specs_string):
00456 spec_texts = specs_string.split(',')
00457 return tuple(SpecItem(spec_text) for spec_text in spec_texts)
00458
00459 def match(self, version):
00460 """Check whether a Version satisfies the Spec."""
00461 return all(spec.match(version) for spec in self.specs)
00462
00463 def filter(self, versions):
00464 """Filter an iterable of versions satisfying the Spec."""
00465 for version in versions:
00466 if self.match(version):
00467 yield version
00468
00469 def select(self, versions):
00470 """Select the best compatible version among an iterable of options."""
00471 options = list(self.filter(versions))
00472 if options:
00473 return max(options)
00474 return None
00475
00476 def __contains__(self, version):
00477 if isinstance(version, Version):
00478 return self.match(version)
00479 return False
00480
00481 def __iter__(self):
00482 return iter(self.specs)
00483
00484 def __str__(self):
00485 return ','.join(str(spec) for spec in self.specs)
00486
00487 def __repr__(self):
00488 return '<Spec: %r>' % (self.specs,)
00489
00490 def __eq__(self, other):
00491 if not isinstance(other, Spec):
00492 return NotImplemented
00493
00494 return set(self.specs) == set(other.specs)
00495
00496 def __hash__(self):
00497 return hash(self.specs)
00498
00499
00500 def compare(v1, v2):
00501 return base_cmp(Version(v1), Version(v2))
00502
00503
00504 def match(spec, version):
00505 return Spec(spec).match(Version(version))
00506
00507
00508 def validate(version_string):
00509 """Validates a version string againt the SemVer specification."""
00510 try:
00511 Version.parse(version_string)
00512 return True
00513 except ValueError:
00514 return False