diff options
author | Raphaël Barrois <raphael.barrois@polytechnique.org> | 2012-05-14 23:37:42 +0200 |
---|---|---|
committer | Raphaël Barrois <raphael.barrois@polytechnique.org> | 2012-05-14 23:37:42 +0200 |
commit | ae1d1ade64d5f0a5dab9b1c9bb8f933708c03694 (patch) | |
tree | 36e75f1d946756ebf0cbaec0c38fcce420362eac | |
parent | 868e2a7bb86bc7586aa6f686d8f5af41fa3a0a92 (diff) | |
download | semantic-version-ae1d1ade64d5f0a5dab9b1c9bb8f933708c03694.tar.gz |
Move code into 'base' module.
Signed-off-by: Raphaël Barrois <raphael.barrois@polytechnique.org>
-rw-r--r-- | src/semantic_version/__init__.py | 118 | ||||
-rw-r--r-- | src/semantic_version/base.py | 344 |
2 files changed, 345 insertions, 117 deletions
diff --git a/src/semantic_version/__init__.py b/src/semantic_version/__init__.py index 2dbf9bb..18019f6 100644 --- a/src/semantic_version/__init__.py +++ b/src/semantic_version/__init__.py @@ -2,123 +2,7 @@ # Copyright (c) 2012 Raphaël Barrois -import itertools -import re - __version__ = '0.1.0' -def _to_int(value): - try: - return int(value), True - except ValueError: - return value, False - - -def identifier_cmp(a, b): - """Compare two identifier (for pre-release/build components).""" - - a_cmp, a_is_int = _to_int(a) - b_cmp, b_is_int = _to_int(b) - - if a_is_int and b_is_int: - # Numeric identifiers are compared as integers - return cmp(a_cmp, b_cmp) - elif a_is_int: - # Numeric identifiers have lower precedence - return -1 - elif b_is_int: - return 1 - else: - # Non-numeric identifers are compared lexicographically - return cmp(a_cmp, b_cmp) - - -def identifier_list_cmp(a, b): - identifier_pairs = zip(a, b) - for id_a, id_b in identifier_pairs: - cmp_res = identifier_cmp(id_a, id_b) - if cmp_res != 0: - return cmp_res - # alpha1.3 < alpha1.3.1 - return cmp(len(a), len(b)) - - -class SemanticVersion(object): - - version_re = re.compile('^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') - - def __init__(self, version_string): - major, minor, patch, prerelease, build = self.parse(version_string) - self.major = int(major) - self.minor = int(minor) - self.patch = int(patch) - if prerelease is None: - self.prerelease = [] - else: - self.prerelease = prerelease.split('.') - if build is None: - self.build = [] - else: - self.build = build.split('.') - - @classmethod - def parse(cls, version_string): - if not version_string: - raise ValueError('Invalid version string %r.' % version_string) - - match = cls.version_re.match(version_string) - if match: - return match.groups() - else: - raise ValueError('Invalid version string %r.' % version_string) - - def __str__(self): - prerelease = '.'.join(self.prerelease) - build = '.'.join(self.build) - version = '%d.%d.%d' % (self.major, self.minor, self.patch) - if prerelease: - version = '%s-%s' % (version, prerelease) - if build: - version = '%s+%s' % (version, build) - return version - - def __repr__(self): - return '<SemVer(%d, %d, %d, %r, %r)>' % ( - self.major, - self.minor, - self.patch, - self.prerelease, - self.build, - ) - - def __cmp__(self, other): - if not isinstance(other, SemanticVersion): - return NotImplemented - - base_cmp = cmp( - (self.major, self.minor, self.patch), - (other.major, other.minor, other.patch)) - - if base_cmp != 0: - return base_cmp - - if self.prerelease and other.prerelease: - prerelease_cmp = identifier_list_cmp(self.prerelease, other.prerelease) - if prerelease_cmp != 0: - return prerelease_cmp - elif self.prerelease: - # Prerelease version have lower precedence - return -1 - elif other.prerelease: - return 1 - - if self.build and other.build: - return identifier_list_cmp(self.build, other.build) - elif self.build: - # Build version have higher precedence - return 1 - elif other.build: - return -1 - else: - return 0 +from .base import compare, match, SemanticVersion, RequirementSpec diff --git a/src/semantic_version/base.py b/src/semantic_version/base.py new file mode 100644 index 0000000..1ffccab --- /dev/null +++ b/src/semantic_version/base.py @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2012 Raphaël Barrois + + +import functools +import re + +def _to_int(value): + try: + return int(value), True + except ValueError: + return value, False + + +def identifier_cmp(a, b): + """Compare two identifier (for pre-release/build components).""" + + a_cmp, a_is_int = _to_int(a) + b_cmp, b_is_int = _to_int(b) + + if a_is_int and b_is_int: + # Numeric identifiers are compared as integers + return cmp(a_cmp, b_cmp) + elif a_is_int: + # Numeric identifiers have lower precedence + return -1 + elif b_is_int: + return 1 + else: + # Non-numeric identifers are compared lexicographically + return cmp(a_cmp, b_cmp) + + +def identifier_list_cmp(a, b): + """Compare two identifier list (pre-release/build components). + + The rule is: + - Identifiers are paired between lists + - They are compared from left to right + - If all first identifiers match, the longest list is greater. + + >>> identifier_list_cmp(['1', '2'], ['1', '2']) + 0 + >>> identifier_list_cmp(['1', '2a'], ['1', '2b']) + -1 + >>> identifier_list_cmp(['1'], ['1', '2']) + -1 + """ + identifier_pairs = zip(a, b) + for id_a, id_b in identifier_pairs: + cmp_res = identifier_cmp(id_a, id_b) + if cmp_res != 0: + return cmp_res + # alpha1.3 < alpha1.3.1 + return cmp(len(a), len(b)) + + +class SemanticVersion(object): + + version_re = re.compile('^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') + partial_version_re = re.compile('^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') + + def __init__(self, version_string, partial=False): + major, minor, patch, prerelease, build = self.parse(version_string, partial) + + self.major = major + self.minor = minor + self.patch = patch + self.prerelease = prerelease + self.build = build + + self.partial = partial + + @classmethod + def parse(cls, version_string, partial=False): + if not version_string: + raise ValueError('Invalid empty version string: %r' % version_string) + + if partial: + version_re = cls.partial_version_re + else: + version_re = cls.version_re + + match = version_re.match(version_string) + if not match: + raise ValueError('Invalid version string: %r' % version_string) + + major, minor, patch, prerelease, build = match.groups() + + major = int(major) + + if minor is None: + if partial: + return (major, None, None, None, None) + else: + raise ValueError('Missing minor number: %r' % version_string) + else: + minor = int(minor) + + if patch is None: + if partial: + return (major, minor, None, None, None) + else: + raise ValueError('Missing patch number: %r' % version_string) + else: + patch = int(patch) + + if prerelease is None: + if partial and not build: + # No build info, strip here + return (major, minor, patch, None, None) + else: + prerelease = [] + else: + prerelease = prerelease.split('.') + + if build is None: + if partial: + build = None + else: + build = [] + else: + build = build.split('.') + + return (major, minor, patch, prerelease, build) + + def __iter__(self): + return iter((self.major, self.minor, self.patch, self.prerelease, self.build)) + + def __str__(self): + if self.minor is None: + return '%d' % self.major + elif self.patch is None: + return '%d.%d' % (self.major, self.minor) + + version = '%d.%d.%d' % (self.major, self.minor, self.patch) + + if self.prerelease: + version = '%s-%s' % (version, '.'.join(self.prerelease)) + if self.build: + version = '%s+%s' % (version, '.'.join(self.build)) + return version + + def __repr__(self): + return '<%sSemVer(%s, %s, %s, %r, %r)>' % ( + '~' if self.partial else '', + self.major, + self.minor, + self.patch, + self.prerelease, + self.build, + ) + + def _comparison_functions(self): + def prerelease_cmp(a, b): + if a and b: + return identifier_list_cmp(a, b) + elif a: + # Versions with prerelease field have lower precedence + return -1 + elif b: + return 1 + else: + return 0 + + def build_cmp(a, b): + if a and b: + return identifier_list_cmp(a, b) + elif a: + # Versions with build field have higher precedence + return 1 + elif b: + return -1 + else: + return 0 + + def make_optional(orig_cmp_fun): + @functools.wraps(orig_cmp_fun) + def alt_cmp_fun(a, b): + if a is None or b is None: + return 0 + return orig_cmp_fun(a, b) + + return alt_cmp_fun + + if self.partial: + return [ + cmp, + make_optional(cmp), + make_optional(cmp), + make_optional(prerelease_cmp), + make_optional(build_cmp), + ] + else: + return [ + cmp, + cmp, + cmp, + prerelease_cmp, + build_cmp, + ] + + def __cmp__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + + field_pairs = zip(self, other) + for cmp_fun, field_pair in zip(self._comparison_functions(), field_pairs): + self_field, other_field = field_pair + cmp_res = cmp_fun(self_field, other_field) + if cmp_res != 0: + return cmp_res + + return 0 + + +class PartialSemanticVersion(SemanticVersion): + version_re = re.compile('^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') + + @classmethod + def parse(cls, version_string): + if not version_string: + raise ValueError('Invalid version string %r.' % version_string) + + match = cls.version_re.match(version_string) + if not match: + raise ValueError('Invalid version string %r.' % version_string) + + major, minor, patch, prerelease, build = match.groups() + + major = int(major) + + if minor is None: + return (major, minor, None, None, None) + else: + minor = int(minor) + + if patch is None: + return (major, minor, patch, None, None) + else: + patch = int(patch) + + if prerelease is None: + if build: + # Build info, let's keep prerelease + prerelease = [] + else: + prerelease = None + else: + prerelease = prerelease.split('.') + + if build is None: + build = None + else: + build = build.split('.') + + return (major, minor, patch, prerelease, build) + + @classmethod + def _comparison_functions(cls): + major_cmp, minor_cmp, patch_cmp, prerelease_cmp, build_cmp = \ + super(PartialSemanticVersion, cls)._comparison_functions() + + return [ + major_cmp, + make_optional(minor_cmp), + make_optional(patch_cmp), + make_optional(prerelease_cmp), + make_optional(build_cmp), + ] + + def __str__(self): + if self.minor is None: + return '%d' % self.major + elif self.patch is None: + return '%d.%d' % (self.major, self.minor) + + version = '%d.%d.%d' % (self.major, self.minor, self.patch) + + if self.prerelease: + version = '%s-%s' % (version, '.'.join(self.prerelease)) + if self.build: + version = '%s+%s' % (version, '.'.join(self.build)) + return version + + +class RequirementSpec(object): + """A requirement specification.""" + + KIND_LT = '<' + KIND_LTE = '<=' + KIND_EQUAL = '==' + KIND_GTE = '>=' + KIND_GT = '>' + KIND_ALMOST = '~' + + re_spec = re.compile(r'^(<|<=|==|>=|>|~)(\d.*)$') + + def __init__(self, requirement_string): + kind, spec = self.parse(requirement_string) + self.kind = kind + self.spec = spec + + @classmethod + def parse(cls, requirement_string): + if not requirement_string: + raise ValueError("Invalid empty requirement specification: %r" % requirement_string) + + match = cls.re_spec.match(requirement_string) + if not match: + raise ValueError("Invalid requirement specification: %r" % requirement_string) + + kind, version = match.groups() + spec = SemanticVersion(version, partial=(kind == cls.KIND_ALMOST)) + return (kind, spec) + + def match(self, version): + if self.kind == self.KIND_LT: + return version < self.spec + elif self.kind == self.KIND_LTE: + return version <= self.spec + elif self.kind in (self.KIND_EQUAL, self.KIND_ALMOST): + # self.spec must be on left side, since it is a partial match. + return self.spec == version + elif self.kind == self.KIND_GTE: + return version >= self.spec + elif self.kind == self.KIND_GT: + return version > self.spec + else: + raise ValueError('Unexpected match kind: %r' % self.kind) + + def __str__(self): + return '%s%s' % (self.kind, self.spec) + + def __repr__(self): + return '<Spec: %s %r>' % (self.kind, self.spec) + + +def compare(v1, v2): + return cmp(SemanticVersion(v1), SemanticVersion(v2)) + + +def match(spec, version): + return RequirementSpec(spec).match(SemanticVersion(version)) |