summaryrefslogtreecommitdiff
path: root/semantic_version
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polytechnique.org>2019-08-23 23:24:50 +0200
committerRaphaël Barrois <raphael.barrois@polytechnique.org>2019-08-26 21:34:10 +0200
commitc4c6ab0e925d8cfabb68d34a10a783cb854b63a0 (patch)
tree84895b9dd586594c380668875ede00ba4b8a928a /semantic_version
parent5b9174aedaf9843ee5b3b6358461910e328e74d1 (diff)
downloadsemantic-version-c4c6ab0e925d8cfabb68d34a10a783cb854b63a0.tar.gz
Add support for NPM-style version ranges.
The code follows closely the specification available at https://docs.npmjs.com/misc/semver.html. Despite similarities, the matching logic is fully separate from the `native` code, since both might evolve at their own scales.
Diffstat (limited to 'semantic_version')
-rw-r--r--semantic_version/__init__.py2
-rw-r--r--semantic_version/base.py194
2 files changed, 195 insertions, 1 deletions
diff --git a/semantic_version/__init__.py b/semantic_version/__init__.py
index ca30919..e04e0ba 100644
--- a/semantic_version/__init__.py
+++ b/semantic_version/__init__.py
@@ -3,7 +3,7 @@
# This code is distributed under the two-clause BSD License.
-from .base import compare, match, validate, NativeSpec, Spec, SpecItem, Version
+from .base import compare, match, validate, NativeSpec, NpmSpec, Spec, SpecItem, Version
__author__ = "Raphaël Barrois <raphael.barrois+semver@polytechnique.org>"
diff --git a/semantic_version/base.py b/semantic_version/base.py
index 0049bb5..9b517ca 100644
--- a/semantic_version/base.py
+++ b/semantic_version/base.py
@@ -1183,6 +1183,200 @@ class Spec(BaseSpec):
return Range(Range.OP_LTE, target)
+@BaseSpec.register_syntax
+class NpmSpec(BaseSpec):
+ SYNTAX = 'npm'
+
+ @classmethod
+ def _parse_to_clause(cls, expression):
+ return cls.Parser.parse(expression)
+
+ class Parser:
+ JOINER = '||'
+ HYPHEN = ' - '
+
+ NUMBER = r'x|X|\*|0|[1-9][0-9]*'
+ PART = r'[a-zA-Z0-9.-]*'
+ NPM_SPEC_BLOCK = re.compile(r"""
+ ^(?:v)? # Strip optional initial v
+ (?P<op><|<=|>=|>|=|^|~|) # Operator, can be empty
+ (?P<major>{nb})(?:\.(?P<minor>{nb})(?:\.(?P<patch>{nb}))?)?
+ (?:-(?P<prerel>{part}))? # Optional re-release
+ (?:\+(?P<build>{part}))? # Optional build
+ $""".format(nb=NUMBER, part=PART),
+ re.VERBOSE,
+ )
+
+ @classmethod
+ def range(cls, operator, target):
+ return Range(operator, target, prerelease_policy=Range.PRERELEASE_SAMEPATCH)
+
+ @classmethod
+ def parse(cls, expression):
+ result = Never()
+ groups = expression.split(cls.JOINER)
+ for group in groups:
+ group = group.strip()
+ if not group:
+ group = '>=0.0.0'
+
+ subclauses = []
+ if cls.HYPHEN in group:
+ low, high = group.split(cls.HYPHEN, 2)
+ subclauses = cls.parse_simple('>=' + low) + cls.parse_simple('<=' + high)
+ else:
+ blocks = group.split(' ')
+ for block in blocks:
+ if not cls.NPM_SPEC_BLOCK.match(block):
+ raise ValueError("Invalid NPM block in %r: %r" % (expression, block))
+
+ subclauses.extend(cls.parse_simple(block))
+
+ prerelease_clauses = []
+ non_prerel_clauses = []
+ for clause in subclauses:
+ if clause.target.prerelease:
+ if clause.operator in (Range.OP_GT, Range.OP_GTE):
+ prerelease_clauses.append(Range(
+ operator=Range.OP_LT,
+ target=Version(
+ major=clause.target.major,
+ minor=clause.target.minor,
+ patch=clause.target.patch + 1,
+ ),
+ prerelease_policy=Range.PRERELEASE_ALWAYS,
+ ))
+ elif clause.operator in (Range.OP_LT, Range.OP_LTE):
+ prerelease_clauses.append(cls.range(
+ operator=Range.OP_GTE,
+ target=Version(
+ major=clause.target.major,
+ minor=clause.target.minor,
+ patch=0,
+ prerelease=(),
+ ),
+ ))
+ prerelease_clauses.append(clause)
+ non_prerel_clauses.append(cls.range(
+ operator=clause.operator,
+ target=clause.target.truncate(),
+ ))
+ else:
+ non_prerel_clauses.append(clause)
+ if prerelease_clauses:
+ result |= AllOf(*prerelease_clauses)
+ result |= AllOf(*non_prerel_clauses)
+
+ return result
+ PREFIX_CARET = '^'
+ PREFIX_TILDE = '~'
+ PREFIX_EQ = '='
+ PREFIX_GT = '>'
+ PREFIX_GTE = '>='
+ PREFIX_LT = '<'
+ PREFIX_LTE = '<='
+ PREFIX_ALIASES = {
+ '': PREFIX_EQ,
+ }
+
+ PREFIX_TO_OPERATOR = {
+ PREFIX_EQ: Range.OP_EQ,
+ PREFIX_LT: Range.OP_LT,
+ PREFIX_LTE: Range.OP_LTE,
+ PREFIX_GTE: Range.OP_GTE,
+ PREFIX_GT: Range.OP_GT,
+ }
+
+ EMPTY_VALUES = ['*', 'x', 'X', None]
+
+ @classmethod
+ def parse_simple(cls, simple):
+ match = cls.NPM_SPEC_BLOCK.match(simple)
+
+ prefix, major_t, minor_t, patch_t, prerel, build = match.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 build is not None and prefix not in [cls.PREFIX_EQ]:
+ # Ignore the 'build' part when not comparing to a specific part.
+ build = None
+
+ if major is None: # '*', 'x', 'X'
+ target = Version(major=0, minor=0, patch=0)
+ if prefix not in [cls.PREFIX_EQ, cls.PREFIX_GTE]:
+ raise ValueError("Invalid expression %r" % simple)
+ prefix = cls.PREFIX_GTE
+ 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 NPM spec: %r" % simple)
+
+ if prefix == cls.PREFIX_CARET:
+ if target.major: # ^1.2.4 => >=1.2.4 <2.0.0
+ high = target.next_major()
+ elif target.minor: # ^0.1.2 => >=0.1.2 <0.2.0
+ high = target.next_minor()
+ else: # ^0.0.1 => >=0.0.1 <0.0.2
+ high = target.next_patch()
+ return [cls.range(Range.OP_GTE, target), cls.Range(Range.OP_LT, high)]
+
+ elif prefix == cls.PREFIX_TILDE:
+ assert major is not None
+ if minor is None: # ~1.x => >=1.0.0 <2.0.0
+ high = target.next_major()
+ else: # ~1.2.x => >=1.2.0 <1.3.0; ~1.2.3 => >=1.2.3 <1.3.0
+ high = target.next_minor()
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, high)]
+
+ elif prefix == cls.PREFIX_EQ:
+ if major is None:
+ return [cls.range(Range.OP_GTE, target)]
+ elif minor is None:
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, target.next_major())]
+ elif patch is None:
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, target.next_minor())]
+ else:
+ return [cls.range(Range.OP_EQ, target)]
+
+ elif prefix == cls.PREFIX_GT:
+ assert major is not None
+ if minor is None: # >1.x
+ return [cls.range(Range.OP_GTE, target.next_major())]
+ elif patch is None: # >1.2.x => >=1.3.0
+ return [cls.range(Range.OP_GTE, target.next_minor())]
+ else:
+ return [cls.range(Range.OP_GT, target)]
+
+ elif prefix == cls.PREFIX_GTE:
+ return [cls.range(Range.OP_GTE, target)]
+
+ elif prefix == cls.PREFIX_LT:
+ assert major is not None
+ return [cls.range(Range.OP_LT, target)]
+
+ else:
+ assert prefix == cls.PREFIX_LTE
+ assert major is not None
+ if minor is None: # <=1.x => <2.0.0
+ return [cls.range(Range.OP_LT, target.next_major())]
+ elif patch is None: # <=1.2.x => <1.3.0
+ return [cls.range(Range.OP_LT, target.next_minor())]
+ else:
+ return [cls.range(Range.OP_LTE, target)]