summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polytechnique.org>2012-05-14 23:37:42 +0200
committerRaphaël Barrois <raphael.barrois@polytechnique.org>2012-05-14 23:37:42 +0200
commitae1d1ade64d5f0a5dab9b1c9bb8f933708c03694 (patch)
tree36e75f1d946756ebf0cbaec0c38fcce420362eac
parent868e2a7bb86bc7586aa6f686d8f5af41fa3a0a92 (diff)
downloadsemantic-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__.py118
-rw-r--r--src/semantic_version/base.py344
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))