summaryrefslogtreecommitdiff
path: root/semantic_version
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polyconseil.fr>2013-12-23 20:31:42 +0100
committerRaphaël Barrois <raphael.barrois@polyconseil.fr>2013-12-23 20:48:57 +0100
commit5550834eae424ac5cfa223b75bdb281fa8b9478f (patch)
treec0a23fc6a6fa416d2a6b31dc4ac54c73f22e50d5 /semantic_version
parent9a9aca22fee237a9f0eba4b2c293279b9ed09f46 (diff)
downloadsemantic-version-5550834eae424ac5cfa223b75bdb281fa8b9478f.tar.gz
Normalize docs to docs/ (Closes #5).
Also normalize the package layout. Thanks @jdowner-gb & tleach for the report.
Diffstat (limited to 'semantic_version')
-rw-r--r--semantic_version/__init__.py10
-rw-r--r--semantic_version/base.py490
-rw-r--r--semantic_version/compat.py18
-rw-r--r--semantic_version/django_fields.py100
4 files changed, 618 insertions, 0 deletions
diff --git a/semantic_version/__init__.py b/semantic_version/__init__.py
new file mode 100644
index 0000000..6ced92a
--- /dev/null
+++ b/semantic_version/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2013 Raphaël Barrois
+# This code is distributed under the two-clause BSD License.
+
+
+__author__ = "Raphaël Barrois <raphael.barrois+semver@polytechnique.org>"
+__version__ = '2.2.1'
+
+
+from .base import compare, match, validate, Spec, SpecItem, Version
diff --git a/semantic_version/base.py b/semantic_version/base.py
new file mode 100644
index 0000000..f5153b2
--- /dev/null
+++ b/semantic_version/base.py
@@ -0,0 +1,490 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2013 Raphaël Barrois
+# This code is distributed under the two-clause BSD License.
+
+from __future__ import unicode_literals
+
+import functools
+import re
+
+
+from .compat import base_cmp
+
+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 base_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 base_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 base_cmp(len(a), len(b))
+
+
+class Version(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 _coerce(cls, value, allow_none=False):
+ if value is None and allow_none:
+ return value
+ return int(value)
+
+ @classmethod
+ def coerce(cls, version_string, partial=False):
+ """Coerce an arbitrary version string into a semver-compatible one.
+
+ The rule is:
+ - If not enough components, fill minor/patch with zeroes; unless
+ partial=True
+ - If more than 3 dot-separated components, extra components are "build"
+ data. If some "build" data already appeared, append it to the
+ extra components
+
+ Examples:
+ >>> Version.coerce('0.1')
+ Version(0, 1, 0)
+ >>> Version.coerce('0.1.2.3')
+ Version(0, 1, 2, (), ('3',))
+ >>> Version.coerce('0.1.2.3+4')
+ Version(0, 1, 2, (), ('3', '4'))
+ >>> Version.coerce('0.1+2-3+4_5')
+ Version(0, 1, 0, (), ('2-3', '4-5'))
+ """
+ base_re = re.compile(r'^\d+(?:\.\d+(?:\.\d+)?)?')
+
+ match = base_re.match(version_string)
+ if not match:
+ raise ValueError("Version string lacks a numerical component: %r"
+ % version_string)
+
+ version = version_string[:match.end()]
+ if not partial:
+ # We need a not-partial version.
+ while version.count('.') < 2:
+ version += '.0'
+
+ if match.end() == len(version_string):
+ return Version(version, partial=partial)
+
+ rest = version_string[match.end():]
+
+ # Cleanup the 'rest'
+ rest = re.sub(r'[^a-zA-Z0-9+.-]', '-', rest)
+
+ if rest[0] == '+':
+ # A 'build' component
+ prerelease = ''
+ build = rest[1:]
+ elif rest[0] == '.':
+ # An extra version component, probably 'build'
+ prerelease = ''
+ build = rest[1:]
+ elif rest[0] == '-':
+ rest = rest[1:]
+ if '+' in rest:
+ prerelease, build = rest.split('+', 1)
+ else:
+ prerelease, build = rest, ''
+ elif '+' in rest:
+ prerelease, build = rest.split('+', 1)
+ else:
+ prerelease, build = rest, ''
+
+ build = build.replace('+', '.')
+
+ if prerelease:
+ version = '%s-%s' % (version, prerelease)
+ if build:
+ version = '%s+%s' % (version, build)
+
+ return cls(version, partial=partial)
+
+ @classmethod
+ def parse(cls, version_string, partial=False, coerce=False):
+ """Parse a version string into a Version() object.
+
+ Args:
+ version_string (str), the version string to parse
+ partial (bool), whether to accept incomplete input
+ coerce (bool), whether to try to map the passed in string into a
+ valid Version.
+ """
+ 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)
+ minor = cls._coerce(minor, partial)
+ patch = cls._coerce(patch, partial)
+
+ if prerelease is None:
+ if partial and (build is None):
+ # No build info, strip here
+ return (major, minor, patch, None, None)
+ else:
+ prerelease = ()
+ elif prerelease == '':
+ prerelease = ()
+ else:
+ prerelease = tuple(prerelease.split('.'))
+
+ if build is None:
+ if partial:
+ build = None
+ else:
+ build = ()
+ elif build == '':
+ build = ()
+ else:
+ build = tuple(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):
+ version = '%d' % self.major
+ if self.minor is not None:
+ version = '%s.%d' % (version, self.minor)
+ if self.patch is not None:
+ version = '%s.%d' % (version, self.patch)
+
+ if self.prerelease or (self.partial and self.prerelease == () and self.build is None):
+ version = '%s-%s' % (version, '.'.join(self.prerelease))
+ if self.build or (self.partial and self.build == ()):
+ version = '%s+%s' % (version, '.'.join(self.build))
+ return version
+
+ def __repr__(self):
+ return 'Version(%r%s)' % (
+ str(self),
+ ', partial=True' if self.partial else '',
+ )
+
+ @classmethod
+ def _comparison_functions(cls, partial=False):
+ """Retrieve comparison methods to apply on version components.
+
+ This is a private API.
+
+ Args:
+ partial (bool): whether to provide 'partial' or 'strict' matching.
+
+ Returns:
+ 5-tuple of cmp-like functions.
+ """
+
+ def prerelease_cmp(a, b):
+ """Compare prerelease components.
+
+ Special rule: a version without prerelease component has higher
+ precedence than one with a prerelease component.
+ """
+ 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):
+ """Compare build components.
+
+ Special rule: a version without build component has lower
+ precedence than one with a build component.
+ """
+ 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):
+ """Convert a cmp-like function to consider 'None == *'."""
+ @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 partial:
+ return [
+ base_cmp, # Major is still mandatory
+ make_optional(base_cmp),
+ make_optional(base_cmp),
+ make_optional(prerelease_cmp),
+ make_optional(build_cmp),
+ ]
+ else:
+ return [
+ base_cmp,
+ base_cmp,
+ base_cmp,
+ prerelease_cmp,
+ build_cmp,
+ ]
+
+ def __cmp__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ field_pairs = zip(self, other)
+ comparison_functions = self._comparison_functions(partial=self.partial or other.partial)
+ comparisons = zip(comparison_functions, self, other)
+
+ for cmp_fun, self_field, other_field in comparisons:
+ cmp_res = cmp_fun(self_field, other_field)
+ if cmp_res != 0:
+ return cmp_res
+
+ return 0
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) == 0
+
+ def __hash__(self):
+ return hash((self.major, self.minor, self.patch, self.prerelease, self.build))
+
+ def __ne__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) != 0
+
+ def __lt__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) < 0
+
+ def __le__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) <= 0
+
+ def __gt__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) > 0
+
+ def __ge__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) >= 0
+
+
+class SpecItem(object):
+ """A requirement specification."""
+
+ KIND_LT = '<'
+ KIND_LTE = '<='
+ KIND_EQUAL = '=='
+ KIND_GTE = '>='
+ KIND_GT = '>'
+ KIND_NEQ = '!='
+
+ STRICT_KINDS = (
+ KIND_LT,
+ KIND_LTE,
+ KIND_EQUAL,
+ KIND_GTE,
+ KIND_GT,
+ KIND_NEQ,
+ )
+
+ 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 = Version(version, partial=True)
+ 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 == self.KIND_EQUAL:
+ return version == self.spec
+ elif self.kind == self.KIND_GTE:
+ return version >= self.spec
+ elif self.kind == self.KIND_GT:
+ return version > self.spec
+ elif self.kind == self.KIND_NEQ:
+ return version != self.spec
+ else: # pragma: no cover
+ raise ValueError('Unexpected match kind: %r' % self.kind)
+
+ def __str__(self):
+ return '%s%s' % (self.kind, self.spec)
+
+ def __repr__(self):
+ return '<SpecItem: %s %r>' % (self.kind, self.spec)
+
+ def __eq__(self, other):
+ if not isinstance(other, SpecItem):
+ return NotImplemented
+ return self.kind == other.kind and self.spec == other.spec
+
+ def __hash__(self):
+ return hash((self.kind, self.spec))
+
+
+class Spec(object):
+ def __init__(self, *specs_strings):
+ subspecs = [self.parse(spec) for spec in specs_strings]
+ self.specs = sum(subspecs, ())
+
+ @classmethod
+ def parse(self, specs_string):
+ spec_texts = specs_string.split(',')
+ return tuple(SpecItem(spec_text) for spec_text in spec_texts)
+
+ def match(self, version):
+ """Check whether a Version satisfies the Spec."""
+ return all(spec.match(version) for spec in self.specs)
+
+ def filter(self, versions):
+ """Filter an iterable of versions satisfying the Spec."""
+ for version in versions:
+ if self.match(version):
+ yield version
+
+ def select(self, versions):
+ """Select the best compatible version among an iterable of options."""
+ options = list(self.filter(versions))
+ if options:
+ return max(options)
+ return None
+
+ def __contains__(self, version):
+ if isinstance(version, Version):
+ return self.match(version)
+ return False
+
+ def __iter__(self):
+ return iter(self.specs)
+
+ def __str__(self):
+ return ','.join(str(spec) for spec in self.specs)
+
+ def __repr__(self):
+ return '<Spec: %r>' % (self.specs,)
+
+ def __eq__(self, other):
+ if not isinstance(other, Spec):
+ return NotImplemented
+
+ return set(self.specs) == set(other.specs)
+
+ def __hash__(self):
+ return hash(self.specs)
+
+
+def compare(v1, v2):
+ return base_cmp(Version(v1), Version(v2))
+
+
+def match(spec, version):
+ return Spec(spec).match(Version(version))
+
+
+def validate(version_string):
+ """Validates a version string againt the SemVer specification."""
+ try:
+ Version.parse(version_string)
+ return True
+ except ValueError:
+ return False
diff --git a/semantic_version/compat.py b/semantic_version/compat.py
new file mode 100644
index 0000000..51102fc
--- /dev/null
+++ b/semantic_version/compat.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2013 Raphaël Barrois
+# This code is distributed under the two-clause BSD License.
+
+import sys
+
+is_python2 = (sys.version_info[0] == 2)
+
+if is_python2: # pragma: no cover
+ base_cmp = cmp
+else: # pragma: no cover
+ def base_cmp(x, y):
+ if x < y:
+ return -1
+ elif x > y:
+ return 1
+ else:
+ return 0
diff --git a/semantic_version/django_fields.py b/semantic_version/django_fields.py
new file mode 100644
index 0000000..6a70129
--- /dev/null
+++ b/semantic_version/django_fields.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2013 Raphaël Barrois
+# This code is distributed under the two-clause BSD License.
+
+from __future__ import unicode_literals
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from . import base
+
+
+class BaseSemVerField(models.CharField):
+ __metaclass__ = models.SubfieldBase
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('max_length', 200)
+ super(BaseSemVerField, self).__init__(*args, **kwargs)
+
+ def get_prep_value(self, obj):
+ return str(obj)
+
+ def get_db_prep_value(self, value, connection, prepared=False):
+ if not prepared:
+ value = self.get_prep_value(value)
+ return value
+
+ def value_to_string(self, obj):
+ value = self.to_python(self._get_val_from_obj(obj))
+ return str(value)
+
+ def run_validators(self, value):
+ return super(BaseSemVerField, self).run_validators(str(value))
+
+
+# Py2 and Py3-compatible metaclass
+SemVerField = models.SubfieldBase(
+ str('SemVerField'), (BaseSemVerField, models.CharField), {})
+
+
+class VersionField(SemVerField):
+ default_error_messages = {
+ 'invalid': _("Enter a valid version number in X.Y.Z format."),
+ }
+ description = _("Version")
+
+ def __init__(self, *args, **kwargs):
+ self.partial = kwargs.pop('partial', False)
+ self.coerce = kwargs.pop('coerce', False)
+ super(VersionField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ """Converts any value to a base.Version field."""
+ if value is None or value == '':
+ return value
+ if isinstance(value, base.Version):
+ return value
+ if self.coerce:
+ return base.Version.coerce(value, partial=self.partial)
+ else:
+ return base.Version(value, partial=self.partial)
+
+
+class SpecField(SemVerField):
+ default_error_messages = {
+ 'invalid': _("Enter a valid version number spec list in ==X.Y.Z,>=A.B.C format."),
+ }
+ description = _("Version specification list")
+
+ def to_python(self, value):
+ """Converts any value to a base.Spec field."""
+ if value is None or value == '':
+ return value
+ if isinstance(value, base.Spec):
+ return value
+ return base.Spec(value)
+
+
+def add_south_rules():
+ from south.modelsinspector import add_introspection_rules
+
+ add_introspection_rules([
+ (
+ (VersionField,),
+ [],
+ {
+ 'partial': ('partial', {'default': False}),
+ 'coerce': ('coerce', {'default': False}),
+ },
+ ),
+ ], ["semantic_version\.django_fields"])
+
+
+try: # pragma: no cover
+ import south
+except ImportError: # pragma: no cover
+ south = None
+
+if south: # pragma: no cover
+ add_south_rules()