You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1420 lines
46 KiB
1420 lines
46 KiB
# -*- 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 '<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))
|
|
|
|
|
|
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<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)
|
|
|
|
|
|
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<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(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)]
|
|
|