base.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 # Copyright (c) 2012-2013 Raphaël Barrois
00003 # This code is distributed under the two-clause BSD License.
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         # Numeric identifiers are compared as integers
00034         return base_cmp(a_cmp, b_cmp)
00035     elif a_is_int:
00036         # Numeric identifiers have lower precedence
00037         return -1
00038     elif b_is_int:
00039         return 1
00040     else:
00041         # Non-numeric identifers are compared lexicographically
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     # alpha1.3 < alpha1.3.1
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             # We need a not-partial version.
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         # Cleanup the 'rest'
00131         rest = re.sub(r'[^a-zA-Z0-9+.-]', '-', rest)
00132 
00133         if rest[0] == '+':
00134             # A 'build' component
00135             prerelease = ''
00136             build = rest[1:]
00137         elif rest[0] == '.':
00138             # An extra version component, probably 'build'
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                 # No build info, strip here
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                 # Versions with prerelease field have lower precedence
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                 # Versions with build field have higher precedence
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,  # Major is still mandatory
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:  # pragma: no cover
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


rocon_semantic_version
Author(s): Raphaël Barrois
autogenerated on Fri May 2 2014 10:35:51