# -*- coding: utf-8 -*- # Copyright (c) The python-semanticversion project # This code is distributed under the two-clause BSD License. import functools import re import warnings def _has_leading_zero(value): return (value and value[0] == '0' and value.isdigit() and value != '0') class MaxIdentifier(object): __slots__ = [] def __repr__(self): return 'MaxIdentifier()' def __eq__(self, other): return isinstance(other, self.__class__) @functools.total_ordering class NumericIdentifier(object): __slots__ = ['value'] def __init__(self, value): self.value = int(value) def __repr__(self): return 'NumericIdentifier(%r)' % self.value def __eq__(self, other): if isinstance(other, NumericIdentifier): return self.value == other.value return NotImplemented def __lt__(self, other): if isinstance(other, MaxIdentifier): return True elif isinstance(other, AlphaIdentifier): return True elif isinstance(other, NumericIdentifier): return self.value < other.value else: return NotImplemented @functools.total_ordering class AlphaIdentifier(object): __slots__ = ['value'] def __init__(self, value): self.value = value.encode('ascii') def __repr__(self): return 'AlphaIdentifier(%r)' % self.value def __eq__(self, other): if isinstance(other, AlphaIdentifier): return self.value == other.value return NotImplemented def __lt__(self, other): if isinstance(other, MaxIdentifier): return True elif isinstance(other, NumericIdentifier): return False elif isinstance(other, AlphaIdentifier): return self.value < other.value else: return NotImplemented class Version(object): version_re = re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') partial_version_re = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-([0-9a-zA-Z.-]*))?(?:\+([0-9a-zA-Z.-]*))?$') def __init__( self, version_string=None, major=None, minor=None, patch=None, prerelease=None, build=None, partial=False): if partial: warnings.warn( "Partial versions will be removed in 3.0; use SimpleSpec('1.x.x') instead.", DeprecationWarning, stacklevel=2, ) has_text = version_string is not None has_parts = not (major is minor is patch is prerelease is build is None) if not has_text ^ has_parts: raise ValueError("Call either Version('1.2.3') or Version(major=1, ...).") if has_text: major, minor, patch, prerelease, build = self.parse(version_string, partial) else: # Convenience: allow to omit prerelease/build. prerelease = tuple(prerelease or ()) if not partial: build = tuple(build or ()) self._validate_kwargs(major, minor, patch, prerelease, build, 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) def next_major(self): if self.prerelease and self.minor == self.patch == 0: return Version( major=self.major, minor=0, patch=0, partial=self.partial, ) else: return Version( major=self.major + 1, minor=0, patch=0, partial=self.partial, ) def next_minor(self): if self.prerelease and self.patch == 0: return Version( major=self.major, minor=self.minor, patch=0, partial=self.partial, ) else: return Version( major=self.major, minor=self.minor + 1, patch=0, partial=self.partial, ) def next_patch(self): if self.prerelease: return Version( major=self.major, minor=self.minor, patch=self.patch, partial=self.partial, ) else: return Version( major=self.major, minor=self.minor, patch=self.patch + 1, partial=self.partial, ) def truncate(self, level='patch'): """Return a new Version object, truncated up to the selected level.""" if level == 'build': return self elif level == 'prerelease': return Version( major=self.major, minor=self.minor, patch=self.patch, prerelease=self.prerelease, partial=self.partial, ) elif level == 'patch': return Version( major=self.major, minor=self.minor, patch=self.patch, partial=self.partial, ) elif level == 'minor': return Version( major=self.major, minor=self.minor, patch=None if self.partial else 0, partial=self.partial, ) elif level == 'major': return Version( major=self.major, minor=None if self.partial else 0, patch=None if self.partial else 0, partial=self.partial, ) else: raise ValueError("Invalid truncation level `%s`." % level) @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' # Strip leading zeros in components # Version is of the form nn, nn.pp or nn.pp.qq version = '.'.join( # If the part was '0', we end up with an empty string. part.lstrip('0') or '0' for part in version.split('.') ) 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() if _has_leading_zero(major): raise ValueError("Invalid leading zero in major: %r" % version_string) if _has_leading_zero(minor): raise ValueError("Invalid leading zero in minor: %r" % version_string) if _has_leading_zero(patch): raise ValueError("Invalid leading zero in patch: %r" % version_string) 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('.')) cls._validate_identifiers(prerelease, allow_leading_zeroes=False) if build is None: if partial: build = None else: build = () elif build == '': build = () else: build = tuple(build.split('.')) cls._validate_identifiers(build, allow_leading_zeroes=True) return (major, minor, patch, prerelease, build) @classmethod def _validate_identifiers(cls, identifiers, allow_leading_zeroes=False): for item in identifiers: if not item: raise ValueError( "Invalid empty identifier %r in %r" % (item, '.'.join(identifiers)) ) if item[0] == '0' and item.isdigit() and item != '0' and not allow_leading_zeroes: raise ValueError("Invalid leading zero in identifier %r" % item) @classmethod def _validate_kwargs(cls, major, minor, patch, prerelease, build, partial): if ( major != int(major) or minor != cls._coerce(minor, partial) or patch != cls._coerce(patch, partial) or prerelease is None and not partial or build is None and not partial ): raise ValueError( "Invalid kwargs to Version(major=%r, minor=%r, patch=%r, " "prerelease=%r, build=%r, partial=%r" % ( major, minor, patch, prerelease, build, partial )) if prerelease is not None: cls._validate_identifiers(prerelease, allow_leading_zeroes=False) if build is not None: cls._validate_identifiers(build, allow_leading_zeroes=True) 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 '%s(%r%s)' % ( self.__class__.__name__, str(self), ', partial=True' if self.partial else '', ) def __hash__(self): # We don't include 'partial', since this is strictly equivalent to having # at least a field being `None`. return hash((self.major, self.minor, self.patch, self.prerelease, self.build)) @property def precedence_key(self): if self.prerelease: prerelease_key = tuple( NumericIdentifier(part) if re.match(r'^[0-9]+$', part) else AlphaIdentifier(part) for part in self.prerelease ) else: prerelease_key = ( MaxIdentifier(), ) return ( self.major, self.minor, self.patch, prerelease_key, ) def __cmp__(self, other): if not isinstance(other, self.__class__): return NotImplemented if self < other: return -1 elif self > other: return 1 elif self == other: return 0 else: return NotImplemented def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented return ( self.major == other.major and self.minor == other.minor and self.patch == other.patch and (self.prerelease or ()) == (other.prerelease or ()) and (self.build or ()) == (other.build or ()) ) def __ne__(self, other): if not isinstance(other, self.__class__): return NotImplemented return tuple(self) != tuple(other) def __lt__(self, other): if not isinstance(other, self.__class__): return NotImplemented return self.precedence_key < other.precedence_key def __le__(self, other): if not isinstance(other, self.__class__): return NotImplemented return self.precedence_key <= other.precedence_key def __gt__(self, other): if not isinstance(other, self.__class__): return NotImplemented return self.precedence_key > other.precedence_key def __ge__(self, other): if not isinstance(other, self.__class__): return NotImplemented return self.precedence_key >= other.precedence_key class SpecItem(object): """A requirement specification.""" KIND_ANY = '*' KIND_LT = '<' KIND_LTE = '<=' KIND_EQUAL = '==' KIND_SHORTEQ = '=' KIND_EMPTY = '' KIND_GTE = '>=' KIND_GT = '>' KIND_NEQ = '!=' KIND_CARET = '^' KIND_TILDE = '~' KIND_COMPATIBLE = '~=' # Map a kind alias to its full version KIND_ALIASES = { KIND_SHORTEQ: KIND_EQUAL, KIND_EMPTY: KIND_EQUAL, } re_spec = re.compile(r'^(<|<=||=|==|>=|>|!=|\^|~|~=)(\d.*)$') def __init__(self, requirement_string, _warn=True): if _warn: warnings.warn( "The `SpecItem` class will be removed in 3.0.", DeprecationWarning, stacklevel=2, ) kind, spec = self.parse(requirement_string) self.kind = kind self.spec = spec self._clause = Spec(requirement_string).clause @classmethod def parse(cls, requirement_string): if not requirement_string: raise ValueError("Invalid empty requirement specification: %r" % requirement_string) # Special case: the 'any' version spec. if requirement_string == '*': return (cls.KIND_ANY, '') match = cls.re_spec.match(requirement_string) if not match: raise ValueError("Invalid requirement specification: %r" % requirement_string) kind, version = match.groups() if kind in cls.KIND_ALIASES: kind = cls.KIND_ALIASES[kind] spec = Version(version, partial=True) if spec.build is not None and kind not in (cls.KIND_EQUAL, cls.KIND_NEQ): raise ValueError( "Invalid requirement specification %r: build numbers have no ordering." % requirement_string ) return (kind, spec) @classmethod def from_matcher(cls, matcher): if matcher == Always(): return cls('*', _warn=False) elif matcher == Never(): return cls('<0.0.0-', _warn=False) elif isinstance(matcher, Range): return cls('%s%s' % (matcher.operator, matcher.target), _warn=False) def match(self, version): return self._clause.match(version) def __str__(self): return '%s%s' % (self.kind, self.spec) def __repr__(self): return '' % (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)) def compare(v1, v2): return Version(v1).__cmp__(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(object): """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 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 __init__(self, expression): super(BaseSpec, self).__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.""" for version in versions: 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)) if options: return max(options) return None def __contains__(self, version): """Whether `version in self`.""" if isinstance(version, Version): return self.match(version) return False 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 self.expression def __repr__(self): return '<%s: %r>' % (self.__class__.__name__, self.expression) class Clause(object): __slots__ = [] def match(self, version): raise NotImplementedError() def __and__(self, other): raise NotImplementedError() def __or__(self, other): raise NotImplementedError() def __eq__(self, other): raise NotImplementedError() def prettyprint(self, indent='\t'): """Pretty-print the clause. """ return '\n'.join(self._pretty()).replace('\t', indent) def _pretty(self): """Actual pretty-printing logic. Yields: A list of string. Indentation is performed with \t. """ yield repr(self) def __ne__(self, other): return not self == other def simplify(self): return self class AnyOf(Clause): __slots__ = ['clauses'] def __init__(self, *clauses): super(AnyOf, self).__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 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)) def _pretty(self): yield 'AnyOF(' for clause in self.clauses: lines = list(clause._pretty()) for line in lines[:-1]: yield '\t' + line yield '\t' + lines[-1] + ',' yield ')' class AllOf(Clause): __slots__ = ['clauses'] def __init__(self, *clauses): super(AllOf, self).__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((AllOf, 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, 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 __repr__(self): return 'AllOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses)) def _pretty(self): yield 'AllOF(' for clause in self.clauses: lines = list(clause._pretty()) for line in lines[:-1]: yield '\t' + line yield '\t' + lines[-1] + ',' yield ')' 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(Range, self).__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 SimpleSpec(BaseSpec): SYNTAX = 'simple' @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<|<=||=|==|>=|>|!=|\^|~|~=) (?P{nb})(?:\.(?P{nb})(?:\.(?P{nb}))?)? (?:-(?P[a-z0-9A-Z.-]*))? (?:\+(?P[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) class LegacySpec(SimpleSpec): def __init__(self, *expressions): warnings.warn( "The Spec() class will be removed in 3.1; use SimpleSpec() instead.", PendingDeprecationWarning, stacklevel=2, ) if len(expressions) > 1: warnings.warn( "Passing 2+ arguments to SimpleSpec will be removed in 3.0; concatenate them with ',' instead.", DeprecationWarning, stacklevel=2, ) expression = ','.join(expressions) super(LegacySpec, self).__init__(expression) @property def specs(self): return list(self) def __iter__(self): warnings.warn( "Iterating over the components of a SimpleSpec object will be removed in 3.0.", DeprecationWarning, stacklevel=2, ) try: clauses = list(self.clause) except TypeError: # Not an iterable clauses = [self.clause] for clause in clauses: yield SpecItem.from_matcher(clause) Spec = LegacySpec @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<|<=|>=|>|=|\^|~|) # Operator, can be empty (?P{nb})(?:\.(?P{nb})(?:\.(?P{nb}))?)? (?:-(?P{part}))? # Optional re-release (?:\+(?P{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(Range( operator=Range.OP_GTE, target=Version( major=clause.target.major, minor=clause.target.minor, patch=0, prerelease=(), ), prerelease_policy=Range.PRERELEASE_ALWAYS, )) 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 ; ^1.x => >=1.0.0 <2.0.0 high = target.truncate().next_major() elif target.minor: # ^0.1.2 => >=0.1.2 <0.2.0 high = target.truncate().next_minor() elif minor is None: # ^0.x => >=0.0.0 <1.0.0 high = target.truncate().next_major() elif patch is None: # ^0.2.x => >=0.2.0 <0.3.0 high = target.truncate().next_minor() else: # ^0.0.1 => >=0.0.1 <0.0.2 high = target.truncate().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)]