summaryrefslogtreecommitdiff
path: root/semantic_version/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'semantic_version/base.py')
-rw-r--r--semantic_version/base.py633
1 files changed, 568 insertions, 65 deletions
diff --git a/semantic_version/base.py b/semantic_version/base.py
index 1caffa5..0049bb5 100644
--- a/semantic_version/base.py
+++ b/semantic_version/base.py
@@ -98,9 +98,9 @@ class Version:
major, minor, patch, prerelease, build = self.parse(version_string, partial)
else:
# Convenience: allow to omit prerelease/build.
+ prerelease = tuple(prerelease or ())
if not partial:
- prerelease = prerelease or ()
- build = build or ()
+ build = tuple(build or ())
self._validate_kwargs(major, minor, patch, prerelease, build, partial)
self.major = major
@@ -538,6 +538,7 @@ class SpecItem:
kind, spec = self.parse(requirement_string)
self.kind = kind
self.spec = spec
+ self._clause = Spec(requirement_string).clause
@classmethod
def parse(cls, requirement_string):
@@ -564,43 +565,17 @@ class SpecItem:
)
return (kind, spec)
+ @classmethod
+ def from_matcher(cls, matcher):
+ if matcher == Always():
+ return cls('*')
+ elif matcher == Never():
+ return cls('<0.0.0-')
+ elif isinstance(matcher, Range):
+ return cls('%s%s' % (matcher.operator, matcher.target))
+
def match(self, version):
- if self.kind == self.KIND_ANY:
- return True
- elif self.kind == self.KIND_LT:
- if version.prerelease and self.spec.prerelease is None:
- version = Version(major=version.major, minor=version.minor, patch=version.patch)
- 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:
- if version.prerelease and version.truncate() == self.spec.truncate() and self.spec.prerelease is None:
- return False
- return version != self.spec
- elif self.kind == self.KIND_CARET:
- if self.spec.major != 0:
- upper = self.spec.next_major()
- elif self.spec.minor != 0:
- upper = self.spec.next_minor()
- else:
- upper = self.spec.next_patch()
- return self.spec <= version < upper
- elif self.kind == self.KIND_TILDE:
- return self.spec <= version < self.spec.next_minor()
- elif self.kind == self.KIND_COMPATIBLE:
- if self.spec.patch is not None:
- upper = self.spec.next_minor()
- else:
- upper = self.spec.next_major()
- return self.spec <= version < upper
- else: # pragma: no cover
- raise ValueError('Unexpected match kind: %r' % self.kind)
+ return self._clause.match(version)
def __str__(self):
return '%s%s' % (self.kind, self.spec)
@@ -617,19 +592,69 @@ class SpecItem:
return hash((self.kind, self.spec))
-class Spec:
- def __init__(self, *specs_strings):
- subspecs = [self.parse(spec) for spec in specs_strings]
- self.specs = sum(subspecs, ())
+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
+
+
+DEFAULT_SYNTAX = 'simple'
+
+
+class BaseSpec:
+ """A specification of compatible versions.
+
+ Usage:
+ >>> Spec('>=1.0.0', syntax='npm')
+
+ A version matches a specification if it matches any
+ of the clauses of that specification.
+
+ Internally, a Spec is AnyOf(
+ AllOf(Matcher, Matcher, Matcher),
+ AllOf(...),
+ )
+ """
+ SYNTAXES = {}
@classmethod
- def parse(self, specs_string):
- spec_texts = specs_string.split(',')
- return tuple(SpecItem(spec_text) for spec_text in spec_texts)
+ def register_syntax(cls, subclass):
+ syntax = subclass.SYNTAX
+ if syntax is None:
+ raise ValueError("A Spec needs its SYNTAX field to be set.")
+ elif syntax in cls.SYNTAXES:
+ raise ValueError(
+ "Duplicate syntax for %s: %r, %r"
+ % (syntax, cls.SYNTAXES[syntax], subclass)
+ )
+ cls.SYNTAXES[syntax] = subclass
+ return subclass
- def match(self, version):
- """Check whether a Version satisfies the Spec."""
- return all(spec.match(version) for spec in self.specs)
+ def __init__(self, expression):
+ super().__init__()
+ self.expression = expression
+ self.clause = self._parse_to_clause(expression)
+
+ @classmethod
+ def parse(cls, expression, syntax=DEFAULT_SYNTAX):
+ """Convert a syntax-specific expression into a BaseSpec instance."""
+ return cls.SYNTAXES[syntax](expression)
+
+ @classmethod
+ def _parse_to_clause(cls, expression):
+ """Converts an expression to a clause."""
+ raise NotImplementedError()
def filter(self, versions):
"""Filter an iterable of versions satisfying the Spec."""
@@ -637,6 +662,10 @@ class Spec:
if self.match(version):
yield version
+ def match(self, version):
+ """Check whether a Version satisfies the Spec."""
+ return self.clause.match(version)
+
def select(self, versions):
"""Select the best compatible version among an iterable of options."""
options = list(self.filter(versions))
@@ -649,37 +678,511 @@ class Spec:
return self.match(version)
return False
- def __iter__(self):
- return iter(self.specs)
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.clause == other.clause
+
+ def __hash__(self):
+ return hash(self.clause)
def __str__(self):
- return ','.join(str(spec) for spec in self.specs)
+ return self.expression
def __repr__(self):
- return '<Spec: %r>' % (self.specs,)
+ return '<%s: %r>' % (self.__class__.__name__, self.expression)
+
+
+class Clause:
+ __slots__ = []
+
+ def match(self, version):
+ raise NotImplementedError()
+
+ def __and__(self, other):
+ raise NotImplementedError()
+
+ def __or__(self, other):
+ raise NotImplementedError()
def __eq__(self, other):
- if not isinstance(other, Spec):
+ raise NotImplementedError()
+
+ def __ne__(self, other):
+ return not self == other
+
+ def simplify(self):
+ return self
+
+
+class AnyOf(Clause):
+ __slots__ = ['clauses']
+
+ def __init__(self, *clauses):
+ super().__init__()
+ self.clauses = frozenset(clauses)
+
+ def match(self, version):
+ return any(c.match(version) for c in self.clauses)
+
+ def simplify(self):
+ subclauses = set()
+ for clause in self.clauses:
+ simplified = clause.simplify()
+ if isinstance(simplified, AnyOf):
+ subclauses |= simplified.clauses
+ elif simplified == Never():
+ continue
+ else:
+ subclauses.add(simplified)
+ if len(subclauses) == 1:
+ return subclauses.pop()
+ return AnyOf(*subclauses)
+
+ def __hash__(self):
+ return hash((AnyOf, self.clauses))
+
+ def __iter__(self):
+ return iter(self.clauses)
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.clauses == other.clauses
+
+ def __and__(self, other):
+ if isinstance(other, AllOf):
+ return other & self
+ elif isinstance(other, Matcher) or isinstance(other, AnyOf):
+ return AllOf(self, other)
+ else:
return NotImplemented
- return set(self.specs) == set(other.specs)
+ def __or__(self, other):
+ if isinstance(other, AnyOf):
+ clauses = list(self.clauses | other.clauses)
+ elif isinstance(other, Matcher) or isinstance(other, AllOf):
+ clauses = list(self.clauses | set([other]))
+ else:
+ return NotImplemented
+ return AnyOf(*clauses)
+
+ def __repr__(self):
+ return 'AnyOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses))
+
+
+class AllOf(Clause):
+ __slots__ = ['clauses']
+
+ def __init__(self, *clauses):
+ super().__init__()
+ self.clauses = frozenset(clauses)
+
+ def match(self, version):
+ return all(clause.match(version) for clause in self.clauses)
+
+ def simplify(self):
+ subclauses = set()
+ for clause in self.clauses:
+ simplified = clause.simplify()
+ if isinstance(simplified, AllOf):
+ subclauses |= simplified.clauses
+ elif simplified == Always():
+ continue
+ else:
+ subclauses.add(simplified)
+ if len(subclauses) == 1:
+ return subclauses.pop()
+ return AllOf(*subclauses)
def __hash__(self):
- return hash(self.specs)
+ return hash((AllOf, self.clauses))
+ def __iter__(self):
+ return iter(self.clauses)
-def compare(v1, v2):
- return base_cmp(Version(v1), Version(v2))
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.clauses == other.clauses
+ def __and__(self, other):
+ if isinstance(other, Matcher) or isinstance(other, AnyOf):
+ clauses = list(self.clauses | set([other]))
+ elif isinstance(other, AllOf):
+ clauses = list(self.clauses | other.clauses)
+ else:
+ return NotImplemented
+ return AllOf(*clauses)
+
+ def __or__(self, other):
+ if isinstance(other, AnyOf):
+ return other | self
+ elif isinstance(other, Matcher):
+ return AnyOf(self, AllOf(other))
+ elif isinstance(other, AllOf):
+ return AnyOf(self, other)
+ else:
+ return NotImplemented
-def match(spec, version):
- return Spec(spec).match(Version(version))
+ def __repr__(self):
+ return 'AllOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses))
-def validate(version_string):
- """Validates a version string againt the SemVer specification."""
- try:
- Version.parse(version_string)
- return True
- except ValueError:
+class Matcher(Clause):
+ __slots__ = []
+
+ def __and__(self, other):
+ if isinstance(other, AllOf):
+ return other & self
+ elif isinstance(other, Matcher) or isinstance(other, AnyOf):
+ return AllOf(self, other)
+ else:
+ return NotImplemented
+
+ def __or__(self, other):
+ if isinstance(other, AnyOf):
+ return other | self
+ elif isinstance(other, Matcher) or isinstance(other, AllOf):
+ return AnyOf(self, other)
+ else:
+ return NotImplemented
+
+
+class Never(Matcher):
+ __slots__ = []
+
+ def match(self, version):
return False
+
+ def __hash__(self):
+ return hash((Never,))
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ def __and__(self, other):
+ return self
+
+ def __or__(self, other):
+ return other
+
+ def __repr__(self):
+ return 'Never()'
+
+
+class Always(Matcher):
+ __slots__ = []
+
+ def match(self, version):
+ return True
+
+ def __hash__(self):
+ return hash((Always,))
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ def __and__(self, other):
+ return other
+
+ def __or__(self, other):
+ return self
+
+ def __repr__(self):
+ return 'Always()'
+
+
+class Range(Matcher):
+ OP_EQ = '=='
+ OP_GT = '>'
+ OP_GTE = '>='
+ OP_LT = '<'
+ OP_LTE = '<='
+ OP_NEQ = '!='
+
+ # <1.2.3 matches 1.2.3-a1
+ PRERELEASE_ALWAYS = 'always'
+ # <1.2.3 does not match 1.2.3-a1
+ PRERELEASE_NATURAL = 'natural'
+ # 1.2.3-a1 is only considered if target == 1.2.3-xxx
+ PRERELEASE_SAMEPATCH = 'same-patch'
+
+ # 1.2.3 matches 1.2.3+*
+ BUILD_IMPLICIT = 'implicit'
+ # 1.2.3 matches only 1.2.3, not 1.2.3+4
+ BUILD_STRICT = 'strict'
+
+ __slots__ = ['operator', 'target', 'prerelease_policy', 'build_policy']
+
+ def __init__(self, operator, target, prerelease_policy=PRERELEASE_NATURAL, build_policy=BUILD_IMPLICIT):
+ super().__init__()
+ if target.build and operator not in (self.OP_EQ, self.OP_NEQ):
+ raise ValueError(
+ "Invalid range %s%s: build numbers have no ordering."
+ % (operator, target))
+ self.operator = operator
+ self.target = target
+ self.prerelease_policy = prerelease_policy
+ self.build_policy = self.BUILD_STRICT if target.build else build_policy
+
+ def match(self, version):
+ if self.build_policy != self.BUILD_STRICT:
+ version = version.truncate('prerelease')
+
+ if version.prerelease:
+ same_patch = self.target.truncate() == version.truncate()
+
+ if self.prerelease_policy == self.PRERELEASE_SAMEPATCH and not same_patch:
+ return False
+
+ if self.operator == self.OP_EQ:
+ if self.build_policy == self.BUILD_STRICT:
+ return (
+ self.target.truncate('prerelease') == version.truncate('prerelease')
+ and version.build == self.target.build
+ )
+ return version == self.target
+ elif self.operator == self.OP_GT:
+ return version > self.target
+ elif self.operator == self.OP_GTE:
+ return version >= self.target
+ elif self.operator == self.OP_LT:
+ if (
+ version.prerelease
+ and self.prerelease_policy == self.PRERELEASE_NATURAL
+ and version.truncate() == self.target.truncate()
+ and not self.target.prerelease
+ ):
+ return False
+ return version < self.target
+ elif self.operator == self.OP_LTE:
+ return version <= self.target
+ else:
+ assert self.operator == self.OP_NEQ
+ if self.build_policy == self.BUILD_STRICT:
+ return not (
+ self.target.truncate('prerelease') == version.truncate('prerelease')
+ and version.build == self.target.build
+ )
+
+ if (
+ version.prerelease
+ and self.prerelease_policy == self.PRERELEASE_NATURAL
+ and version.truncate() == self.target.truncate()
+ and not self.target.prerelease
+ ):
+ return False
+ return version != self.target
+
+ def __hash__(self):
+ return hash((Range, self.operator, self.target, self.prerelease_policy))
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, self.__class__)
+ and self.operator == other.operator
+ and self.target == other.target
+ and self.prerelease_policy == other.prerelease_policy
+ )
+
+ def __str__(self):
+ return '%s%s' % (self.operator, self.target)
+
+ def __repr__(self):
+ policy_part = (
+ '' if self.prerelease_policy == self.PRERELEASE_NATURAL
+ else ', prerelease_policy=%r' % self.prerelease_policy
+ ) + (
+ '' if self.build_policy == self.BUILD_IMPLICIT
+ else ', build_policy=%r' % self.build_policy
+ )
+ return 'Range(%r, %r%s)' % (
+ self.operator,
+ self.target,
+ policy_part,
+ )
+
+
+@BaseSpec.register_syntax
+class Spec(BaseSpec):
+
+ SYNTAX = 'simple'
+
+ def __init__(self, expression, *legacy_extras):
+ expression = ','.join((expression,) + legacy_extras)
+ super().__init__(expression)
+
+ def __iter__(self):
+ for clause in self.clause:
+ yield SpecItem.from_matcher(clause)
+
+ @classmethod
+ def _parse_to_clause(cls, expression):
+ return cls.Parser.parse(expression)
+
+ class Parser:
+ NUMBER = r'\*|0|[1-9][0-9]*'
+ NAIVE_SPEC = re.compile(r"""^
+ (?P<op><|<=||=|==|>=|>|!=|\^|~|~=)
+ (?P<major>{nb})(?:\.(?P<minor>{nb})(?:\.(?P<patch>{nb}))?)?
+ (?:-(?P<prerel>[a-z0-9A-Z.-]*))?
+ (?:\+(?P<build>[a-z0-9A-Z.-]*))?
+ $
+ """.format(nb=NUMBER),
+ re.VERBOSE,
+ )
+
+ @classmethod
+ def parse(cls, expression):
+ blocks = expression.split(',')
+ clause = Always()
+ for block in blocks:
+ if not cls.NAIVE_SPEC.match(block):
+ raise ValueError("Invalid simple block %r" % block)
+ clause &= cls.parse_block(block)
+
+ return clause
+
+ PREFIX_CARET = '^'
+ PREFIX_TILDE = '~'
+ PREFIX_COMPATIBLE = '~='
+ PREFIX_EQ = '=='
+ PREFIX_NEQ = '!='
+ PREFIX_GT = '>'
+ PREFIX_GTE = '>='
+ PREFIX_LT = '<'
+ PREFIX_LTE = '<='
+
+ PREFIX_ALIASES = {
+ '=': PREFIX_EQ,
+ '': PREFIX_EQ,
+ }
+
+ EMPTY_VALUES = ['*', 'x', 'X', None]
+
+ @classmethod
+ def parse_block(cls, expr):
+ if not cls.NAIVE_SPEC.match(expr):
+ raise ValueError("Invalid simple spec component: %r" % expr)
+ prefix, major_t, minor_t, patch_t, prerel, build = cls.NAIVE_SPEC.match(expr).groups()
+ prefix = cls.PREFIX_ALIASES.get(prefix, prefix)
+
+ major = None if major_t in cls.EMPTY_VALUES else int(major_t)
+ minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t)
+ patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t)
+
+ if major is None: # '*'
+ target = Version(major=0, minor=0, patch=0)
+ if prefix not in (cls.PREFIX_EQ, cls.PREFIX_GTE):
+ raise ValueError("Invalid simple spec: %r" % expr)
+ elif minor is None:
+ target = Version(major=major, minor=0, patch=0)
+ elif patch is None:
+ target = Version(major=major, minor=minor, patch=0)
+ else:
+ target = Version(
+ major=major,
+ minor=minor,
+ patch=patch,
+ prerelease=prerel.split('.') if prerel else (),
+ build=build.split('.') if build else (),
+ )
+
+ if (major is None or minor is None or patch is None) and (prerel or build):
+ raise ValueError("Invalid simple spec: %r" % expr)
+
+ if build is not None and prefix not in (cls.PREFIX_EQ, cls.PREFIX_NEQ):
+ raise ValueError("Invalid simple spec: %r" % expr)
+
+ if prefix == cls.PREFIX_CARET:
+ # Accept anything with the same most-significant digit
+ if target.major:
+ high = target.next_major()
+ elif target.minor:
+ high = target.next_minor()
+ else:
+ high = target.next_patch()
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+ elif prefix == cls.PREFIX_TILDE:
+ assert major is not None
+ # Accept any higher patch in the same minor
+ # Might go higher if the initial version was a partial
+ if minor is None:
+ high = target.next_major()
+ else:
+ high = target.next_minor()
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+ elif prefix == cls.PREFIX_COMPATIBLE:
+ assert major is not None
+ # ~1 is 1.0.0..2.0.0; ~=2.2 is 2.2.0..3.0.0; ~=1.4.5 is 1.4.5..1.5.0
+ if minor is None or patch is None:
+ # We got a partial version
+ high = target.next_major()
+ else:
+ high = target.next_minor()
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+ elif prefix == cls.PREFIX_EQ:
+ if major is None:
+ return Range(Range.OP_GTE, target)
+ elif minor is None:
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, target.next_major())
+ elif patch is None:
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, target.next_patch())
+ elif build == '':
+ return Range(Range.OP_EQ, target, build_policy=Range.BUILD_STRICT)
+ else:
+ return Range(Range.OP_EQ, target)
+
+ elif prefix == cls.PREFIX_NEQ:
+ assert major is not None
+ if minor is None:
+ # !=1.x => <1.0.0 || >=2.0.0
+ return Range(Range.OP_LT, target) | Range(Range.OP_GTE, target.next_major())
+ elif patch is None:
+ # !=1.2.x => <1.2.0 || >=1.3.0
+ return Range(Range.OP_LT, target) | Range(Range.OP_GTE, target.next_minor())
+ elif prerel == '':
+ # !=1.2.3-
+ return Range(Range.OP_NEQ, target, prerelease_policy=Range.PRERELEASE_ALWAYS)
+ elif build == '':
+ # !=1.2.3+ or !=1.2.3-a2+
+ return Range(Range.OP_NEQ, target, build_policy=Range.BUILD_STRICT)
+ else:
+ return Range(Range.OP_NEQ, target)
+
+ elif prefix == cls.PREFIX_GT:
+ assert major is not None
+ if minor is None:
+ # >1.x => >=2.0
+ return Range(Range.OP_GTE, target.next_major())
+ elif patch is None:
+ return Range(Range.OP_GTE, target.next_minor())
+ else:
+ return Range(Range.OP_GT, target)
+
+ elif prefix == cls.PREFIX_GTE:
+ return Range(Range.OP_GTE, target)
+
+ elif prefix == cls.PREFIX_LT:
+ assert major is not None
+ if prerel == '':
+ # <1.2.3-
+ return Range(Range.OP_LT, target, prerelease_policy=Range.PRERELEASE_ALWAYS)
+ return Range(Range.OP_LT, target)
+
+ else:
+ assert prefix == cls.PREFIX_LTE
+ assert major is not None
+ if minor is None:
+ # <=1.x => <2.0
+ return Range(Range.OP_LT, target.next_major())
+ elif patch is None:
+ return Range(Range.OP_LT, target.next_minor())
+ else:
+ return Range(Range.OP_LTE, target)
+
+
+
+
+